多线程编程
1、Python 的多线程支持模块简介
Python 提供了多个模块来支持多线程编程,包括 thread、threading 和 Queue 模块等。程序是可以使用 thread 和 threading 模块来创建与管理线程。thread 模块提供了基本的线程和锁定支持;而 threading 模块提供了更高级别、功能更全面的线程管理。使用 Queue 模块,用户可以创建一支队列数据结构,用于在多线程之间进行共享。
-
核心提示:避免使用 thread 模块
推荐使用更高级别的 threading 模块,而不使用 thread 模块有很多原因:
- threading 模块更加先进,有更好的线程支持,并且 thread 模块中的一些属性会和 threading 模块有冲突。
- 另一个原因是低级别的 thread 模块拥有的同步原语很少(实际只有一个),而 threading 模块则很多。
- 避免使用 thread 模块的另一个原因是它对于进程如何退出没有控制。当主线程结束时,所有其他线程也都强制结束,不会发出警告或者进行适当的清理。但至少 threading 模块能确保重要的子线程在进程退出前结束。
2、thread 模块
除了派生线程外,thread 模块还提供了基本的同步数据结构,称为锁对象(也称为原语锁、简单锁、互斥锁、互斥和二进制信号量)。下表展示了一些常用的线程函数以及 LockType 锁对象的方法:
函数/方法 | 说明 |
---|---|
thread 模块的函数 | |
start_new_thread(function,args,kwargs=None) | 派生一个新的线程,使用给定的 args 和可选的 kwargs 来执行 function |
allocate_lock() | 分配 LockType 锁对象 |
exit() | 给线程退出指令 |
LockType 锁对象的方法 | |
acquire(wait=None) | 尝试获取锁对象 |
locked() | 如果获取了锁对象则返回 True,否则,返回 False |
release() | 释放锁 |
3、threading 模块
除了 Thread 类之外,该模块还包括许多非常好用的同步机制。下表给出了 threading 模块中所有可用对象的列表。
对象 | 说明 |
---|---|
Thread | 表示一个执行线程的对象 |
Lock | 锁原语对象 |
RLock | 可重入锁对象,使单一线程可以(再次)获得已持有的锁(递归锁) |
Condition | 条件变量对象,使得一个线程等待另一个线程满足特定的 “条件”,比如改变状态或某个数据值 |
Event | 条件变量的通用版本,任意数量的线程等待某个事件的发生,在该事件发生后所有线程将被激活 |
Semaphore | 为线程间共享的有限资源提供一个 “计数器”,如果没有可用资源时会阻塞 |
BoundedSemaphore | 与 Semaphore 相似,不过它不允许超过初始值 |
Timer | 与 Thread 相似,不过它要在运行前等待一段时间 |
Barrier | 创建一个 “障碍”,必须达到指定数量的线程后才可以继续 |
-
核心提示:守护线程
避免使用 thread 的另一个原因是该模块不支持守护线程的概念。**当主线程退出时,所有子线程都将终止,不管他们是否正在工作。**如果你不想发生这种行为,就要引入守护线程了。
threading 模块支持守护线程,其工作方式是:守护线程一般是一个等待客户端请求的服务器。如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置成守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。
要将一个线程设置成守护线程,需要在启动线程之前执行如下语句:thread.daemon = True。同样,要检查线程的守护状态,也只需要检查这个值即可。一个新的线程会继承父线程的守护标记。整个 Python 程序将在所有非守护线程退出后才退出。
3.1、Thread 类
threading 模块的 Thread 类是主要的执行对象。
属性 | 说明 |
---|---|
Thread 对象数据属性 | |
name | 线程名 |
ident | 线程的标识符 |
daemon | 布尔标识,标识这个线程是否是守护线程 |
Thread 对象方法 | |
_init_(group=None,target(func)=None,name=None,args=(),kwargs={},verbose=None,daemon=None) | 实例化一个线程对象,需要有一个可调用的 target,以及其参数 args 或 kwargs。还可以传递 name 或 group 参数,不过后者还未实现。此外,verbose 标志也是可以接受的。而 daemon 的值也将会设定 thread.daemon 属性/标志 |
start() | 开始执行该进程 |
run() | 定义线程功能的方法(通常在子类中被应用开发者重写) |
join(timeout=None) | 直至启动的线程终止之前一直挂起;除非给出了 timeout,否则会一直阻塞 |
getName() | 返回线程名(更好的方法是设置(或获取)thread.name 属性,或者在实例化过程中传递该属性) |
setName() | 设定线程名(同上) |
isAlivel/is_alive() | 布尔标识,表示这个线程是否还存活 |
isDaemon() | 如果是守护线程,则返回 True;否则,返回 False(更好的方法是设置(或获取)thread.daemon 属性,或者在实例化过程中传递该属性) |
setDaemon(daemon) | 把线程的守护标识设定为布尔值 daemonic(必须在线程 start() 之前调用)(同上) |
使用 Thread 类,可以有很多种方法创建线程:
- 创建 Thread 的实例,传给它一个函数;
- 创建 Thread 的实际,传给它一个可调用的类实例;
- 派生 Thread 的子类,并创建子类的实例。
3.1.1、派生 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())
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())
starting at: Thu Aug 20 16:09:44 2020
start loop 0 at: Thu Aug 20 16:09:44 2020
start loop 1 at: Thu Aug 20 16:09:44 2020
loop 1 done at: Thu Aug 20 16:09:46 2020
loop 0 done at: Thu Aug 20 16:09:48 2020
all DONE at: Thu Aug 20 16:09:48 2020
3.2、threading 模块的其他函数
函数 | 说明 |
---|---|
active_count() | 当前活动的 Thread 对象个数 |
current_thread | 返回当前的 Thread 对象 |
enumerate() | 返回当前活动的 Thread 对象列表 |
settrace(func) | 为所有线程设置一个 trace 函数 |
setprofile(func) | 为所有线程设置一个 profile 函数 |
stack_size(size=0) | 返回新建线程的栈大小;或为后续创建的线程设定栈的大小为 size |
4、单线程与多线程的对比
下面的脚本比较了递归求斐波那契、阶乘与累加函数的执行。该脚本按照单线程的方式运行这三个函数。之后使用的多线程的方式执行同样的任务,用来说明多线程的优势。
import threading
from time import ctime,sleep
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):
print('starting', self.name, 'at:', ctime())
self.res = self.f unc(*self.args)
print(self.name, 'finished at:', ctime())
def getResult(self):
return self.res
def fib(x):
sleep(0.005)
if x < 2:
return 1
return (fib(x-2) + fib(x-1))
def fac(x):
sleep(0.1)
if x < 2:
return 1
return (x * fac(x-1))
def sum(x):
sleep(0.1)
if x < 2:
return 1
return (x + sum(x-1))
funcs = [fib,fac,sum]
n = 12
nfuncs = range(len(funcs))
print('*** SINGLE THREAD')
for i in nfuncs:
print('starting',funcs[i].__name__,'at:',ctime())
print(funcs[i](n))
print(funcs[i].__name__,'finished at:',ctime())
print('\n*** NULTIPLE THREADS')
threads = []
for i in nfuncs:
t = MyThread(funcs[i],(n,),funcs[i].__name__)
threads.append(t)
for i in nfuncs:
threads[i].start()
for i in nfuncs:
threads[i].join()
print(threads[i].getResult())
print('all Done')
*** SINGLE THREAD
starting fib at: Fri Aug 21 10:17:44 2020
233
fib finished at: Fri Aug 21 10:17:53 2020
starting fac at: Fri Aug 21 10:17:53 2020
479001600
fac finished at: Fri Aug 21 10:17:55 2020
starting sum at: Fri Aug 21 10:17:55 2020
78
sum finished at: Fri Aug 21 10:17:56 2020
*** NULTIPLE THREADS
starting fib at: Fri Aug 21 10:17:56 2020
starting fac at: Fri Aug 21 10:17:56 2020
starting sum at: Fri Aug 21 10:17:56 2020
fac finished at: Fri Aug 21 10:17:57 2020
sum finished at: Fri Aug 21 10:17:57 2020
fib finished at: Fri Aug 21 10:18:05 2020
all Done
以单线程模式运行时,只是简单地依次调用每个函数,并在函数执行结束后立刻显示结果。而以多线程模式运行时,并不会立刻显示结果。我们一直等待所有线程都执行结束,然后调用 getResult() 方法来显示每个函数的结果。
5、同步原语
5.1、信号量机制
原理见《操作系统学习笔记之——进程管理》。
我们将模拟一个简化的糖果机。这个特制的机器只有 5 个可用的槽来保存库存(糖果)。如果所有的槽都满了,糖果就不能再加到这个机器中了;相似的,如果所有的槽都空了,那么买糖果的消费者就无法买到糖果了。
from atexit import register
from random import randrange
from threading import Thread,Lock,BoundedSemaphore
from time import ctime,sleep
lock = Lock()
MAX = 5
candytray = BoundedSemaphore(MAX)
def refill():
lock.acquire()
print('Refill candy...',end='')
try:
candytray.release()
except ValueError:
print('full,skipping')
else:
print('OK')
lock.release()
def buy():
lock.acquire()
print('Buying candy...',end='')
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))
print('starting ay:',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() #vndr
@register
def _atexit():
print('all Done at:',ctime())
-
第 1~4 行
启动行和导入模块的行与之前的例子非常相似。唯一不同的是信号量。threading 模块包括两种信号量类:Semaphore 和 BoundedSemaphore。信号量实际上就是一个计数器,它们从固定数量的有限资源起始。
当分配一个单位的资源时,计数器就减 1,而当一个单位的资源返回资源池时,计数器就加 1。BoundedSemaphore 的一个额外的功能是这个计数器的值永远不会超过它的初始值,换句话说,它可以防止其中信号量释放次数多于获得次数的异常用例。
-
第 6~8 行
这个脚本的全局变量包括:一个锁,一个表示库存商品的最大值的常量,一个糖果托盘信号量。
-
第 10~19 行
当虚构的糖果所有者向库存中添加糖果时,会执行 refill() 函数。这段代码是一个临界区,这就是为什么获取锁是执行所有行的仅有办法。代码会输出用户的行动,并在某人添加的糖果超过最大库存时给予警告。
-
第 21~28 行
buy() 函数它允许消费者获取一个单位的库存。条件语句检测是否所有的资源都已经消费完。计数器的值不能小于 0,因此这个调用一般会在计数器在此增加之前被阻塞。通过传入非阻塞的标志 False,让调用不再阻塞,而在应当阻塞的时候返回一个 False,指明没有更多的资源了。
-
第 30~38 行
productor() 和 consumer() 函数只是一个循环,进行对应的 refill() 和 buy() 调用,并在期间暂停。
-
第 40~48 行
创建了生产者和消费者的线程对。同时还进行了额外的数学操作,用于随机给出正偏差,使得消费者真正消费的糖果数可能会比生产者放入机器的更多,否则,代码将永远不会进行消费者尝试从空机器购买糖果的情况。
starting ay: Fri Aug 21 15:26:05 2020
THE CANDY MACHINE (FULL WITH 5 BARS!)
Buying candy...OK
Refill candy...OK
Refill candy...full,skipping
Buying candy...OK
Buying candy...OK
Buying candy...OK
Buying candy...OK
Buying candy...OK
all Done at: Fri Aug 21 15:26:11 2020
6、生产者–消费者问题和 queue 模块
在这个场景下,商品或服务的生产者生产商品,然后将其放到类似队列的数据结构中。生产商品的时间是不确定的,同样消费者消费生产者生产的商品的时间也是不确定的。我们使用 queue 模块来提供线程间的通信机制,从而让线程之间可以互相分享数据。具体而言,就是创建一个队列,让生产者(线程)在其中放入新的商品,而消费者(线程)消费商品。
属性 | 说明 |
---|---|
queue 模块 | |
Queue(maxsize=0) | 创建一个先入先出的队列。如果给定最大值,则在队列没有空间时阻塞;否则为无限队列 |
LifoQueue(maxsize=0) | 创建一个后入先出的队列。最大值同上 |
PriorityQueue(maxsize=0) | 创建一个优先级队列。最大值同上 |
queue 异常 | |
Empty | 当对空队列调用 get*() 时抛出异常 |
Full | 当对已满队列调用 put*() 时抛出异常 |
queue 对象方法 | |
qsize() | 返回队列的大小 |
empty() | 如果队列为空,则返回 True;否则,返回 False |
full() | 如果队列为满,则返回 True;否则,返回 False |
put(item,block=True,timeout=None) | 将 item 放入队列。如果 block 为 True 且 timeout 为 None,则在有可用空间之前阻塞;如果 timeout 为正值,则最多阻塞 timeout 秒;如果 block 为 False,则抛出 Empty 异常 |
put_nowait(item) | 和 put(item,False) 相同 |
get(block=True,timeout=None) | 从队列中取得元素。如果给定了 block(非0),则一直阻塞到有可用的元素为止 |
get_nowait() | 和 get(False) 相同 |
task_done() | 用于表示队列中的某元素已执行完成,该方法会被下面的 join() 使用 |
join() | 在队列中所有的元素执行完成并调用上面的 task_done() 信号之前,保持阻塞 |
from random import randint
from time import sleep,ctime
from queue import Queue
import threading
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):
print('starting', self.name, 'at:', ctime())
self.res = self.func(*self.args)
print(self.name, 'finished at:', ctime())
def getResult(self):
return self.res
def writeQ(queue):
print('producing object for Q...',end='')
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))
nloops = randint(2,5)
q =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')
-
第 1~4 行
使用了 queue.Queue 对象,另外使用 random.randint() 以使生产和消费的数量有所不同。
-
第 21~28 行
writeQ() 和 readQ() 函数分别用于将一个对象(我们这里使用字符串 “xxx”)放入队列中和消费队列中的一个对象。注意,我们这里每次只生产或读取一个对象。
-
第 30~38 行
writer() 将作为单个线程运行,其目的只有一个:向对列中放入一个对象,等待片刻,然后重复上述步骤,直至达到每次脚本执行时随机产生的次数为止。reader() 与之类似。
你会注意到,writer 睡眠的时间通常会比 reader 的时间短。这是为了阻碍 reader 从空队列中获取对象。
starting writer at: Fri Aug 21 16:00:03 2020
producing object for Q...size now: 1
starting reader at: Fri Aug 21 16:00:03 2020
consumed object from Q... size now: 0
producing object for Q...size now: 1
consumed object from Q... size now: 0
producing object for Q...size now: 1
producing object for Q...size now: 2
producing object for Q...size now: 3
consumed object from Q... size now: 2
writer finished at: Fri Aug 21 16:00:12 2020
consumed object from Q... size now: 1
consumed object from Q... size now: 0
reader finished at: Fri Aug 21 16:00:21 2020
All Done