线程和Python
全局解释锁
Python代码的执行是由Python虚拟机进行控制的。Python在设计时是这样考虑的,在主循环中同时只能有一个控制线程在执行,就像单核CPU系统中的多线程一样。内存中可以有许多程序,但是在任意给定时刻只能有一个程序在运行。同理,尽管Python解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。
对Python虚拟机的访问是由全局解释锁(GIL)控制的。这个锁就是用来保证同时只能有一个线程运行的。
在多线程环境中,Python虚拟机将按照下面所述的方式执行。
- 设置GIL
- 切换进一个线程去运行
- 执行下面操作之一 (a.指定数量的字节码指令 b.线程主动让出控制权)
- 切换出线程
- 解锁GIL
- 重复上述步骤
当调用外部代码时,GIL会保持锁定,直至函数执行结束(因为在这期间没有Python字节码计数??)
不适用线程的情况
#该脚本在一个单线程程序里连续执行两个循环。一个循环必须在另一个开始前完成。总共消耗的时间是每个虚幻所用时间之和
from time import sleep,ctime
def loop0():
print('start loop 0 at :',ctime())
sleep(4)
print('loop 0 done at :',ctime())
def loop1():
print('start loop 1 at:',ctime())
sleep(2)
print('loop 1 done at : ',ctime())
def main():
print('staring at :',ctime())
loop0()
loop1()
print('all Done at :',ctime())
if __name__=='__main__':
main()
使用thread模板
#这里执行的循环和上面一样,这次使用了多线程机制,两个循环是并发执行的。总的运行时间由慢的决定
import _thread
from time import sleep,ctime
def loop0():
print('start loop 0 at:',ctime())
sleep(4)
print('loop 0 done at:',ctime())
def loop1():
print('start loop 1 at:',ctime())
sleep(2)
print('loop 1 done at : ',ctime())
def main():
print('staring at :',ctime())
_thread.start_new_thread(loop0,())
_thread.start_new_thread(loop1,())
sleep(6) #如果没有这条语句,则会直接执行all Done语句,然后退出,loop0()和loop1()这两个线程也会直接中止
print('all Done at :',ctime())
if __name__=='__main__':
main()
改进sleep(6),引入锁
引入锁之后,我们不需要再像mtsleepA那样等待额外的时间后才结束。通过使用锁,我们可以在所有线程全部完成执行后立即退出
import _thread
from time import sleep,ctime
loops = [4,2]
def loop(nloop,nsec,lock):
print('start loop ',nloop,'at:',ctime())
sleep(nsec)
print('loop',nloop,' done at:',ctime())
lock.release()
def main():
print ('staring 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()
threading模块
现在介绍级别更高的threading模块。
核心提示:守护线程
避免使用thread模块的另一个原因是该模块不支持守护线程这个概念,当主线程退出时,所有子线程都将中止,不管它们是否在工作。
threading模块支持守护线程,其工作方式是:守护线程一般是一个等待客户端请求服务的服务器。如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。
如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置守护线程标记,该标记值为真时,表示该线程是不重要的,或者说该线程只是用来等待客户端请求而不做任何事情。
要将一个线程设置为守护线程,需要在启动线程之前执行如下赋值语句:thread.daemon=True.一个新的子线程会继承父线程的守护标记。
使用Thread类,可以有很多方法来创建线程。我们将介绍其中比较相似的三种方案(更倾向最后一种)
- 创建Thread的实例,传给它一个函数。
- 创建Thread的实例,传给它一个可调用的类实例。
- 派生Thread的子类,并创建子类的实例。
创建Thread的实例,传给它一个函数
在第一个例子中,我们只是把Thread类实例化,然后将函数(及其参数)传递进去,和之前例子中采用的方式一样。
import threading
from time import sleep,ctime
loops = [4,2]
def loop(nloop,nsec):
print ('start loop ', nloop, 'at:', ctime ())
sleep (nsec)
print ('loop', nloop, ' done at:', ctime ())
def main():
print('starting 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(调用Thread())和调用thread.start_new_thread()的最大区别是新线程不会立即开始执行。这是一个非常有用的同步功能,尤其是当你并不希望线程立即开始执行时。
当所有线程都分配完成之后,通过调用每个线程的start()方法让它们开始执行,而不是在这之前就会执行。
相比于管理一组锁(分配、获取、释放、检查锁状态等)而言,这里只需要为每个线程调用join()方法即可。join()方法将等待线程结束,或者在提供了超时时间的情况下,达到超时时间。使用join()方法要比等待锁释放的无限循环更加清晰(这也是这种锁又称为自旋锁的原因)。
对于join()方法而言,其另一个重要方面是其实它根本不需要调用。一旦线程启动,它们就会一直执行,直到给定的函数完成后退出。如果主线程还有其他事情要去做,而不是等待这些线程完成(例如其他处理或者等待新的客户端请求),就可以不调用join()。join()方法只有在你需要等待线程完成的时候才是有用的。
派生Thread的子类,并创建子类的实例
#派生Thread的子类,并创建子类的实例
#本例中将对Thread子类化,而不是直接对其实例化。这将使我们在定制线程对象时拥有更多的灵活性,也能够简化线程创建的调用过程。
import threading
from time import sleep,ctime
loops = (4,2)
class MyThread(threading.Thread):
def __init__(self,func,args,name=''):
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
def run(self):
self.func(*self.args)
def loop(nloop,nsec):
print ('start loop ', nloop, 'at:', ctime ())
sleep (nsec)
print ('loop', nloop, ' done at:', ctime ())
def main():
print ('starting at:', ctime ())
threads = []
nloops = range (len (loops))
for i in nloops:
t = MyThread(loop,(i,loops[i]),loop.__name__)
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()
现在,对MyThread类进行修改,增加一些调试信息的输出,并将其存储为一个名为myThread的独立模块,以便在接下来的例子中导入这个类。除了简单地调用函数外,还将把结果保存在实例属性self.res中,并创建一个新的方法getResult()来获取这个值。
#派生Thread的子类,并创建子类的实例
import threading
from time import sleep,ctime
loops = (4,2)
class MyThread(threading.Thread):
def __init__(self,func,args,name=''):
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
def getResult(self):
return self.res
def run(self):
print ('start loop ', self.name, 'at:', ctime ())
self.res = self.func(*self.args)
print (self.name, ' finished at:', ctime ())
同步原语
同步。一般在多线程代码中,总会有一些特定的函数或代码块不希望被多个线程同时执行,通常包括修改数据库,更新文件或其他会产生竞态条件的类似情况。下面使用两种类型的同步原语演示几个示例程序:锁/互斥,信号量。
锁是所有机制中最简单,最低级的机制,而信号量用于多线程竞争有限资源的情况。
锁示例
当多线程争夺锁时,允许第一个获得锁的线程进入临界区,并执行代码。所有之后到达的线程将被阻塞,直到第一个线程执行结束,退出临界区,并释放锁。此时,其他等待的线程可以获得锁并进入临界区。不过请记住,那些被阻塞的线程是没有顺序的(即不是先到先执行),胜出线程的选择是不确定的,而且还会根据Python实现的不同而有所区别。
#演示了锁和一些其他线程工具的使用
from atexit import register
from random import randrange
from threading import Thread,Lock,currentThread
from time import sleep,ctime
from __future__ import with_statement
class CleanOutputSet(set):
def __str__(self):
return ','.join(x for x in self)
lock = Lock()
#随机数量的线程(3~6个线程),每个线程暂停或睡眠2~4秒
loops = (randrange(2,5) for x in range(randrange(3,7)))
remaining = CleanOutputSet()
def loop(nsec):
myname = currentThread().name
with lock:
remaining.add(myname)
print('[%s] Started %s' % (ctime(),myname))
sleep(nsec)
with lock:
remaining.remove(myname)
print('[%s] Completed %s (%d secs)' % (ctime(),myname,nsec))
print('(remaining:%s) '% (remaining or 'None'))
def _main():
for pause in loops:
print('****',pause,'-',loops,'-')
Thread(target=loop,args=(pause,)).start()
#使用atexit.register()来注册_atexit()函数,以便让解释器在脚本退出前执行该函数
@register
def _atexit():
print('all DONE at:',ctime())
if __name__ == '__main__':
_main()
可是出现混乱的情况
I/O和访问相同的数据结构都属于临界区,因此需要用锁来防止多个线程同时进入临界区
#演示了锁和一些其他线程工具的使用
from atexit import register
from random import randrange
from threading import Thread,Lock,currentThread
from time import sleep,ctime
lock = Lock()
class CleanOutputSet(set):
def __str__(self):
return ','.join(x for x in self)
#随机数量的线程(3~6个线程),每个线程暂停或睡眠2~4秒
loops = (randrange(2,5) for x in range(randrange(3,7)))
remaining = CleanOutputSet()
def loop(nsec):
myname = currentThread().name
lock.acquire()
remaining.add(myname)
print('[%s] Started %s' % (ctime(),myname))
#3.x print('[{0}] Started {1}'.format(ctime(),myname))
lock.release()
sleep(nsec)
lock.acquire()
remaining.remove(myname)
print('[%s] Completed %s (%d secs)' % (ctime(),myname,nsec))
print('(remaining:%s) '% (remaining or 'None'))
lock.release()
def _main():
for pause in loops:
print('****',pause,'-',loops,'-')
Thread(target=loop,args=(pause,)).start()
#使用atexit.register()来注册_atexit()函数,以便让解释器在脚本退出前执行该函数
@register
def _atexit():
print('all DONE at:',ctime())
if __name__ == '__main__':
_main()
信号量示例
信号量是最古老的同步原语之一。它是一个计数器,当资源消耗时递减,当资源释放时递增。你可以认为信号量代表它们的资源可用或不可用。消耗资源使计数器递减的操作习惯上称为P(),也称为wait,try,acquire,pend或procure。相对地,当一个线程对一个资源完成操作时,该资源需要返回资源池中。这个操作一般称为V(),也称为signal,increment,release,post,vacate。Python简化了所有的命名,使用和锁的函数/方法一样的名字:acquire和release。信号量比锁更加灵活,因为可以有多个线程,每个线程拥有有限资源的一个实例。
在下面的例子中,我们将模拟一个简化的糖果机。这个特制的机器只有5个可用的槽来保持库存(糖果)。如果所有的槽都满了,糖果就不能再加到这个机器中了;相似地,如果每个槽都空了,想要购买的消费者就无法买到糖果了。我们可以使用信号量来跟踪这些有限的资源(糖果槽)。
#该脚本使用了锁和信号量来模拟一个糖果机
from atexit import register
from random import randrange
from threading import BoundedSemaphore,Lock,Thread
from time import sleep,ctime
lock = Lock()
MAX = 5
candytray = BoundedSemaphore(MAX)
#添加糖果
def refill():
lock.acquire()
print('Refilling candy...')
try:
candytray.release()
except ValueError:
print('full,skipping')
else:
print('OK')
lock.release()
#允许消费者获取一个单位的库存
def buy():
lock.acquire()
print('Buying candy...')
if candytray.acquire(False): #检测是否所有资源都已经消费完
print('OK')
else:
print('empty,skipping')
lock.release()
def producer(loops):
for i in range(loops):
refill()
sleep(randrange(3))
def consumer(loops):
for i in range(loops):
buy()
sleep(randrange(3))
def _main():
print('starting at:',ctime())
nloops = randrange(2,6)
print('THE CANDY MACHINE (full with %d bars)!' % MAX)
Thread(target=consumer,args=(randrange(nloops,nloops+MAX+2),)).start() #buyer
Thread(target=producer,args=(nloops,)).start()
@register
def _atexit():
print('all DONE at:',ctime())
if __name__ == '__main__':
_main()
threading模块包括两种信号量类:Semaphore和BoundedSemaphore。如你所知,信号量实际上就是计数器,它们从固定数量的有限资源起始
当分配一个单位的资源时,计数器值减1,而当一个单位的资源返回资源池时,计数器值加1。BoundedSemaphore的一个额外功能是这个计数器的值永远不会超过它的初始值,换句话说,它可以防范其中信号量释放次数多于获得次数的异常用例。
生产者-消费者问题和Queue/queue模块
在这个场景下,商品或服务的生产者生产商品,然后将其放到类似队列的数据结构中。生产商品的时间是不确定的,同样消费者消费生产者生产的商品的时间也是不确定的。
#该生产者-消费者问题的实现使用了Queue对象,以及随机生产(消费)的商品的数量。生产者和消费者独立且并发地执行线程
from random import randint #使生产和消费地数量有所不同
from time import sleep
import queue
from myThread import MyThread
def writeQ(queue):
print('producing object for Q...')
queue.put('xxx',1)
print('size now',queue.qsize())
def readQ(queue):
val = queue.get(1)
print('consumed 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(2,5))
funcs = [writer,reader]
nfuncs = range(len(funcs))
def main():
nloops = randint(2,5)
q = queue.Queue(32)
threads = []
for i in nfuncs:
t = MyThread(funcs[i],(q,nloops),funcs[i].__name__)
threads.append(t)
for i in nfuncs:
threads[i].start()
for i in nfuncs:
threads[i].join()
print('all done')
if __name__ == '__main__':
main()