- 背景/简介
- 线程和进程
- 线程和Python
- thread 模块
- threading模块
- 单线程和多线程对比
- 多线程实践
- 生产者-消费者问题和Queue/queue 模块
- 线程的替代方案
现在介绍更高级别的threading
模块。除了Thread
类之外,该模块还包括许多非常好用的同步机制。
对象 | 描述 |
---|---|
Thread | 表示一个执行线程的对象 |
Lock | 锁原语对象(和 thread 模块中的锁一样) |
Codition | 条件变量对象,使得一个线程等待另一个线程满足特定的 “条件” ,比如改变状态或某个数据值 |
Event | 条件变量的通用版本,任意数量的线程等待某个事件的发生,在该事件发生后所有的线程将被激活 |
Semapore | 为线程间共享的有限资源提供一个“计数器”,如果没有可用的资源时会被阻塞 |
BoundSemapore | 与Semapore 相似,不过它不允许超过初始值 |
Timer | 与Thread相似,不过它要在运行前等待一段时间 |
Barrier | 创造一个“障碍” , 必须到达指定数量的线程后才可以继续 |
本节将研究如何使用Thread
类来实现多线程。由于之前已经介绍过锁的基本概念,因此这里不会再对锁原语进行介绍。因为Thread()
类同样包含某种同步机制,所以锁原语的显示使用不再是必需的了
核心提示:
避免使用
thread
模块的另一个原因是该模块不支持守护线程的概念。当主线程退出时,所有子线程都将终止,不管它们是否仍在工作。如果你不希望发生这种行为,就要引入守护线程的概念了。
threading
模块支持守护线程,其工作方式是:守护线程一般是等待客户端请求服务的服务器。如果没有客户端请求,守护线程就是清闲的。如果把一个线程设置为守护线程,就表示这个线程不是重要的,进程退出是不需要等待这个线程执行完成。如同服务器线程运行在一个无限循环里,并在在正常情况下不会退出。
如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置守护线程标记。当该标记为真时,表示该线程是不重要的,或者说这些线程只是用来等待客户端的请求而不做任何其他事情。
要将一个线程设置为守护线程,需要在启动线程之前执行如下的赋值语句:thread.daemon = True
。同样,要检查线程的守护状态,也只需要检查这个值即可。一个新的子线程会继承父线程的守护标记。整个Python程序(可以解读为:主线程)将在所有非守护线程退出之后才退出。换句话说,就是没有剩下存活的非守护线程时。
Thread类
属性 | 描述 |
---|---|
name | 线程名 |
ident | 线程的标识符 |
daemon | 布尔标志,表示这个线程是否是守护线程 |
创建 Thread 的实例,传递给它一个参数
在第一个例子中,我们只是把 Thread
类实例化,然后将函数(以及参数)传递进去,和之前的例子中采用的参数一样。当线程开始执行时,这个函数也将执行。我们把mtsleepB.py脚本进行修改,添加使用 Thread类,得到示例中的mtsleepC.py文件
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()
运行脚本时,可以得到和之前相似的输出。
starting at: Wed Jan 24 01:23:05 2018
start loop 0 at:Wed Jan 24 01:23:05 2018
start loop 1 at:Wed Jan 24 01:23:05 2018
loop 1 done at:Wed Jan 24 01:23:07 2018
loop 0 done at:Wed Jan 24 01:23:09 2018
all DONE at: Wed Jan 24 01:23:09 2018
那么这里到底做了那些修改呢?使用 Thread
模块时实现的锁没有了,取而代之的是一组Thread
对象。当实例化Thread
对象时,把函数 ( target ) 和参数(args)传递进去,然后得到返回的Thread实例,实例化Thread
和调用thread.start_new_thread()
的最大区别是新线程不会立即执行。这是一个非常有用的同步功能,尤其是你不希望新线程立即执行时。
当所有的线程都分配完成之后,通过调用每个线程的 start()
方法让它们开始执行,而不是在这之前就会执行。相比于管理一组锁(分配、获取、释放、检查锁状态等)而言,这里只需要为每个线程调用join()
方法即可。join()
方法将等待线程结束,或者在提供了超时时间的情况下,大到超时时间。join()
方法要比等待锁释放的无限循环更加清晰(这也是这种锁又称为自旋锁的原因)。
对于join()
方法而言,其另外一个重要的方面是其实它不需要调用。一旦线程启动,它将一直执行,知道给定的函数完成后退出。如果主线程还有其他事情要做,而不是等待这些线程完成(例如其他处理或者等待新用户的客户端请求),就可以不调用join()
。join()
方法只有在你需要等待线层完成的时候才是有用的。
创建 Thread 的实例,传给他一个可调用的类实例
在创建线程时,与函数相似的一个方式是传入一个可调用的类的实例,用于线程执行----这种方法更加接近面线对象的多线程编程。这种可调用的类包含一个执行环境,比起一个函数或者从一组函数中选择而言,有更好的灵活性。现在你有了一个类对象,而不仅仅是单个函数或者一个函数列表/元组。
在mtsleepC.py的代码中添加一个新类 ThreadFunction,并进行一些其他的轻微改动,得到mtsleepD.py
import threading
from time import sleep, ctime
loops = [4, 2]
class ThreadFunc(object):
def __init__(self, func, args, name=''):
self.name =name
self.func = func
self.args = args
def __call__(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 = threading.Thread(target=ThreadFunc(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()
运行mtsleepD.py得到下面的输出。
starting at: Wed Jan 24 10:36:42 2018
start loop 0 at:Wed Jan 24 10:36:42 2018
start loop 1 at:Wed Jan 24 10:36:42 2018
loop 1 done at:Wed Jan 24 10:36:44 2018
loop 0 done at:Wed Jan 24 10:36:46 2018
all DONE at: Wed Jan 24 10:36:46 2018
那么,这次又修改了什么?主要是添加了ThreadFunc
类,并在实例化Thread
对象时,做了一点小改动,同时实例化了可调用类ThreadFunc
。实际上,这里完成了两个实例化。让我们先仔细看看ThreadFunc
类吧。
我们希望这个类更加通用,而不是局限于 loop()
函数,因此添加了一些新的东西,比如让这个类保存了函数的参数、函数自身、以及函数名的字符串。而构造函数__inin__()
用于设定上述值。
当创建新线程时,Thread 类的代码将要调用 ThreadFunc
对象,此时会调用 __call__()
这个特殊的方法。由于我们已经有了要用到的参数,这里就不需要再将其传递给Thread()
的构造函数了,直接调用即可。