多线程编程(五) threading模块


  • 背景/简介
  • 线程和进程
  • 线程和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()的构造函数了,直接调用即可。

你可能感兴趣的:(多线程编程(五) threading模块)