当初在刚学习python多线程时,上网搜索资料几乎都是一片倒的反应python没有真正意义上的多线程,python多线程就是鸡肋。当时不明因此,只是了解到python带有GIL解释器锁的概念,同一时刻只能有一个线程在运行,遇到IO操做才会释放切换。那么,python多线程是否真的很鸡肋呢?要解决这个疑惑,我想必须亲自动手测试。
通过对比python与java的多线程测试,我发现python多线程的效率确实不如java,但远尚未达到鸡肋的程度,那么跟其余机制相比较呢?java
展转了多篇博文,我看到了一些网友的观点,以为应该使用python多进程来代替多线程的需求,由于多进程不受GIL的限制。因而我便动手使用多进程去解决一些并发问题,期间也遇到了一些坑,所幸大部分查找资料解决了,而后对多进程作了简单汇总介绍Python多进程。
那么是否多进程能彻底替代多线程呢?别急,咱们继续往下看。python
协程的概念目前来讲是比较火热的,协程不一样于线程的地方在于协程不是操做系统进行切换,而是由程序员编码进行切换的,也就是说切换是由程序员控制的,这样就没有了线程所谓的安全问题。协程的概念很是广而深,本文暂不作具体介绍,之后会单独成文。程序员
好了,网上的观点无非是使用多进程或者协程来代替多线程(固然换编程语言,换解释器之类方法除外),那么咱们就来测试下这三者的性能之差。既然要公平测试,就应该考虑IO密集型与CPU密集型的问题,因此分两组数据进行测试。编程
测试IO密集型,我选择最经常使用的爬虫功能,计算爬虫访问bing所须要的时间。(主要测试多线程与协程,单线程与多进程就不测了,由于没有必要)
测试代码:安全
Python服务器
#! -*- coding:utf-8 -*- from gevent import monkey;monkey.patch_all() import gevent import time import threading import urllib2 def urllib2_(url): try: urllib2.urlopen(url,timeout=10).read() except Exception,e: print e def gevent_(urls): jobs=[gevent.spawn(urllib2_,url) for url in urls] gevent.joinall(jobs,timeout=10) for i in jobs: i.join() def thread_(urls): a=[] for url in urls: t=threading.Thread(target=urllib2_,args=(url,)) a.append(t) for i in a: i.start() for i in a: i.join() if __name__=="__main__": urls=["https://www.bing.com/"]*10 t1=time.time() gevent_(urls) t2=time.time() print 'gevent-time:%s' % str(t2-t1) thread_(urls) t4=time.time() print 'thread-time:%s' % str(t4-t2)
CPU密集型测试结果:多线程
访问10次 gevent-time:0.380326032639 thread-time:0.376606941223 访问50次 gevent-time:1.3358900547 thread-time:1.59564089775 访问100次 gevent-time:2.42984986305 thread-time:2.5669670105 访问300次 gevent-time:6.66330099106 thread-time:10.7605059147
从结果能够看出,当并发数不断增大时,协程的效率确实比多线程要高,但在并发数不是那么高时,二者差别不大。并发
CPU密集型,我选择科学计算的一些功能,计算所需时间。(主要测试单线程、多线程、协程、多进程)
测试代码:app
Python编程语言
#! -*- coding:utf-8 -*- from multiprocessing import Process as pro from multiprocessing.dummy import Process as thr from gevent import monkey;monkey.patch_all() import gevent def run(i): lists=range(i) list(set(lists)) if __name__=="__main__": ''' 多进程 ''' for i in range(30): ##10-2.1s 20-3.8s 30-5.9s t=pro(target=run,args=(5000000,)) t.start() ''' 多线程 ''' # for i in range(30): ##10-3.8s 20-7.6s 30-11.4s # t=thr(target=run,args=(5000000,)) # t.start() ''' 协程 ''' # jobs=[gevent.spawn(run,5000000) for i in range(30)] ##10-4.0s 20-7.7s 30-11.5s # gevent.joinall(jobs) # for i in jobs: # i.join() ''' 单线程 ''' # for i in range(30): ##10-3.5s 20-7.6s 30-11.3s # run(5000000)
并发10次:【多进程】2.1s 【多线程】3.8s 【协程】4.0s 【单线程】3.5s测试结果:
能够看到,在CPU密集型的测试下,多进程效果明显比其余的好,多线程、协程与单线程效果差很少。这是由于只有多进程彻底使用了CPU的计算能力。在代码运行时,咱们也可以看到,只有多进程能够将CPU使用率占满。
从两组数据咱们不难发现,python多线程并无那么鸡肋。如若否则,Python3为什么不去除GIL呢?对于此问题,Python社区也有两派意见,这里再也不论述,咱们应该尊重Python之父的决定。
至于什么时候该用多线程,什么时候用多进程,什么时候用协程?想必答案已经很明显了。
当咱们须要编写并发爬虫等IO密集型的程序时,应该选用多线程或者协程(亲测差距不是特别明显);当咱们须要科学计算,设计CPU密集型程序,应该选用多进程。固然以上结论的前提是,不作分布式,只在一台服务器上测试。
答案已经给出,本文是否就此收尾?既然已经论述Python多线程尚有用武之地,那么就来介绍介绍其用法吧。
Multiprocessing.dummy用法与多进程Multiprocessing用法相似,只是在import包的时候,加上.dummy。
用法参考Multiprocessing用法
这是python自带的threading多线程模块,其建立多线程主要有2种方式。一种为继承threading类,另外一种使用threading.Thread函数,接下来将会分别介绍这两种用法。
利用threading.Thread()函数建立线程。
代码:
Python
def run(i): print i for i in range(10): t=threading.Thread(target=run,args=(i,)) t.start()
线程对象的方法:说明:Thread()函数有2个参数,一个是target,内容为子线程要执行的函数名称;另外一个是args,内容为须要传递的参数。建立完子线程,将会返回一个对象,调用对象的start方法,能够启动子线程。
threading类的方法:
经过继承threading类,建立线程。
代码:
Python
import threading class test(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): try: print "code one" except: pass for i in range(10): cur=test() cur.start() for i in range(10): cur.join()
获取线程返回值问题说明:此方法继承了threading类,而且重构了run函数功能。
有时候,咱们每每须要获取每一个子线程的返回值。然而经过调用普通函数,获取return值的方式在多线程中并不适用。所以须要一种新的方式去获取子线程返回值。
代码:
Python
import threading class test(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.tag=1 def get_result(self): if self.tag==1: return True else: return False f=test() f.start() while f.isAlive(): continue print f.get_result()
说明:多线程获取返回值的首要问题,就是子线程何时结束?咱们应该何时去获取返回值?可使用isAlive()方法判断子线程是否存活。
当须要执行的任务很是多时,咱们每每须要控制线程的数量,threading类自带有控制线程数量的方法。
代码:
Python
import threading maxs=10 ##并发的线程数量 threadLimiter=threading.BoundedSemaphore(maxs) class test(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): threadLimiter.acquire() #获取 try: print "code one" except: pass finally: threadLimiter.release() #释放 for i in range(100): cur=test() cur.start() for i in range(100): cur.join()
说明:以上程序能够控制多线程并发数为10,超过这个数量会引起异常。
除了自带的方法,咱们还能够设计其余方案:
Python
threads=[] ''' 建立全部线程 ''' for i in range(10): t=threading.Thread(target=run,args=(i,)) threads.append(t) ''' 启动列表中的线程 ''' for t in threads: t.start() while True: #判断正在运行的线程数量,若是小于5则退出while循环, #进入for循环启动新的进程.不然就一直在while循环进入死循环 if(len(threading.enumerate())<5): break
线程池以上两种方式皆能够,本人更喜欢用下面那种方式。
Python
import threadpool def ThreadFun(arg1,arg2): pass def main(): device_list=[object1,object2,object3......,objectn]#须要处理的设备个数 task_pool=threadpool.ThreadPool(8)#8是线程池中线程的个数 request_list=[]#存听任务列表 #首先构造任务列表 for device in device_list: request_list.append(threadpool.makeRequests(ThreadFun,[((device, ), {})])) #将每一个任务放到线程池中,等待线程池中线程各自读取任务,而后进行处理,使用了map函数,不了解的能够去了解一下。 map(task_pool.putRequest,request_list) #等待全部任务处理完成,则返回,若是没有处理完,则一直阻塞 task_pool.poll() if __name__=="__main__": main()
多进程问题,能够赶赴Python多进程现场,其余关于多线程问题,能够下方留言讨 论