python进程、线程和协程01--线程

参考文档:https://docs.python.org/zh-cn/3.8/library/threading.html

参考文档:《Python核心编程(第3版 2016)》

1、多任务

  • 多任务就是同一时间内运行多个程序。
  • 单核CPU实现多任务的原理:操作系统轮流让各个任务在CPU上执行。
  • 多核CPU实现多任务的原理:真正的并行执行多任务只能在多核CPU上实现。但是由于任务数量可能多于CPU的核心数量,所以操作系统也会把任务轮流调度到每个CPU核心上执行。

2、并发和并行

  • 并发:如果只有一个CPU,同一时刻只能有一个线程在运行。把CPU运行时间分成若干个时间段,再将各个时间段分配给各个线程,在一个线程在运行时,其它线程处于挂起状态,这种方式称为并发(Concurrent).
  • 并行:如果有多个CPU,每个CPU运行一个线程,每个线程互不抢占CPU资源,可以同时进行,这种方式称为并行(Parallel)

1、多线程编程

  • Python提供了多个模块来支持多线程编程,包括thread、threading和queue模块等。
    • 可以使用thread和threading模块来创建与管理线程。
      • thread模块提供了基本的线程和锁定支持。
      • threading模块提供了更高级别、功能更全面的线程管理。
    • 使用queue模块,用户可以创建一个队列数据结构,用于在多线程之间进行数据交换。
  • 当主线程退出时,所有子线程都将终止,不管它们是否仍在工作。但整个Python程序(可以解读为:主线程)将在所有非守护线程退出之后才退出。(无论如何主线程都不会在非守护线程完成之前退出
  • 多线程编程
    • 本质上是异步的;需要多个并发活动;每个活动的处理顺序可能是不确定的,或者说是随机的、不可预测的。
      • 程序可以被组织或划分成多个执行流,其中每个执行流都有一个指定要完成的任务。
      • 根据应用的不同,这些子任务可能需要计算出中间结果,然后合并为最终的输出结果。
  • 多线程编程可以规划成几个执行特定函数的线程
    • UserRequestThread:负责读取客户端输入,该输入可能来自I/O通道。程序将创建多个线程,每个客户端一个,客户端的请求将会被放入队列中。
    • RequestProcessor:该线程负责从队列中获取请求并进行处理,为第3个线程提供输出。
    • ReplyThread:负责向用户输出,将结果传回给用户(如果是网络应用),或者把数据写到本地文件系统或数据库中。
  • 多线程编程可以降低程序的复杂性,使其实现更加清晰、高效、简洁:
    • 每个线程中的逻辑都不复杂,因为它只有一个要完成的特定作业。
    • 每个线程都有其明确的作业,你只需要设计每类线程去做一件事,并把这件事情做好就可以了。
  • 线程的运行
    • 线程(有时候称为轻量级进程)与进程类似,不过它们是在同一个进程下执行的,并共享相同的上下文。可以将它们认为是在一个主进程或“主线程”中并行运行的一些“迷你进程”。
    • 线程包括开始、执行顺序和结束三部分。它有一个指令指针,用于记录当前运行的上下文。当其他线程运行时,它可以被抢占(中断)和临时挂起(也称为睡眠)——这种做法叫做让步(yielding)。
    • 一个进程中的各个线程与主线程共享同一片数据空间
    • 共享是有风险的。如果两个或多个线程访问同一片数据,由于数据访问顺序不同,可能导致结果不一致。这种情况通常称为竞态条件(racecondition)。幸运的是,大多数线程库都有一些同步原语,以允许线程管理器控制执行和访问。
    • 执行线程时无法给予公平的执行时间。如果没有专门为多线程情况进行修改,会导致CPU的时间分配会向阻塞状态的线程倾斜
    • 单核CPU系统中,因为真正的并行是不可能的,因此线程的执行实际上是这样规划的:每个线程运行一小会儿,然后让步给其他线程(再次排队等待更多的CPU时间)。在整个进程的执行过程中,每个线程执行它自己特定的任务,在必要时和其他线程进行结果通信。
  • 退出线程
    • 当一个线程完成函数的执行时,它就会退出。
    • 还可以通过调用诸如thread.exit()之类的退出函数,或者sys.exit()之类的退出Python进程的标准方法,亦或者抛出SystemExit异常,来使线程退出。
    • 线程不能被“终止”。即不能被手动“终止”。
  • 主线程(主进程)应该做一个好的管理者。
    • 负责了解每个单独的线程需要执行什么,每个派生的线程需要哪些数据或参数,这些线程执行完成后会提供什么结果。
    • 主线程收集每个线程的结果,然后汇总成一个有意义的最终结果。

2、线程的五个状态

  • 当程序中包含多个线程时,CPU不是一直执行一个线程,而是轮流执行各个线程。
  • CPU在轮流执行线程时,线程从创建到消亡可能会历经5种状态,分别是新建、就绪、运行、阻塞和死亡。

1、线程的新建状态

  • 无论是通过threading类直接实例化对象创建线程,还是通过继承自threading类的子类实例化创建线程,新创建的线程在调用start()方法之前,不会得到执行,此阶段的线程就处于新建状态。

2、线程的就绪状态

  • 当线程调用start()方法后,该线程就转换到就绪状态。
  • 就绪,就是告诉操作系统,该线程已经可以执行了,但是具体什么时候执行,取决于操作系统什么时候调度它。
  • 换句话说,如果一个线程处于就绪状态,只能说明此线程已经做好了准备,并随时等待操作系统调度执行,并不是说执行了start()方法就会立即被执行。

3、线程的运行状态

  • 当就绪状态的线程得到了CPU,并开始执行target参数(目标函数)或者run()方法,就表明当前线程处于运行状态。
  • 如果有多个线程处于就绪状态(等待CPU调度)时,运行状态的线程将无法一直使用CPU资源,为了使其它线程也有执行的机会,操作系统会在一定时间强制当前运行的线程让出CPU资源,以供其他线程使用。

4、线程的阻塞状态

  • 每一个线程只能获取一段固定的CPU时间来处理任务,当该CPU时间用完后,操作系统就会阻止这个线程继续使用CPU,即这个线程进入阻塞状态,从而让其他线程获得执行的机会。进入阻塞状态的线程,如果等待的条件已成立,将转入就绪状态,重复争取CPU资源,直到其执行结束。
  • 对于选择哪个线程继续使用CPU,不同的平台采用不同的算法,比如先进先出算法(FIFO)、时间片轮转算法、优先级算法等,每种算法各有优缺点,适用于不同的场景。目前几乎所有的操作系统,都采用的是抢占式优先级调度策略。
  • 以下几种情况,也会将线程转到阻塞状态:
    • 线程调用了sleep()方法;
    • 线程等待接收用户输入的数据;
    • 线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程进入阻塞状态;
    • 线程调用wait()方法,等待特定条件的满足。

5、线程的死亡状态

  • 执行结束的线程即处于死亡状态。
  • 除了线程的正常执行结束外,如果程序执行过程发生异常(Exception)或者错误(Error),线程也会进入死亡状态。

3、全局解释器锁GIL

  • Python代码的执行是由Python虚拟机(又名解释器主循环)进行控制的。
  • 对CPython虚拟机的访问是由全局解释器锁(GIL)控制的。这个锁就是为了确保一个进程在同一时刻只有一个线程在运行
  • 全局解释器锁GIL只存在于通过C语言实现的Python解释器上即CPython
    • 为了绕过GIL的问题有人利用Java开发了解释器Jpython或使用Python自己开发了自己的解释器PyPy,这些python解释器上都不存在GIL全局解释器锁的问题 。
    • 但CPython才是当前最多人使用的主流Python解释器 。
  • 全局解释器锁GIL只存在于线程级别
  • 绕过GIL的限制,一种简单的方法就是使用多进程,多个进程之间并不会出现GIL限制,不同的进程会运行在CPU不同的核上,实现真正的并行。
  • CPython解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行
  • 在多线程环境中,CPython虚拟机将按照下面所述的方式执行。

1.设置GIL。
2.切换进一个线程去运行。
3.执行下面操作之一。
      a.指定数量的字节码指令。
      b.线程主动让出控制权(可以调用time.sleep(0)来完成)。
4.把线程设置回睡眠状态(切换出线程)。
5.解锁GIL。
6.重复上述步骤。

  • Python中的线程工作一段时间后,会主动释放GIL,这是为了让其他线程都有机会执行 ,而释放的时机就涉及到了「检查间隔」(check interval)机制 ,在早期版本的Python中,检查机制是100ticks,而Python3后,每15毫米使用一次检查间隔,然后就会释放GIL锁
  • 当调用外部代码(即,任意C/C++扩展的内置函数)时,GIL会保持锁定,直至函数执行结束(因为在这期间没有Python字节码计数)。编写扩展函数的程序员有能力解锁GIL。
  • I/O密集型的Python程序要比计算密集型的代码能够更好地利用多线程环境。

4、thread模块

  • 不建议使用thread模块
    • threading模块在主线程退出之后,所有其他线程都会在没有清理的情况下直接退出。threading模块会确保在所有“重要的”子线程退出前,保持整个进程的存活。
    • thread模块拥有的同步原语很少(实际上只有一个),而threading模块则有很多。
    • thread模块不支持守护线程,threading模块支持守护线程
  • 为了强调不建议使用thread模块,在Python3中该模块被重命名为_thread

1、thread模块和锁对象

  • thread 模块的核心函数是start_new_thread()。它的参数包括函数(对象)、函数的参数以及可选的关键字参数。将专门派生新的线程来调用这个函数。

2、示例

###Python2.7
# -*- coding: utf-8 -*-
import thread
from time import sleep, ctime
loops = [4, 2]
def loop(nloop, nsec, lock):
    print 'loop', nloop, '开始:', ctime()
    sleep(nsec)
    print 'loop', nloop, '结束:', ctime()
    lock.release()
def main():
    print 'main 开  始:', ctime()
    locks = []
    nloops = range(len(loops))
    for i  in nloops:
        lock = thread.allocate_lock()
        lock.acquire()
        locks.append(lock)
    print locks
    for i  in nloops:
        print loop, (i, loops[i], locks[i])
        thread.start_new_thread(loop, (i, loops[i], locks[i]))
        sleep(0.01)
    for i  in nloops:
        while locks[i].locked(): pass
    print 'main 结  束:', ctime()
if __name__ == '__main__':
    main()

<<<
main 开  始: Tue Mar 23 16:40:17 2021
[<thread.lock object at 0x00000000032EBBD0>, <thread.lock object at 0x00000000032EBBB0>]
<function loop at 0x00000000032BCB38> (0, 4, <thread.lock object at 0x00000000032EBBD0>)
loop 0 开始: Tue Mar 23 16:40:17 2021
<function loop at 0x00000000032BCB38> (1, 2, <thread.lock object at 0x00000000032EBBB0>)
loop 1 开始: Tue Mar 23 16:40:17 2021
loop 1 结束: Tue Mar 23 16:40:19 2021
loop 0 结束: Tue Mar 23 16:40:21 2021
main 结  束: Tue Mar 23 16:40:21 2021

5、threading模块简介

1、threading模块的对象

2、threading模块的函数

6、threading模块--Thread类

1、Thread类

  • Thread类表示在单独的控制线程中运行的活动(代码)。
  • 有两种方法可以指定活动:
    • 通过向构造函数传递一个可调用对象
    • 通过覆盖子类中的run()方法。子类中不应该重写任何其他方法(构造函数除外)。换句话说,只重写该类的__init__()和run()方法。
  • 当线程对象一但被创建,其活动一定会因调用线程的start()方法开始。这会在独立的控制线程调用run()方法。
  • 一旦线程活动开始,该线程会被认为是'存活的'。当它的run()方法终结了(不管是正常的还是抛出未被处理的异常),就不是'存活的'。is_alive()方法用于检查线程是否存活。
  • 其他线程可以调用一个线程的join()方法。这会阻塞调用该方法的线程,直到被调用join()方法的线程终结。
  • 线程有名字。名字可以传递给构造函数,也可以通过name属性读取或者修改。

1、Thread类的构造函数

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

  • 调用这个构造函数时,必需带有关键字参数
  • group应该为None;为了日后扩展ThreadGroup类实现而保留。
  • target是用于run()方法调用的可调用对象。默认是None,表示不需要调用任何方法。
  • name是线程名称。默认情况下,由"Thread-N"格式构成一个唯一的名称,其中N是小的十进制数。
  • args是用于调用目标函数的参数元组。默认是()。
  • kwargs是用于调用目标函数的关键字参数字典。默认是{}。
  • daemon参数将显式地设置该线程是否为守护模式。如果是None(默认值),线程将继承当前线程的守护模式属性。(在3.3版更改:加入daemon参数。)
  • 注意:如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(Thread.__init__())。

2、Thread对象的方法

1、start()

  • 开始线程活动。
  • 它在一个线程里最多只能被调用一次。它安排对象的run()方法在一个独立的控制进程中调用。
  • 如果同一个线程对象中调用这个方法的次数大于一次,会抛出RuntimeError。

2、run()

  • 代表线程活动的方法。
  • 可以在子类中重写此方法。标准的run()方法会对作为target参数传递给该对象构造器的可调用对象(如果存在)发起调用,并附带从args和kwargs参数分别获取的位置和关键字参数。

3、join(timeout=None)

  • 阻塞调用这个方法的线程,直到被调用join()方法的线程终结--不管是正常终结还是抛出未处理异常,亦或者直到发生超时。
  • 当可选参数timeout存在而且不是None时,它可以是浮点数或者分数用于指定超时时长,以秒为单位的。
  • 因为join()总是返回None,所以你一定要在join()后调用is_alive()才能判断是否发生超时--如果被调用join()方法的线程仍然存活,则join()超时。
  • 一个线程可以被join()很多次。
  • 如果尝试加入当前线程会导致死锁,join()会引起RuntimeError异常。如果尝试join()一个尚未开始的线程,也会抛出相同的异常。

4、name

  • 只用于识别的字符串。它没有语义。多个线程可以赋予相同的名称。初始名称由构造函数设置。

5、native_id

  • 此线程的原生集成线程ID。这是一个非负整数,或者如果线程还未启动则为None。
  • 这表示线程ID(TID)已被OS(内核)赋值给线程。它的值可能被用来在全系统范围内唯一地标识这个特定线程(直到线程终结,在那之后该值可能会被OS回收再利用)。
  • 线程ID的有效期(全系统范围内保证唯一)将从线程被创建开始直到线程被终结。
  • 3.8新版功能

6、is_alive()

  • 返回线程是否存活。
  • 当run()方法刚开始直到run()方法刚结束,这个方法返回True。模块函数enumerate()返回包含所有存活线程的列表。

7、daemon

  • 表示这个线程是(True)否(False)为守护线程。
  • 一定要在调用start()前设置好,不然会抛出RuntimeError。初始值继承于创建线程;主线程不是守护线程,因此主线程创建的所有线程默认都是daemon=False。
  • 当没有存活的非守护线程时,整个Python程序才会退出。

2、创建线程

  • 使用Thread类,创建线程的三种方法:
    • 创建Thread的实例,传给它一个函数。
    • 派生Thread的子类,并创建子类的实例。
    • 创建Thread的实例,传给它一个可调用的类实例。
  • 当需要一个更加符合面向对象的接口时,会选择第二种方法,否则会选择第一种方法。第三种方法稍微难以阅读。

1、创建Thread的实例,传给它一个函数

示例1

###Python3.8
import threading
from time import sleep
def download(n):
    images = ['girl.jpg', 'boy.jpg', 'man.jpg']
    for image  in images:
        print('正在下载:', image)
        sleep(n)
        print('下载{}成功!'.format(image))
def listenMusic():
    musics = ['大碗面', '土耳其冰淇淋', '烤面筋']
    for music  in musics:
        sleep(0.5)
        print('正在听{}!'.format(music))
if __name__ == '__main__':
    t1 = threading.Thread(target=download, name='AA', args=(1,))    #创建线程对象t1
    t1.start()                                                      #启动线程t1
    t2 = threading.Thread(target=listenMusic, name='BB')
    t2.start()

示例2

###Python3.8
import threading
from time import sleep, ctime
loops = [4, 2]
def loop(nloop, nsec):
    print('loop', nloop, '开始:', ctime())
    sleep(nsec)
    print('loop', nloop, '结束:', ctime())
def main():
    print('main 开  始:', ctime())
    theads = []
    nloops = range(len(loops))
    for i  in nloops:
        print(loop, (i, loops[i]))
        t = threading.Thread(target=loop, args=(i, loops[i]))    #创建线程
        theads.append(t)
    for i  in nloops:
        theads[i].start()                                        #启动线程
    for i  in nloops:
        theads[i].join()                                         #让主进程让步
if __name__ == '__main__':
    main()

<<<
main 开  始: Wed Mar 24 10:15:33 2021
<function loop at 0x0000021238D771F0> (0, 4)
<function loop at 0x0000021238D771F0> (1, 2)
loop 0 开始: Wed Mar 24 10:15:33 2021
loop 1 开始: Wed Mar 24 10:15:33 2021
loop 1 结束: Wed Mar 24 10:15:35 2021
loop 0 结束: Wed Mar 24 10:15:37 2021

2、派生Thread 的子类,并创建子类的实例

  • 必须继承Thread类,子类中必须重写run()方法

示例1

import threading
from time import sleep, ctime
class MyThread(threading.Thread):          #派生Thread子类
    def __init__(self, func, args, name=''):
        threading.Thread.__init__(self)
        # super().__init__()
        self.name = name
        self.func = func
        self.args = args
    def run(self):                         #必须重写run方法
        self.func(*self.args)
def download(n):
    images = ['girl.jpg', 'boy.jpg', 'man.jpg']
    for image  in images:
        print('正在下载:', image)
        sleep(n)
        print('下载{}成功!'.format(image))
def listenMusic():
    musics = ['大碗面', '土耳其冰淇淋', '烤面筋']
    for music  in musics:
        sleep(0.5)
        print('正在听{}!'.format(music))
if __name__ == '__main__':
    t1 = MyThread(download, (1,), 'AA')     #创建线程t1
    t2 = MyThread(listenMusic, (), 'BB')    #创建线程t2,注意传递的是空元组
    t1.start()                              #启动线程t1
    t2.start()
    t1.join()                               #让主进程为线程t1让步
    t2.join()
    print('qqqqqqqq')

示例2

###Python3.8
import threading
from time import sleep, ctime
loops = [4, 2]
class MyThread(threading.Thread):                   #派生Thread子类
    def __init__(self, func, args, name=''):
        threading.Thread.__init__(self)
        # super().__init__()
        self.name = name
        self.func = func
        self.args = args
    def run(self):                                  #必须重写run方法
        self.func(*self.args)
def loop(nloop, nsec):
    print('loop', nloop, '开始:', ctime())
    sleep(nsec)
    print('loop', nloop, '结束:', ctime())
def main():
    print('main 开  始:', ctime())
    theads = []
    nloops = range(len(loops))
    for i  in nloops:
        print(loop, (i, loops[i]))
        t = MyThread(loop, (i, loops[i]), loop.__name__)    #创建线程
        theads.append(t)
    for i  in nloops:
        theads[i].start()                                   #启动线程
    for i  in nloops:
        theads[i].join()                                    #让主线程让步,否则‘qqq...’的打印位置不确定
if __name__ == '__main__':
    main()
    print('qqqqqqqqqqqqqqq')

<<<
main 开  始: Wed Mar 24 10:23:49 2021
<function loop at 0x000002A6771C93A0> (0, 4)
<function loop at 0x000002A6771C93A0> (1, 2)
loop 0 开始: Wed Mar 24 10:23:49 2021
loop 1 开始: Wed Mar 24 10:23:49 2021
loop 1 结束: Wed Mar 24 10:23:51 2021
loop 0 结束: Wed Mar 24 10:23:53 2021
qqqqqqqqqqqqqqq

3、创建Thread的实例,传给它一个可调用的类实例

###Python3.8
import threading
from time import sleep, ctime
loops = [4, 2]
class ThreadFunc:
    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('loop', nloop, '开始:', ctime())
    sleep(nsec)
    print('loop', nloop, '结束:', ctime())
def main():
    print('main 开  始:', ctime())
    theads = []
    nloops = range(len(loops))
    for i  in nloops:
        print(loop, (i, loops[i]))
        t = threading.Thread(target=ThreadFunc(loop, (i, loops[i]), loop.__name__))
        theads.append(t)
    for i  in nloops:
        theads[i].start()
    for i  in nloops:
        theads[i].join()
if __name__ == '__main__':
    main()
    print('qqqqqqqqqqqqqqq')

3、守护线程

  • threading模块支持守护线程,其工作方式是:守护线程一般是一个等待客户端请求服务的服务器。如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。
  • 要将一个线程设置为守护线程,需要在启动线程之前执行如下赋值语句:thread.daemon=True(调用thread.setDaemon(True)的旧方法已经弃用了)。或者通过构造函数的daemon参数来设置。
  • 要检查线程的守护状态,也只需要检查这个值即可(对比过去调用thread.isDaemon()的方法)。一个新的子线程会继承父线程的守护标记
  • 整个Python程序(可以解读为:主线程)将在所有非守护线程退出之后才退出。即当剩下的线程都是守护线程时,整个Python程序将会退出。
  • 守护线程在程序关闭时会突然关闭。他们的资源(例如已经打开的文档,数据库事务等等)可能没有被正确释放。如果你想你的线程正常停止,设置他们成为非守护模式并且使用合适的信号机制。

4、单线程和多线程执行对比

###Python3.8
import threading
from time import sleep, ctime
class MyThread(threading.Thread):               #创建Thread的子类
    def __init__(self, func, args, name=''):    #重写__init__方法
        threading.Thread.__init__(self)
        self.name = name
        self.func = func
        self.args = args
    def getResult(self):
        return return '{}函数的结果是{}'.format(self.name, self.res)
    def run(self):                              #重写run方法
        print('{}函数开始:{}'.format(self.name, ctime()))
        self.res = self.func(*self.args)
        print('{}函数结束:{}'.format(self.name, ctime()))
def fib(x):                                     #定义斐波那契函数
    sleep(0.1)
    if x < 2: return 1
    return (fib(x - 2) + fib(x - 1))
def fac(x):                                     #定义阶乘函数
    sleep(0.5)
    if x < 2: return 1
    return (x * fac(x - 1))
def sum(x):                                     #定义累加和函数
    sleep(1)
    if x < 2: return 1
    return (x + sum(x - 1))
funcs = [fib, fac, sum]
n = 10
nfuncs = range(len(funcs))
def main1():                                   #调用后以单线程的方式执行
    print('单线程开始:', ctime())
    for i  in nfuncs:
        print('{}函数开始:{}'.format(funcs[i].__name__, ctime()))
        print('{}函数的结果是{}'.format(funcs[i].__name__, funcs[i](n)))
        print('{}函数结束:{}'.format(funcs[i].__name__, ctime()))
    print('单线程结束:', ctime())
def main2():                                   #调用后以多线程的方式执行
    threads = []
    print('多线程开始:', ctime())
    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()                      #注意第一函数fib执行时间最长,因此第一个线程结束后才输出结果
        print(threads[i].getResult())
    print('多线程结束:', ctime())
if __name__ == '__main__':
    main1()

<<<
单线程开始: Wed Mar 24 14:01:32 2021
fib函数开始:Wed Mar 24 14:01:32 2021
fib函数的结果是89
fib函数结束:Wed Mar 24 14:01:50 2021
fac函数开始:Wed Mar 24 14:01:50 2021
fac函数的结果是3628800
fac函数结束:Wed Mar 24 14:01:55 2021
sum函数开始:Wed Mar 24 14:01:55 2021
sum函数的结果是55
sum函数结束:Wed Mar 24 14:02:05 2021
单线程结束: Wed Mar 24 14:02:05 2021
<<<
多线程开始: Wed Mar 24 14:06:46 2021
fib函数开始:Wed Mar 24 14:06:46 2021
fac函数开始:Wed Mar 24 14:06:46 2021
sum函数开始:Wed Mar 24 14:06:46 2021
fac函数结束:Wed Mar 24 14:06:52 2021
sum函数结束:Wed Mar 24 14:06:56 2021
fib函数结束:Wed Mar 24 14:07:04 2021
fib函数的结果是89
fac函数的结果是3628800
sum函数的结果是55
多线程结束: Wed Mar 24 14:07:04 2021

7、threading模块--同步原语

  • 一个进程中的各个线程与主线程共享同一片数据空间。如果两个线程运行的顺序发生变化,就有可能造成代码的执行轨迹或行为不相同,或者产生不一致的数据。
  • 一般在多线程代码中,总会有一些特定的函数或代码块不希望(或不应该)被多个线程同时执行,通常包括修改数据库、更新文件或其他会产生竞态条件的类似情况。
  • 当任意数量的线程可以访问临界区的代码,但在给定的时刻只有一个线程可以通过时,就是使用同步的时候了。
  • 介绍两种类型的同步原语:锁/互斥信号量

1、原始锁(Lock类)

  • 原始锁是一个在锁定时不属于特定线程的同步基元组件。在Python中,它是能用的最低级的同步基元组件,由_thread扩展模块直接实现。
  • 原始锁处于"锁定"或者"非锁定"两种状态之一。它被创建时为非锁定状态。
  • 它有两个基本方法,acquire()和release()。(获得锁和释放锁
    • 当状态为非锁定时,acquire()将状态改为锁定并立即返回。当状态是锁定时,acquire()将阻塞至其他线程调用release()将其改为非锁定状态,然后acquire()调用重置其为锁定状态并返回。
    • release()只在锁定状态下调用它将状态改为非锁定并立即返回。如果尝试释放一个非锁定的锁,则会引发RuntimeError异常。
  • 锁同样支持上下文管理协议。
  • 当多线程争夺锁时,允许第一个获得锁的线程进入临界区,并执行代码。所有之后到达的线程被阻塞,直到第一个线程执行结束,退出临界区,并释放锁。此时,其他等待的线程可以获得锁并进入临界区。不过,那些被阻塞的线程是没有顺序的(即不是先到先执行),胜出的线程是不确定的,而且还会根据Python 实现的不同而有所区别。

1、Lock类的构造函数

class threading.Lock

  • 实现原始锁对象的类。
  • 一旦一个线程获得一个锁,会阻塞随后尝试获得这个锁的线程,直到它被释放;任何线程都可以释放它

2、Lock对象的方法

1、acquire(blocking=True, timeout=-1)

  • 可以阻塞或非阻塞地获得锁。
  • 当参数blocking为True(缺省值)。若可以获得锁返回True,并将锁锁定;若不能获取锁,将发生阻塞,直到锁被释放或超时返回False。
  • 当参数blocking为False,将不会发生阻塞。
  • 当浮点型参数timeout为正值时,只要无法获得锁,将最多阻塞timeout设定的秒数。
  • 当参数timeout为-1时将无限等待。
  • 当blocking为false时,timeout指定的值将被忽略。
  • 如果成功获得锁,则返回 True,否则返回 False (例如发生 超时 的时候)。

2、release()

  • 释放一个锁,没有返回值。这个方法可以在任何线程中调用,不单指获得锁的线程。
  • 当锁被锁定时,它将锁重置为未锁定,并返回。如果其他线程正在等待这个锁解锁而被阻塞,只允许其中一个线程获取锁。
  • 在未锁定的锁调用时,会引发RuntimeError异常。

3、locked()

  • 如果获得了锁则返回真值。

3、死锁

  • 开发过程中使用线程,在线程同共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会适成死锁。尽管死锁很少发生,但一旦发生就会造成应用停止,程序不做任何事情。
  • 解决死锁问题:

1、重构代码
2、使用timeout参数

示例1:不使用锁

  • 两个线程不加锁同时操作公有数据,操作次数少时可能不会有问题,但次数多的时候就一定会有问题。
###Python3.8
import threading
n = 100
def func1():
    global n
    for i  in range(100000):
        n += 1
def func2():
    global n
    for i  in range(100000):
        n += 1
if __name__ == '__main__':
    t1 = threading.Thread(target=func1, name='AA')
    t2 = threading.Thread(target=func2, name='BB')
    t1.start()
    t2.start()
    t1.join()
    t1.join()
    print(n)    #因为没有加锁,打印的n值是不确定

示例2:使用锁,不可变类型的变量

###Python3.8
import threading
lock = threading.Lock()        #创建锁对象
n =100                         #声明不可变类型变量
def func1():
    global n
    for i  in range(100000):    #每次循环都会重新获取和释放锁
        lock.acquire()         #获取锁
        n += 1
        lock.release()         #释放锁
def func2():
    global n
    for i  in range(100000):
        lock.acquire()         #获取锁
        n += 1
        lock.release()         #释放锁
if __name__ == '__main__':
    t1 = threading.Thread(target=func1, name='AA')
    t2 = threading.Thread(target=func2, name='BB')
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(n)                   #加锁了,n的值是正确的

示例3:可变类型的变量

###Python3.8
import threading
from time import sleep
lock = threading.Lock()    #创建一个锁对象
lst = [0] * 10             #声明可变类型的变量
def func1():
    lock.acquire()         #获取锁
    for i  in range(len(lst)):
        lst[i] = i
        sleep(0.01)        #线程主动让出CPU的控制权,但没有释放锁
    lock.release()         #循环结束后才会释放锁
def func2():
    lock.acquire()         #上个锁不释放,就获取不到锁,将阻塞等待获取锁    #注意这里使用的是同一个锁对象,才会阻塞
    for i  in range(len(lst)):
        print('---->', lst[i])
    lock.release()
if __name__ == '__main__':
    t1 = threading.Thread(target=func1, name='AA')
    t2 = threading.Thread(target=func2, name='BB')
    t1.start()
    t2.start()
    t1.join()
    t2.join()

示例4:死锁

from threading import Thread, Lock
import time
lock1 = Lock()
lock2 = Lock()
class MyThread1(Thread):
    def run(self):
        if lock1.acquire():                 #如果可以获取到锁,返回True
            print(self.name + '获取了锁lock1')
            time.sleep(0.1)
            if lock2.acquire(timeout=3):    #注意超时时间是3秒    #超时后将继续执行后面的代码
                print(self.name + '获取了锁lock2')
                lock2.release()
            print('锁超时,后面的代码')
            lock1.release()
class MyThread2(Thread):
    def run(self):
        if lock2.acquire():
            print(self.name + '获取了锁lock2')
            time.sleep(0.1)
            if lock1.acquire(timeout=6):    #注意超时时间是6秒,超时时间比上面的长,将会得到锁lock1
                print(self.name + '获取了锁lock1')
                lock1.release()
            lock2.release()
if __name__ == '__main__':
    t1 = MyThread1(target=MyThread1, name='t1')
    t2 = MyThread2(target=MyThread2, name='t2')
    t1.start()
    t2.start()

<<<
t1获取了锁lock1
t2获取了锁lock2
锁超时,后面的代码
t2获取了锁lock1

2、信号量(BoundedSemaphoreLock类)

  • 这是计算机科学史上最古老的同步原语之一,早期的荷兰科学家Edsger W. Dijkstra发明了它。(他使用名称P()和V()而不是acquire()和release())。
  • 信号量方法的名字和原始锁的一样:acquire和release。
  • 一个信号量管理一个内部计数器当资源消耗时递减当资源释放时递增。即该计数器因acquire()方法的调用而递减,因release()方法的调用而递增。
  • 计数器的值永远不会小于零。当acquire()方法发现计数器为零时,将会阻塞,直到其它线程调用release()方法。
  • 信号量通常用于保护数量有限的资源,例如数据库服务器。
  • 信号量比锁更加灵活,因为可以有多个线程,每个线程拥有有限资源的一个实例。
  • threading模块包括两种信号量类:
    • Semaphore和BoundedSemaphore。
    • BoundedSemaphore的一个额外功能是这个计数器的值永远不会超过它的初始值,换句话说,它可以防范其中信号量释放次数多于获得次数的异常用例。
  • 信号量对象也支持上下文管理协议。

1、Semaphore和BoundedSemaphore类的构造函数

class threading.Semaphore(value=1)

  • 可选参数value赋予内部计数器初始值,默认值为1。
  • 如果value被赋予小于0的值,将会引发ValueError异常。

class threading.BoundedSemaphore(value=1)

  • 可选参数value赋予内部计数器初始值,默认值为1。
  • 该类实现有界信号量。有界信号量通过检查以确保它当前的值不会超过初始值。如果超过了初始值,将会引发ValueError异常。
  • 在大多情况下,信号量用于保护数量有限的资源。如果信号量被释放的次数过多,则表明出现了错误。

2、信号量对象的方法

1、acquire(blocking=True, timeout=None)

  • 当参数blocking为True时:
    • 如果内部计数器的值大于零,则将其减1并立即返回True。
    • 如果内部计数器的值等于零,则将会阻塞,直到被对release()的调用唤醒。一旦被唤醒(并且计数器的值大于0),则将计数器减1并返回True。每次对release()的调用将只唤醒一个线程。线程被唤醒的次序是不可确定的。
  • 当参数blocking为False时,则不进行阻塞
    • 如果内部计数器的值大于零,则将其减1并立即返回True。
    • 如果内部计数器的值等于零,则将返回False。
  • 当发起调用时如果timeout不为None,则它将阻塞最多timeout秒。如果请求在此段时未能成功获取则将返回False,在其他情况下返回True。

2、release()

  • 释放一个信号量,将内部计数器的值增加1。当计数器原先的值为0且有其它线程正在等待它再次大于0时,唤醒正在等待的线程。

示例1:BoundedSemaphore类

  • 在资源数量固定的任何情况下,都应该使用有界信号量。在生成任何工作线程前,应该在主线程中初始化信号量。
###python3.8
#!/usr/bin/env python
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...', end='')
    try:
        candytray.release()         #释放资源
    except ValueError:
        print('full, skipping')
    else:
        print('OK111')
        lock.release()
def buy():                          #消费者获取一个单位的库存
    lock.acquire()
    print('Buying candy...', end='')
    if candytray.acquire(False):    #消耗资源
        print('OK222')
    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()  # buy
    Thread(target=producer, args=(nloops,)).start()                               # refill
@register                           #atexit.register()函数(这里使用了装饰器的方式)会在Python解释器中注册一个退出函数,它会在脚本退出之前请求调用这个特殊函数。
def _atexit():
    print('all DONE at:', ctime())
if __name__ == '__main__':
    _main()

示例2:Semaphore类

import threading
from time import sleep

def func(i):    #注意:这是个函数
    if semaphore.acquire():
        lock.acquire()
        print('func{}开始了'.format(i))
        lock.release()
        sleep(5)
        semaphore.release()
        lock.acquire()
        print('func{}结束了'.format(i))
        lock.release()

semaphore = threading.Semaphore(5)
lock = threading.Lock()
for i  in range(1, 10):
    t = threading.Thread(target=func, args=(i,))
    t.start()

示例3:Semaphore类

import threading
from time import sleep

class myThread(threading.Thread):    #注意:这是Thread类派生的子类
    def __init__(self, i):
        threading.Thread.__init__(self)
        self.i = i
    def run(self):
        if semaphore.acquire():
            lock.acquire()
            print('func{}开始了'.format(self.i))
            lock.release()
            sleep(5)
            semaphore.release()
            lock.acquire()
            print('func{}结束了'.format(self.i))
            lock.release()

semaphore = threading.Semaphore(5)
lock = threading.Lock()
for i  in range(1, 10):
    t = myThread(i)
    t.start()

3、递归锁(RLock类)

  • 递归锁是一个可以被同一个线程多次获取的同步基元组件
  • 在内部,它在基元锁的锁定/非锁定状态上附加了"所属线程"和"递归等级"的概念。
  • 在锁定状态下,某些线程拥有锁;在非锁定状态下,没有线程拥有它。
  • 若要锁定锁,线程调用其acquire()方法;一旦线程拥有了锁,方法将返回。若要解锁,线程调用release()方法。
  • acquire()/release()对可以嵌套;只有最终release()(最外面一对的release())将锁解开,才能让其他线程继续处理acquire()阻塞。
  • 递归锁也支持上下文管理协议。

1、RLock类的构造函数

class threading.RLock

  • 此类实现了递归锁对象。递归锁必须由获取它的线程释放。一旦线程获得了递归锁,同一个线程再次获取它将不阻塞;线程必须在每次获取它时释放一次。
  • 注意:RLock其实是一个工厂函数,返回平台支持的具体递归锁类中最有效的版本的实例。

2、RLock对象的方法

1、acquire(blocking=True, timeout=-1)

  • 可以阻塞或非阻塞地获得锁。
  • 当无参数调用时:
    • 如果这个线程已经拥有锁,递归级别增加一,并立即返回。
    • 如果其他线程拥有该锁,则阻塞至该锁解锁。一旦锁被解锁(不属于任何线程),则抢夺所有权,设置递归等级为一,并返回。如果多个线程被阻塞,等待锁被解锁,一次只有一个线程能抢到锁的所有权。在这种情况下,没有返回值。
  • 当blocking为True,则执行与无参数调用时一样的操作,然后返回True。
  • 当blocking为False,则不进行阻塞。
    • 如果其他线程拥有该锁,则立即返回False;
    • 在其他情况下,执行与无参数调用时一样的操作,然后返回True。
  • 当浮点数参数timeout设为正值时,只要无法获得锁,将最多阻塞timeout所指定的秒数。如果已经获得锁则返回True,如果超时则返回False。

2、release()

  • 释放锁,自减递归等级,没有返回值。
  • 如果自减后,递归等级是零,则将锁重置为非锁定状态(不被任何线程拥有),并且,如果其他线程正被阻塞着等待锁被解锁,则仅允许其中一个线程继续。
  • 如果自减后,递归等级不是零,则锁保持锁定,仍由调用线程拥有。
  • 只有当前线程拥有锁才能调用这个方法。如果锁被释放后调用这个方法,会引起RuntimeError异常。

示例

from threading import Thread, RLock
import time
lock = RLock()
class MyThread1(Thread):
    def run(self):
        if lock.acquire():                 #如果可以获取到锁,返回True
            print(self.name + '获取了锁lock1')
            time.sleep(9)
            if lock.acquire(timeout=3):    #注意超时时间是3秒
                print(self.name + '获取了锁lock2')
                lock.release()
            lock.release()
class MyThread2(Thread):
    def run(self):
        if lock.acquire(timeout=6):        #注意超时时间是6秒,超时时间比上面的短,将超时并且不能获得锁,返回False
            print(self.name + '获取了锁lock11')
            time.sleep(0.1)
            if lock.acquire():
                print(self.name + '获取了锁lock22')
                lock.release()
            lock.release()
        else:
            print('锁超时,后面的代码')
if __name__ == '__main__':
    t1 = MyThread1(target=MyThread1, name='t1')
    t2 = MyThread2(target=MyThread2, name='t2')
    t1.start()
    t2.start()

4、栅栏对象(Barrier类)

  • Barrier类提供一个简单的同步原语,用于固定数量的线程需要彼此相互等待
  • 线程调用wait()方法后将阻塞,直到所有线程都调用了wait()方法,此时所有线程将被同时释放。
  • 栅栏对象可以被多次使用,但进程的数量不能改变。

1、Barrier类的构造函数

class threading.Barrier(parties, action=None, timeout=None)

  • 创建一个需要parties个线程的栅栏对象。
  • 如果提供了可调用的action参数,它会在所有线程被释放时在其中一个线程中自动调用。
  • timeout是默认的超时时间,如果没有在wait()方法中指定超时时间的话。

2、Barrier对象的方法

1、wait(timeout=None)

  • 冲出栅栏。当Barrier对象中所有线程都已经调用了这个函数,它们将同时被释放。如果提供了timeout参数,这里的timeout参数优先于创建栅栏对象时提供的timeout参数。
  • 函数返回值是一个整数,取值范围在0到parties-1,在每个线程中的返回值不相同。可用于从所有线程中选择唯一的一个线程执行一些特别的工作。
  • 如果创建栅栏对象时在构造函数中提供了action参数,它将在其中一个线程释放前被调用。如果此调用引发了异常,栅栏对象将进入损坏态。
  • 如果发生了超时,栅栏对象将进入破损态。
  • 如果栅栏对象进入破损态,或重置栅栏时仍有线程等待释放,将会引发BrokenBarrierError异常。

2、reset()

  • 重置栅栏为默认的初始态。如果栅栏中仍有线程等待释放,这些线程将会收到BrokenBarrierError异常。
  • 请注意使用此函数时,如果存在状态未知的其他线程,则可能需要执行外部同步。如果栅栏已损坏则最好将其废弃并新建一个。

3、abort()

  • 使栅栏处于损坏状态。这将导致任何现有和未来对wait()的调用失败并引发BrokenBarrierError。例如可以在需要中止某个线程时使用此方法,以避免应用程序的死锁。
  • 更好的方式是:创建栅栏时提供一个合理的超时时间,来自动避免某个线程出错。

4、parties

  • 冲出栅栏所需要的线程数量。

5、n_waiting

  • 当前时刻正在栅栏中阻塞的线程数量。

6、broken

  • 一个布尔值,值为True表明栅栏为破损态。

示例

import threading
from time import sleep

def func1(barrier):
    print('func1开始了')
    sleep(1)
    barrier.wait()
    print('func1结束了')
def func2(barrier):
    print('func2开始了')
    sleep(3)
    barrier.wait()
    print('func2结束了')
def func3(barrier):
    print('func3开始了')
    sleep(7)
    barrier.wait()
    print('func3结束了')

barrier = threading.Barrier(3)
t1 = threading.Thread(target=func1, args=(barrier,))
t2 = threading.Thread(target=func2, args=(barrier,))
t3 = threading.Thread(target=func3, args=(barrier,))
t1.start()
t2.start()
t3.start()
sleep(2)
print(barrier.parties)
print(barrier.n_waiting)
print(barrier.broken)
print('wwwwwwwww')

8、threading模块--其他类

1、条件对象(Condition类)

  • 条件变量总是与某种类型的锁对象相关联,锁对象可以通过传入获得,或者在缺省的情况下自动创建
  • 当多个条件变量需要共享同一个锁时,传入一个锁很有用。锁是条件对象的一部分,你不必单独地跟踪它。
  • 条件变量服从上下文管理协议:使用with语句会在它包围的代码块内获取关联的锁。acquire()和release()方法也能调用关联锁的相关方法。
  • 其它方法必须在持有关联的锁的情况下调用。wait()方法释放锁然后阻塞直到其它线程调用notify()方法或notify_all()方法唤醒它。一旦被唤醒,wait()方法重新获取锁并返回。它也可以指定超时时间。
  • notify()方法唤醒等待条件变量的线程之一(如果有线程正在等待)。notify_all()方法唤醒所有等待条件变量的线程。
    • 注意:notify()方法和notify_all()方法并不会释放锁,这意味着被唤醒的线程不会立即从它们的wait()方法调用中返回,而是会在调用notify()方法或notify_all()方法的线程最终放弃了锁的所有权后返回
  • 使用条件变量的典型编程风格是将锁用于同步某些共享状态的权限,那些对状态的某些特定改变感兴趣的线程,它们重复调用wait()方法,直到看到所期望的改变发生;而对于修改状态的线程,它们将当前状态改变为可能是等待者所期待的新状态后,调用notify()方法或者notify_all()方法。

1、Condition类的构造函数

class threading.Condition(lock=None)

  • 实现条件变量对象的类。一个条件变量对象允许一个或多个线程在被其它线程所通知之前进行等待。
  • 参数lock的值,如果不是None,就必须是Lock或RLock对象,并且它将被用作底层锁。否则,将会自动创建RLock对象,并将其用作底层锁。

2、Condition对象的方法

1、acquire(*args)

  • 请求底层锁。此方法调用底层锁的相应方法,返回值是底层锁相应方法的返回值。

2、release()

  • 释放底层锁。此方法调用底层锁的相应方法。没有返回值。

3、wait(timeout=None)

  • 等待直到被通知或发生超时。线程在调用此方法时必须已获得锁,否则将会引发RuntimeError异常。
  • wait()方法释放底层锁,然后阻塞,直到在另外一个线程中调用同一个条件变量的notify()或notify_all()唤醒它,或者直到可选的超时发生。一旦被唤醒或者超时,它重新获得锁并返回
  • 当提供了timeout参数且不是None时,它应该是一个浮点数,代表操作的超时时间,以秒为单位(可以为小数)。
  • 当底层锁是个RLock,不会使用它的release()方法释放锁,因为当它被递归多次获取时,实际上可能无法解锁。相反,使用了RLock类的内部接口,即使多次递归获取它也能解锁它。然后,在重新获取锁时,使用另一个内部接口来恢复递归级别。
  • 返回True,除非提供的timeout过期,会返回False。

4、wait_for(predicate, timeout=None)    #(3.2新版功能)

  • 等待,直到条件计算为真。predicate应该是一个可调用对象而且它的返回值可被解释为一个布尔值。可以提供timeout参数给出最大等待时间。
  • 这个实用方法会重复地调用wait()直到满足判断式或者发生超时。返回值是判断式最后一个返回值,而且如果方法发生超时会返回False。
  • 锁必须在被调用时保持获取,并在返回时重新获取。随着锁定执行判断式。

5、notify(n=1)

  • 默认唤醒一个等待这个条件的线程。线程在调用此方法时必须已获得锁,会引发RuntimeError异常。
  • 这个方法唤醒最多n个正在等待这个条件变量的线程;如果没有线程在等待,这是一个空操作。
  • 当前实现中,如果至少有n个线程正在等待,准确唤醒n个线程。但是依赖这个行为并不安全。未来,优化的实现有时会唤醒超过n个线程。

6、notify_all()

  • 唤醒所有正在等待这个条件的线程。这个方法行为与notify()相似,但并不只唤醒单一线程,而是唤醒所有等待线程。线程在调用此方法时必须已获得锁,会引发RuntimeError异常。

示例1

import threading
from time import sleep

def func1():
    condition.acquire()      #获得锁
    condition.wait()         #解锁并阻塞,等待nolify的唤醒
    print('func1:条件以满足')
    condition.release()
def func2():
    condition.acquire()      #获得锁
    condition.wait()         #解锁并阻塞,等待nolify的唤醒
    print('func2:条件以满足')
    condition.release()
def func3():
    sleep(3)
    print('func3:调用notify')
    condition.acquire()      #获得锁
    condition.notify(n=2)    #唤醒等待的线程,只唤醒其中的两个
    condition.release()

condition = threading.Condition()
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t3 = threading.Thread(target=func3)
t1.start()
t2.start()
t3.start()

示例2

import threading, time
from random import randint
class Producer(threading.Thread):      #定义生产者类
    def run(self):
        while True:
            val = randint(0, 100)
            lock.acquire()             #print加原始锁是为了打印不乱
            print('生产者', self.name, '生产了:', val)
            lock.release()
            if condition.acquire():    #生产者是否获得条件对象的锁。可以获得,直接生产。不能获得锁,阻塞并等待锁
                lst.append(val)
                condition.notify()     #通知wait,等待的条件已成立
                condition.release()    #释放条件对象的锁    ###注意这时才激活wait方法
            time.sleep(5)
class Consumer(threading.Thread):      #定义消费者类
    def run(self):
        while True:
            condition.acquire()        #获得条件对象的锁
            if len(lst) == 0:          #判断是否有商品,可供消费
                lock.acquire()
                print('---已消费完,等待生产者生产---')
                lock.release()
                condition.wait()       #没有商品,就释放锁并阻塞,等待notify的通知
            lock.acquire()
            print('消费者', self.name, '消费了:', lst[0])
            lock.release()
            del lst[0]
            condition.release()        #释放条件对象的锁
            time.sleep(0.2)

lst = []
lock= threading.Lock()                 #创建原始锁对象
condition = threading.Condition()      #创建条件对象
threads = []
for i  in range(5):
    threads.append(Producer())         #创建5个生产者对象,并添加进threads列表 
threads.append(Consumer())             #创建消费者对象,并添加进threads列表
for t  in threads:
    t.start()
for t  in threads:
    t.join()

2、事件对象(Event类)

  • Event类是线程之间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号。
  • 事件对象与条件对象的作用类似,区别是事件对象没有锁功能,这是因为事件对象不用于需要访问共享资源的环境中

1、Event类的构造函数

class threading.Event

  • 实现事件对象的类。
  • 事件对象管理一个内部标志,调用set()方法可将其设置为true。调用clear()方法可将其设置为false。调用wait()方法将进入阻塞直到标志为true。这个标志初始时为false
  • 在python3.3版更改: 从工厂函数变为类。

2、Event对象的方法

1、is_set()

  • 当且仅当内部标志为True时返回True。

2、set()

  • 将内部标志设置为true。所有正在等待这个事件的线程将被唤醒。当标志为true时,调用wait()方法的线程不会被被阻塞。

3、clear()

  • 将内部标志设置为false。之后调用wait()方法的线程将会被阻塞,直到调用set()方法将内部标志再次设置为true。

4、wait(timeout=None)

  • 阻塞线程直到内部变量为true。如果调用时内部标志为true,将立即返回。否则将阻塞线程,直到调用set()方法将标志设置为true或者发生超时。
  • 当可选参数timeout不是None时,它应该是一个浮点数,代表操作的超时时间,以秒为单位。
  • 当且仅当内部标志被设置为True时(在wait()调用之前或wait()开始之后),该方法返回True。也就是说,它将总是返回True除非设定了超时且操作发生了超时。

示例1:老板与员工

import threading, time
class Boss(threading.Thread):
    def run(self):
        print('BOSS:今天加班到22:00')
        event.is_set() or event.set()
        time.sleep(6)
        print('BOSS:22:00了,大家可以下班了')
        event.is_set() or event.set()
class Worker(threading.Thread):
    def run(self):
        event.wait()     #阻塞线程直到内部变量为true
        print('Worker:命苦啊,又要加班啦!')
        time.sleep(0.2)
        event.clear()    #将内部标志设置为false
        event.wait()     #阻塞线程直到内部变量为true
        print('Worker:终于可以下班了。')

event = threading.Event()
threads = []
for i  in range(5):
    threads.append(Worker())  # 创建5个生产者对象,并添加进threads列表
threads.append(Boss())  # 创建消费者对象,并添加进threads列表
for t  in threads:
    t.start()
for t  in threads:
    t.join()

示例2:红路灯

import threading
import time, random
def light():
    if not event.is_set():
        event.set()               #设置内置标志是Ture为绿灯
    count = 0
    while True:
        if count < 10:
            print('\033[42;1m--green light on--\033[0m')
        elif count < 13:
            print('\033[43;1m--yellow light on--\033[0m')
        elif count < 20:
            if event.is_set():
                event.clear()     #设置False为红灯
            print('\033[41;1m--red light on--\033[0m')
        else:                     #重置红绿灯,为绿灯
            count = 0
            event.set()
        time.sleep(1)
        count += 1
def car(n):
    while True:
        time.sleep(random.randrange(10))
        if event.is_set():        #为Ture时即绿灯,让车通行
            print('car{} is running...'.format(n))
        else:                     #为False时即红灯,让车等待
            print('car{} is waiting for the red light...'.format(n))

event = threading.Event()
lt = threading.Thread(target=light)
lt.start()
for i  in range(1, 6):
    c = threading.Thread(target=car, args=(i,))
    c.start()

3、定时器对象(Timer类)

  • Timer类表示一个操作应该在等待一定的时间之后运行,相当于一个定时器。
  • Timer类是Thread类的子类,因此可以像一个自定义线程一样工作。
  • 与线程一样,通过调用start()方法启动定时器
  • 通过调用cancel()方法可以停止计时器(在计时结束前),定时器在执行其操作之前等待的时间间隔可能与用户指定的时间间隔不完全相同。

1、Timer类的构造函数

class threading.Timer(interval, function, args=None, kwargs=None)

  • 创建一个定时器,在经过interval秒时间后,将会用参数args和关键字参数kwargs调用function。如果args为None(默认值),则会使用一个空列表。如果kwargs为None(默认值),则会使用一个空字典。
  • 在3.3版更改:从工厂函数变为类。

2、Timer对象的方法

1、cancel()

  • 停止定时器并取消执行计时器将要执行的操作。仅当计时器仍处于等待状态时有效。

示例

import threading
def hello():
    print("hello, world")

t = threading.Timer(30.0, hello)  #30秒后执行hello函数
t.start()

9、多线程利器(queue)

  • 使用queue模块(Python2.x版本名为Queue)来提供线程间的通信机制,从而让线程之间可以互相分享数据。
  • 具体而言,就是创建一个队列,让生产者(线程)在其中放入新的数据,而消费者(线程)消费这些数据。
  • queue模块可以参看 我的queue模块,也可以参看 官方的queue的模块

1、queue模块常用属性

 示例

import threading
import queue
import random
import time
lock = threading.Lock()
def produce(q):
    i = 0
    while i < 10:
        lock.acquire()
        num = random.randint(1, 100)
        q.put(num)
        print('生产者产生数据:%d' % num)
        time.sleep(0.5)
        i += 1
        lock.release()
def consume(q):
    i = 0
    while True:
        lock.acquire()
        if q.empty():
            print('空')
            if i ==10:
                break
        else:
            item = q.get()
            print('消费者获取数据:%d' % item)
            i += 1
        lock.release()
if __name__ == '__main__':
    q = queue.Queue(10)
    t1 = threading.Thread(target=produce, args=(q,))    #将队列当作参数传递给线程
    t2 = threading.Thread(target=consume, args=(q,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('******END******') 

2、task_done()的作用

  • tase_done()的作用:只有消费者把队列所有的数据处理完毕,queue.join()才会停止阻塞
  • task_done()的作用:消费者处理一个任务,它就通知生产者我处理了一个任务。
    • 1.不注释q.task_done(),不注释q.join(),我们发现produce函数在消费者消费完后才结束,而不是生产完就结束。
    • 2.注释q.task_done(),注释q.join(),我们发现生产者生产完任务produce函数就结束了,而不是等到消费者消费完才结束。
    • 3.注释q.task_done(),不注释q.join(),我们发现produce函数一直阻塞没有结束。
import time
from queue import Queue
from threading import Thread, Lock
lock = Lock()
q = Queue()
def produce():
    for i  in range(10):
        lock.acquire()
        q.put(i)
        print('生产:', i)
        lock.release()
    print('生产任务完毕!')
    q.join()                #q.join
    print(produce.__name__, '函数结束!')
def consumer():
    for i  in range(10):
        lock.acquire()
        print('消费:', q.get())
        q.task_done()       #q.task_done
        if i == 4:
            print('休息1s...')
            time.sleep(1)    #sleep作用:查看生产者是否阻塞
        lock.release()
    print(consumer.__name__, '函数结束!')
pro = Thread(target=produce)
con = Thread(target=consumer)
pro.start()
con.start()
con.join()
print('消费者任务完成')
pro.join()
print('生产者任务完成') 
posted @ 2021-07-08 11:47  麦恒  阅读(183)  评论(0编辑  收藏  举报