一 threading模块介绍
multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍
二 开启线程的两种方式
2.1.1 Thread类直接创建
from threading importThreadimporttimedefsayhi(name):
time.sleep(2)print('%s say hello' %name)if __name__ == '__main__':
t=Thread(target=sayhi,args=('egon',))
t.start()print('主线程')
方式一
2.1.2 Thread类继承式创建
from threading importThreadimporttimeclassMyThread(Thread):def __init__(self,num):
Thread.__init__(self)
self.num=numdefrun(self):print("running on number:%s" %self.num)
time.sleep(3)
t1=MyThread(56)
t2=MyThread(78)
t1.start()
t2.start()print("ending")
方式二
2.2 Thread类的实例方法
join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞(join写在start之后)
setDaemon(True):
将线程声明为守护线程,必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起。
当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦
当主线程执行完,要等非守护线程完成,而此时守护线程并没有死,等待的过程中可以执行
importthreadingfrom time importctime,sleepimporttimedefMusic(name):print ("Begin listening to {name}. {time}".format(name=name,time=ctime()))
sleep(3)print("end listening {time}".format(time=ctime()))defBlog(title):print ("Begin recording the {title}. {time}".format(title=title,time=ctime()))
sleep(5)print('end recording {time}'.format(time=ctime()))
threads=[]
t1= threading.Thread(target=Music,args=('FILL ME',))
t2= threading.Thread(target=Blog,args=('',))
threads.append(t1)
threads.append(t2)if __name__ == '__main__':#t2.setDaemon(True)
for t inthreads:#t.setDaemon(True) #注意:一定在start之前设置
t.start()#t.join()
#t1.join()
#t2.join() # 考虑这三种join位置下的结果?
print ("all over %s" %ctime())
daemon
A boolean value indicating whether this thread is a daemon thread (True) or not (False). This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread; the main thread is not a daemon thread and therefore all threads created in the main thread default to daemon = False.
The entire Python program exits when no alive non-daemon threads are left.
当daemon被设置为True时,如果主线程退出,那么子线程也将跟着退出,(注意退出不是死,)
反之,子线程将继续运行,直到正常退出。
daemon
2.2.2 其它方法
Thread实例对象的方法
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。
threading模块提供的一些方法:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
2.3 GIL(全局解释器锁)
定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
Python中的线程是操作系统的原生线程,Python虚拟机使用一个全局解释器锁(Global Interpreter Lock)来互斥线程对Python虚拟机的使用。为了支持多线程机制,一个基本的要求就是需要实现不同线程对共享资源访问的互斥,所以引入了GIL。
GIL:在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。
在调用任何Python C API之前,要先获得GIL
GIL缺点:多处理器退化为单处理器;优点:避免大量的加锁解锁操作
2.3.1 GIL的早期设计
Python支持多线程,而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,并且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?
2.3.2 GIL的影响
无论你启多少个线程,你有多少个cpu, Python在执行一个进程的时候会淡定的在同一时刻只允许一个线程运行。
所以,python是无法利用多核CPU实现多线程的。
这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
计算密集型:
#coding:utf8
from threading importThreadimporttimedefcounter():
i=0for _ in range(50000000):
i= i + 1
returnTruedefmain():
l=[]
start_time=time.time()for i in range(2):
t= Thread(target=counter)
t.start()
l.append(t)
t.join()#for t in l:
#t.join()
end_time=time.time()print("Total time: {}".format(end_time -start_time))if __name__ == '__main__':
main()'''py2.7:
串行:25.4523348808s
并发:31.4084379673s
py3.5:
串行:8.62115597724914s
并发:8.99609899520874s'''
2.3.3 解决方案
用multiprocessing替代Thread multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。
#coding:utf8
from multiprocessing importProcessimporttimedefcounter():
i=0for _ in range(40000000):
i= i + 1
returnTruedefmain():
l=[]
start_time=time.time()for _ in range(2):
t=Process(target=counter)
t.start()
l.append(t)#t.join()
for t inl:
t.join()
end_time=time.time()print("Total time: {}".format(end_time -start_time))if __name__ == '__main__':
main()'''py2.7:
串行:6.1565990448 s
并行:3.1639978885 s
py3.5:
串行:6.556925058364868 s
并发:3.5378448963165283 s'''
当然multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。
总结:因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能 - 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或