本章将了解python的多线程机制和GIL,了解threading模块与thread模块的区别和关系,熟练掌握使用threading.Thread进行多线程执行。
在多线程(Multithread)编程出现之前,电脑程序的运行由一个执行序列组成,执行序列按顺序在主机的CPU运行。即使子任务相互独立,互相无关时也是按照一条线的顺序执行。
所以多线程编程的目的就是并行的运行这些相互独立的子任务,大幅度的提高整个任务的效率。
多线程编程对于某些任务是非常理想的,这些任务通常具有以下特点:
一个顺序执行的程序要从每个IO终端信道检查用户输入时,程序无论如何也不能在读取IO终端信道的时候阻塞。因为用户输入的到达是不确定的,阻塞会导致其他IO信息的数据不能被处理。所以顺序执行通常使用非阻塞IO,或带有计时器的阻塞IO。
计算机程序只不过是磁盘中可执行的二进制的数据,他们只有在被读取到内存中,被操作系统调用的时候才开始他们的生命周期。
进程使程序的一次执行。**每个进程都有自己独立的地址空间、内存、数据栈以及其他记录其运行轨迹的辅助数据。**操作系统管理在其上运行的所有进程,并为这些进程公平的分配时间。进程也可以通过fork和spawn来完成其他任务。不过各个进程之间相互独立,所以只能使用进程间通讯(IPC)的方式,而不能直接共享信息。
线程跟进程有些相似,不同的是,所有线程运行在同一个进程中,共享相同的运行环境。他们可以想象成是在进程中并行运行的迷你进程。线程有开始、顺序执行、结束三部分。它有一个自己的指令指针,记录自己运行到什么地方,线程的运行可能被抢占(中断),或者暂时的被挂起(也叫睡眠),让其他的线程运行,这叫做让步。
一个进程中的各个线程之间共享一片数据空间,所以线程之间可以更加方便的共享数据以及相互通信。
然而实际中,对于单核CPU,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU让出来(系统中断),让其他的线程运行。关于进程与线程更多的内容需要去了解《操作系统原理》。
Python代码的执行由python虚拟机(解释器主循环)来控制。对于python虚拟机的访问由全局解释器锁(GIL)来控制,这个锁保证同一时刻只有一个线程在运行。
在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止。对此感兴趣的可以查看Python/ceval.c文件。
当一个线程结束计算时,就算是退出。。线程可以调用thread.exit()或者使用python退出进程的标准方法,如sys.exit()结束线程。不过,你不可以直接kill一个线程。
通常有两种方式:
这里先从thread模块开始学习。thread模块中,除了产生线程之外,thread模块也提供了基本的同步数据结构的锁对象。start_new_thread()函数是thread模块中的关键函数,其参数为:函数、函数的参数以及可选的关键字参数。这个函数会产生一个新线程来运行。
thread模块函数还有:
LockType类型对象方法:
# thread.start_new_thread()使用例子
import thread
from time import ctime,sleep
def loop0():
print 'start loop 0 at: ',ctime()
sleep(4)
print 'loop 0 down at ',ctime()
def loop1():
print 'start loop 1 at: ',ctime()
sleep(2)
print 'loop 1 down at ',ctime()
def main():
print 'start at: ',ctime()
thread.start_new_thread(loop0,())
thread.start_new_thread(loop1,())
sleep(6)
print 'all done at: ',ctime()
if __name__ == '__main__':
main()
# start at: Tue Jan 21 15:31:13 2020
# start loop 0 at: Tue Jan 21 15:31:13 2020
# start loop 1 at: Tue Jan 21 15:31:13 2020
# loop 1 down at Tue Jan 21 15:31:15 2020
# loop 0 down at Tue Jan 21 15:31:17 2020
# all done at: Tue Jan 21 15:31:19 2020
其中在主函数中用到了sleep(6),这是因为两个loop分别只用4秒与2秒,sleep在此处的作用是同步线程,如果没有让主线程停止下来,那么主线程会过早的退出,会连带退出子线程。
对于线程同步,使用sleep显然不是最好的办法,此处使用sleep的前提是已知两个线程的执行时间,而线程的大部分使用场景并不能满足这样的前提。此时就要使用多线程管理中的锁了。将之前的程序用锁改进后:
import thread
from time import ctime,sleep
loops=[4,2]
def loop(nloop,nsec,lock):
print 'start loop ',nloop, 'at: ',ctime()
sleep(nsec)
print 'loop ',nloop, ' down at ',ctime()
lock.release()
def main():
print 'start at: ',ctime()
locks=[]
nloops=range(len(loops))
for i in nloops:
lock=thread.allocate_lock()
lock.acquire()
locks.append(lock)
for i in nloops:
thread.start_new_thread(loop,(i,loops[i],locks[i]))
for i in nloops:
while locks[i].locked():pass
print 'all done at: ',ctime()
if __name__ == '__main__':
main()
两段函数主要的区别在于main()函数中,通过三个for循环,我们先调用thread.allocate_lock()函数创建一个锁列表,并分别调用acquire()获得锁。第二个循环开始线程,第三个循环按照顺序检测锁是否被释放(release)。
接下来来看更为高级的threading模块。threading模块对象:
另一个不推荐使用thread模块的原因是,它不支持守护线程。当主线程退出时,所有子线程无论是否在工作,都会被强制退出。
threading模块支持守护线程(deamon):如果设定一个线程为守护线程,就表示这个线程是不重要的,在晋城退出时,不用等待这个线程退出,例如服务器线程通常就是运行在一个无限循环中。
threading的Thread类是主要的运行对象,有许多thread模块里没有的函数。Thread对象的函数:
现在将上面的例子,用threading模块进行重写:
import threading
from time import ctime,sleep
loops=[4,2]
def loop(nloop,nsec):
print 'start loop ',nloop, 'at: ',ctime()
sleep(nsec)
print 'loop ',nloop, ' down at ',ctime()
def main():
print 'start at: ',ctime()
threads=[]
nloops=range(len(loops))
for i in nloops:
t=threading.Thread(target=loop,args=(i,loops[i]))
threads.append(t)
for i in nloops:
threads[i].start()
for i in nloops:
threads[i].join()
print 'all done at: ',ctime()
if __name__ == '__main__':
main()
与使用thread模块不同的是,threading可以将所有进程创建好后,再一起调用start()函数启动线程,而不是创建一个启动一个。而且不需要再管理锁(分配、获得、释放、检查锁),只需要简单的对每个线程调用join()函数就可以。
join()函数会等到线程结束,或在给了timeout参数的时候,等到超时(也叫spinlock)。
如果你的主线程除了线程结束外,还有其他事情要做,就不需要join,只有在需要等待线程结束才进行接下来的步骤时需要使用join()。
threading模块的其他函数:
生产者生产货物,放入一个队列。消费者消耗生产者的货物。
生产货物所花费的时间和消耗货物的时间均不确定。
Queue模块函数:
Queue对象函数:
Queue模块可以用来进行线程间的通信,让各个线程共享数据。生产者-消费者代码:
# producer-consumer
from random import randint
from time import sleep
from Queue import Queue
import threading
# 创建对象放入队列
def writeQ(queue):
print 'produce object for Q ...'
queue.put('xxx', 1)
print 'size now: ', queue.qsize()
# 从队列中取出一个对象,get中的block会在队列为空的时候阻塞函数
def readQ(queue):
val=queue.get(1)
print 'consume object from Q... size now: ', queue.qsize()
def writer(queue,loops):
for i in range(loops):
writeQ(queue)
sleep(randint(1,3))
def reader(queue,loops):
for i in range(loops):
readQ(queue)
sleep(randint(1,3))
funcs=[writer,reader]
nfuncs=range(len(funcs))
# 两个线程,一个运行生产者,一个运行消费者
def main():
nloops=randint(2,5)
q=Queue(32)
threads=[]
for i in nfuncs:
t=threading.Thread(target=funcs[i], args=(q,nloops))
threads.append(t)
for i in nfuncs:
threads[i].start()
for i in nfuncs:
threads[i].join()
print 'all done'
if __name__ == '__main__':
main()