Python进阶并发基础--线程,全局解释器锁GIL由来,如何更好的利用Python线程,

全局解释器锁GIL

官方对于线程的介绍:

在 CPython 中,由于存在全局解释器锁,同一时刻只有一个线程可以执行 Python代码(虽然某些性能导向的库可能会去除此限制)。如果你想让你的应用更好地利用多核心计算机的计算资源,推荐你使用multiprocessing或concurrent.futures.ProcessPoolExecutor。但是,如果你想要同时运行多个 I/O 密集型任务,则多线程仍然是一个合适的模型。
该模块的设计基于 Java 的线程模型。但是,在 Java 里面,锁和条件变量是每个对象的基础特性,而在Python 里面,这些被独立成了单独的对象Python 的Thread 类只是 Java 的 Thread 类的一个子集;目前还没有优先级,没有线程组,线程还不能被销毁、停止、暂停、恢复或中断。Java 的 Thread 类的静态方法在实现时会映射为模块级函数。

那么python的线程是否真的一无是处?

Python代码的执行是由Python虚拟机(又名解释器主循环)进行控制的。Python在设计时是这样考虑的,在主循环中同时只能由一个控制线程在执行,就像单核CPU系统中的多进程一样。内存中可以有许多程序,但是在任意给定时刻只能有一个程序在运行。同理,尽管Python解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。

对Python虚拟机的访问是由全局解释器锁(GIL)控制的。这个锁就是用来保证同时只能有一个线程运行的。在多线程环境中,Python虚拟机将按照下面所述方式执行。
1.设置GIL。
2.切换进一个线程去运行。
3.执行下面操作之一:
a.指定数量的字节码指令。
b.线程主动让出控制权。
4.把线程设置回睡眠状态(切换出线程)。
5.解锁GIL。
6.重复上述步骤。

当调用外部代码(即,任意C/C++扩展的内置函数)时,GIL会保持锁定,直至函数执行结束(因为在这期间没有Python字节码计数)。编写扩展函数的程序员有能力解锁GIL,然而,作为Python开发者,你并不需要担心Python代码会在这些情况下被锁住。

对于任意面向I/O的Python例程(调用了内置的操作系统C代码的那种),GIL会在I/O调用前被释放,以允许其他线程在I/O执行的时候运行。而对于那些没有太多I/O操作的代码而言,更倾向于在该线程整个时间片内始终占有处理器(和GIL)。换句话说就是,I/O密集型的Python程序要比计算密集型的代码能够更好的利用多线程环境。

CPU密集型
CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作CPU读写IO(硬盘/内存)时,IO可以在很短的时间内完成,而CPU还有许多运算要处理,因此,CPU负载很高。
CPU密集表示该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就只有这么多。
CPU使用率较高(例如:计算圆周率、对视频进行高清解码、矩阵运算等情况)的情况下,通常,线程数只需要设置为CPU核心数的线程个数就可以了。 这一情况多出现在一些业务复杂的计算和逻辑处理过程中。比如说,现在的一些机器学习和深度学习的模型训练和推理任务,包含了大量的矩阵运算。

IO密集型
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等IO (硬盘/内存) 的读写操作,因此,CPU负载并不高。
密集型的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而程序的逻辑做得不是很好,没有充分利用处理器能力。
CPU 使用率较低,程序中会存在大量的 I/O 操作占用时间,导致线程空余时间很多,通常就需要开CPU核心数数倍的线程。
其计算公式为:IO密集型核心线程数 = CPU核数 / (1-阻塞系数)。
当线程进行 I/O 操作 CPU 空闲时,启用其他线程继续使用 CPU,以提高 CPU 的使用率。例如:数据库交互,文件上传下载,网络传输等。

有关python的线程,被喷的一塌糊涂,但是它仍然是我们最简单提升多任务效率,实现并发的基础方式。所以我们就来看看如何实现更好的利用线程。

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

调用这个构造函数时,必需带有关键字参数。参数如下:

group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。

target 是用于run() 方法调用的可调用对象。默认是 None,表示不需要调用任何方法。

name 是线程名称。在默认情况下,会以”Thread-N” 的形式构造唯一名称,其中 N 为一个较小的十
进制数值,或是”Thread-N (target)” 的形式,其中”target” 为 target.__name__,如果指定了 target
参数的话。

args 是用于调用目标函数的参数元组。默认是 ()。

kwargs 是用于调用目标函数的关键字参数字典。默认是 {}。

如果不是 None,daemon 参数将显式地设置该线程是否为守护模式。如果是 None (默认值),
线程将继承当前线程的守护模式属性。
如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器 (Thread.__init__())。
在 3.10 版更改: 使用 target 名称,如果 name 参数被省略的话。
在 3.3 版更改: 加入 daemon 参数。

线程对象有如下方法:

start()
开始线程活动。
它在一个线程里最多只能被调用一次。它安排对象的run() 方法在一个独立的控制进程中调用。
如果同一个线程对象中调用这个方法的次数大于一次,会抛出RuntimeError 。
run()
代表线程活动的方法。你可以在子类型里重载这个方法。标准的run() 方法会对作为
 target 参数传递给该对象构造器的可调用对象(如果存在)发起调用,并附带从
  args 和 kwargs 参数分别获取的位置和关键字参数。

run方法只是单纯的调用目标函数,并不会开启一个新的线程。

join(timeout=None)
等待,直到线程终结。这会阻塞调用这个方法的线程,直到被调用join() 的线程终结 -- 不管
是正常终结还是抛出未处理异常 -- 或者直到发生超时,超时选项是可选的。

当 timeout 参数存在而且不是 None 时,它应该是一个用于指定操作超时的以秒为单位的浮点
数或者分数。因为join() 总是返回 None ,所以你一定要在join() 后调用is_alive() 才
能判断是否发生超时 -- 如果线程仍然存活,则join() 超时。

当 timeout 参数不存在或者是 None ,这个操作会阻塞直到线程终结。一个线程可以被join() 很多次。
如果尝试加入当前线程会导致死锁,join() 会引起RuntimeError 异常。如果尝试join()
一个尚未开始的线程,也会抛出相同的异常。

name
只用于识别的字符串。它没有语义。多个线程可以赋予相同的名称。初始名称由构造函数设置。
getName()
setName()
已被弃用的name 的取值/设值 API;请改为直接以特征属性方式使用它。
3.10 版后已移除.
ident
这个线程的’线程标识符’,如果线程尚未开始则为None 。这是个非零整数。参见get_ident()函数。
当一个线程退出而另外一个线程被创建,线程标识符会被复用。即使线程退出后,仍可得到标识符。
native_id
此线程的线程 ID (TID),由 OS (内核) 分配。这是一个非负整数,或者如果线程还未启动则为
None。请参阅get_native_id() 函数。这个值可被用来在全系统范围内唯一地标识这个特
定线程 (直到线程终结,在那之后该值可能会被 OS 回收再利用)
is_alive()
返回线程是否存活。
当run() 方法刚开始直到run() 方法刚结束,这个方法返回 True 。模块函数enumerate()
返回包含所有存活线程的列表。
daemon
一个表示这个线程是(True)否(False)守护线程的布尔值。一定要在调用start() 前设置
好,不然会抛出RuntimeError 。初始值继承于创建线程;主线程不是守护线程,因此主线
程创建的所有线程默认都是daemon = False。
当没有存活的非守护线程时,整个 Python 程序才会退出。
isDaemon()
setDaemon()
已被弃用的daemon 的取值/设值 API;请改为直接以特征属性方式使用它。
3.10 版后已移除.

有用的模块级别函数:

threading.active_count()
返回当前存活的Thread 对象的数量。返回值与enumerate() 所返回的列表长度一致。
函数 activeCount 是此函数的已弃用别名。

threading.current_thread()
返回当前对应调用者的控制线程的Thread 对象。如果调用者的控制线程不是利用threading 创
建,会返回一个功能受限的虚拟线程对象。函数 currentThread 是此函数的已弃用别名

threading.excepthook(args, /)
处理由Thread.run() 引发的未捕获异常。
args 参数具有以下属性:
• exc_type: 异常类型
• exc_value: 异常值,可以是 None.
• exc_traceback: 异常回溯,可以是 None.
• thread: 引发异常的线程,可以为 None。
如果 exc_type 为SystemExit,则异常会被静默地忽略。在其他情况下,异常将被打印到sys.
stderr。
如果此函数引发了异常,则会调用sys.excepthook() 来处理它。
threading.excepthook() 可以被重载以控制由Thread.run() 引发的未捕获异常的处理方
式。
使用定制钩子存放 exc_value 可能会创建引用循环。它应当在不再需要异常时被显式地清空以打破
引用循环。
如果一个对象正在被销毁,那么使用自定义的钩子储存 thread 可能会将其复活。请在自定义钩子生
效后避免储存 thread,以避免对象的复活。

threading.get_ident()
返回当前线程的“线程标识符”。它是一个非零的整数。它的值没有直接含义,主要是用作 magic
cookie,比如作为含有线程相关数据的字典的索引。线程标识符可能会在线程退出,新线程创建时被复用。

threading.get_native_id()
返回内核分配给当前线程的原生集成线程 ID。这是一个非负整数。它的值可被用来在整个系统中
唯一地标识这个特定线程(直到线程终结,在那之后该值可能会被 OS 回收再利用)。
可用性: Windows, FreeBSD, Linux, macOS, OpenBSD, NetBSD, AIX。
3.8 新版功能.

threading.enumerate()
返回当前所有存活的Thread 对象的列表。该列表包括守护线程以及current_thread() 创建的
空线程。它不包括已终结的和尚未开始的线程。但是,主线程将总是结果的一部分,即使是在已终
结的时候。

threading.main_thread()
返回主Thread 对象。一般情况下,主线程是 Python 解释器开始时创建的线程

threading.settrace(func)
为所有threading 模块开始的线程设置追踪函数。在每个线程的run() 方法被调用前,func 会被
传递给sys.settrace() 。
threading.gettrace()
返回由settrace() 设置的跟踪函数。
3.10 新版功能.

threading.setprofile(func)
为所有threading 模块开始的线程设置性能测试函数。在每个线程的run() 方法被调用前,func
会被传递给sys.setprofile() 。
threading.getprofile()
返回由setprofile() 设置的性能分析函数。

threading.stack_size( [ size ] )
返回创建线程时使用的堆栈大小。可选参数 size 指定之后新建的线程的堆栈大小,而且一定要是 0
(根据平台或者默认配置)或者最小是 32,768(32KiB) 的一个正整数。如果 size 没有指定,默认是 0。
如果不支持改变线程堆栈大小,会抛出RuntimeError 错误。如果指定的堆栈大小不合法,会抛
出ValueError 错误并且不会修改堆栈大小。32KiB 是当前最小的能保证解释器有足够堆栈空间
的堆栈大小。需要注意的是部分平台对于堆栈大小会有特定的限制,例如要求大于 32KiB 的堆栈
大小或者需要根据系统内存页面的整数倍进行分配 - 应当查阅平台文档有关详细信息(4KiB 页面
比较普遍,在没有更具体信息的情况下,建议的方法是使用 4096 的倍数作为堆栈大小)。
适用于: Windows,具有 POSIX 线程的系统。

这个模块同时定义了以下常量:threading.TIMEOUT_MAX
阻塞函数(Lock.acquire(), RLock.acquire(), Condition.wait(), ...)中形参 timeout 允
许的最大值。传入超过这个值的 timeout 会抛出OverflowError 异常。

一顿官方文档的疯狂输出之后,我们来正式进入线程的混乱世界:

为什么要join:

一段简单的代码,为了对比,并没有用线程


import threading
import time

#计算程序耗时装饰器
def timecount(func):
    st= time.time()
    def do(*args):
        func(*args)
        print("{}:程序耗时—{}".format(func.__name__,time.time()-st))
    return do
    
#每次向列表内添加数据
@timecount
def add():
    for i in range(10):
        time.sleep(0.5)
        li.append(i)
        print("我是add:",li)

#每次抛出列表最后一个数据
@timecount
def lpop():
    for i in range(10):
        time.sleep(1)
        li.pop()
        print("我是lpop",li)

@timecount
def main(*args):
    
    for i in args:
        i()
    print("主程序结束")

if __name__ == "__main__":
    
    li = ['abcd']
    
    main(add,lpop)
======================= RESTART: D:\Users\Desktop\test.py ======================
我是add: ['abcd', 0]
我是add: ['abcd', 0, 1]
我是add: ['abcd', 0, 1, 2]
我是add: ['abcd', 0, 1, 2, 3]
我是add: ['abcd', 0, 1, 2, 3, 4]
我是add: ['abcd', 0, 1, 2, 3, 4, 5]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
add:程序耗时—5.065239429473877
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6, 7]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5]
我是lpop ['abcd', 0, 1, 2, 3, 4]
我是lpop ['abcd', 0, 1, 2, 3]
我是lpop ['abcd', 0, 1, 2]
我是lpop ['abcd', 0, 1]
我是lpop ['abcd', 0]
我是lpop ['abcd']
lpop:程序耗时—15.207695722579956
主程序结束
main:程序耗时—15.211196660995483

细心的朋友将会发现这个程序有一个问题,为什么loop的耗时与主程序的耗时相差无几,主程序应该是两个顺序函数执行时间之和。

这是什么原因呢?这就是之前在有关装饰器的文章的说的,装饰器会在文件导入时立刻运行。我们来看一看,我们main函数注释掉,修改装饰器再次运行。

def timecount(func):
    st= time.time()
    print("函数{},开始时间{}".format(func.__name__,st))
    def do(*args):
        #st= time.time()
        func(*args)
        print("{}:程序耗时—{}".format(func.__name__,time.time()-st))
    return do


if __name__ == "__main__":
    
    li = ['abcd']
    
    #main(add,lpop)
    pass
 ======================= RESTART: D:\Users\Desktop\test.py ======================
函数add,开始时间1649303988.278308
函数lpop,开始时间1649303988.281307
函数main,开始时间1649303988.282809
>>>    

没有运行任何程序,但是装饰器已经启动了。修改下装饰器以便于精确的工作

#计算程序耗时装饰器
def timecount(func):
    def do(*args):
        st= time.time()
        print("函数{},开始时间{}".format(func.__name__,st))
        func(*args)
        print("{}:程序耗时—{}".format(func.__name__,time.time()-st))
    return do

再次运行

======================= RESTART: D:\Users\Desktop\test.py ======================
函数main,开始时间1649307255.1195786
函数add,开始时间1649307255.123581
我是add: ['abcd', 0]
我是add: ['abcd', 0, 1]
我是add: ['abcd', 0, 1, 2]
我是add: ['abcd', 0, 1, 2, 3]
我是add: ['abcd', 0, 1, 2, 3, 4]
我是add: ['abcd', 0, 1, 2, 3, 4, 5]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
add:程序耗时—5.104216575622559
函数lpop,开始时间1649307260.2292998
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6, 7]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5]
我是lpop ['abcd', 0, 1, 2, 3, 4]
我是lpop ['abcd', 0, 1, 2, 3]
我是lpop ['abcd', 0, 1, 2]
我是lpop ['abcd', 0, 1]
我是lpop ['abcd', 0]
我是lpop ['abcd']
lpop:程序耗时—10.056710481643677
主程序结束
main:程序耗时—15.170434951782227
>>> 

这次程序耗时恢复了正常,我们用线程改造一下程序:

@timecount
def main(*args):
    for i in args:
        t=threading.Thread(target=i, args=())
        t.start()
        
    print("主程序结束")

if __name__ == "__main__":
    
    li = ['abcd']
    
    main(add,lpop)
    pass
======================= RESTART: D:\Users\Desktop\test.py ======================
函数main,开始时间1649307402.1302774
函数add,开始时间1649307402.1327686主程序结束函数lpop,开始时间1649307402.133268


main:程序耗时—0.008994579315185547
>>> 我是add: ['abcd', 0]
我是lpop 我是add:['abcd', 1] 
['abcd', 1]
我是add: ['abcd', 1, 2]
我是lpop我是add:  ['abcd', 1, 3]['abcd', 1, 3]

我是add: ['abcd', 1, 3, 4]
我是lpop我是add:  ['abcd', 1, 3, 5]['abcd', 1, 3, 5]

我是add: ['abcd', 1, 3, 5, 6]
我是lpop我是add:  ['abcd', 1, 3, 5, 7]['abcd', 1, 3, 5, 7]

我是add: ['abcd', 1, 3, 5, 7, 8]
我是lpop我是add:  ['abcd', 1, 3, 5, 7, 9]['abcd', 1, 3, 5, 7, 9]

add:程序耗时—5.061230182647705
我是lpop ['abcd', 1, 3, 5, 7]
我是lpop ['abcd', 1, 3, 5]
我是lpop ['abcd', 1, 3]
我是lpop ['abcd', 1]
我是lpop ['abcd']
lpop:程序耗时—10.0930335521698

结果比较混乱,但是可以确定的是,我们的主线程先于,其他线程结束了,导致我们无法测试出线程耗时。得到结果,所以这个时候我们需要join,修改main函数

@timecount
def main(*args):
    temp = []
    for i in args:
        t=threading.Thread(target=i, args=())
        temp.append(t)
        
    for th in temp:
        th.start()
        th.join()#阻塞主线程,直到调用join方法的线程结束
        
    print("主程序结束")
======================= RESTART: D:\Users\Desktop\test.py ======================
函数main,开始时间1649307664.1160014
函数add,开始时间1649307664.120005
我是add: ['abcd', 0]
我是add: ['abcd', 0, 1]
我是add: ['abcd', 0, 1, 2]
我是add: ['abcd', 0, 1, 2, 3]
我是add: ['abcd', 0, 1, 2, 3, 4]
我是add: ['abcd', 0, 1, 2, 3, 4, 5]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8]
我是add: ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
add:程序耗时—5.061814308166504
函数lpop,开始时间1649307669.1863232
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6, 7, 8]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6, 7]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5, 6]
我是lpop ['abcd', 0, 1, 2, 3, 4, 5]
我是lpop ['abcd', 0, 1, 2, 3, 4]
我是lpop ['abcd', 0, 1, 2, 3]
我是lpop ['abcd', 0, 1, 2]
我是lpop ['abcd', 0, 1]
我是lpop ['abcd', 0]
我是lpop ['abcd']
lpop:程序耗时—10.074697494506836
主程序结束
main:程序耗时—15.151013612747192

结果很无语,居然与函数单独执行的时间一样,根本没有任何提升。原因是我们的join加入的太早了。修改main函数

@timecount
def main(*args):
    temp = []
    for i in args:
        t=threading.Thread(target=i, args=())
        temp.append(t)
        
    for th in temp:
        th.start()
        
    for th in temp:
        th.join()
        
    print("主程序结束")
======================= RESTART: D:\Users\Desktop\test.py ======================
函数main,开始时间1649307848.275581
函数add,开始时间1649307848.283844函数lpop,开始时间1649307848.283844

我是add: ['abcd', 0]
我是lpop我是add:  ['abcd', 1]['abcd', 1]

我是add: ['abcd', 1, 2]
我是lpop我是add:  ['abcd', 1, 3]['abcd', 1, 3]

我是add: ['abcd', 1, 3, 4]
我是lpop我是add:  ['abcd', 1, 3, 5]['abcd', 1, 3, 5]

我是add: ['abcd', 1, 3, 5, 6]
我是lpop我是add:  ['abcd', 1, 3, 5, 7]['abcd', 1, 3, 5, 7]

我是add: ['abcd', 1, 3, 5, 7, 8]
我是lpop我是add:  ['abcd', 1, 3, 5, 7, 9]['abcd', 1, 3, 5, 7, 9]

add:程序耗时—5.0876429080963135
我是lpop ['abcd', 1, 3, 5, 7]
我是lpop ['abcd', 1, 3, 5]
我是lpop ['abcd', 1, 3]
我是lpop ['abcd', 1]
我是lpop ['abcd']
lpop:程序耗时—10.126869678497314
主程序结束
main:程序耗时—10.143129110336304
>>> 

这次时间仅用了10秒多,也看到了并发,这也是为什么我们要在所有线程start之后,开始调用join,因为如果在同一个线程中调用,主程序在第一次join的时候阻塞了,执行不到下一个线程,只有等待当前线程结束,才能开始执行下一个线程,这与单独调用函数,基本是一个效果。

当然,如果我们并不需要线程操作后的结果,或者主线程中没有线程运行所必须的,上下文变量,我们完全可以不用join,让线程撒欢的跑,但是那样我们就不知道线程何时结束,无法实现精确的控制。

看下一个例子:
我要向文件中写入数据,但是数据较多,我打算用多线程操作。

#向文件中写入字符串
def twrite(s,fileobj):
    for i in range(len(s)):
        time.sleep(0.5)
        fileobj.write(s[i])
        print(s[i])
        
#运行主函数开启线程           
def main(obj,*args,n=2):
        temp = []
        for i in range(n):
            t = threading.Thread(target=obj,args=args)
            temp.append(t)
        for th in temp:
            th.start()
        
    


if __name__ == "__main__":
    
    with open(r"d:\Users\Desktop\aaa.txt","w") as w:
    
        main(twrite,'abcd',w)

程序很简单,我们并没有用join,结果是:

======================= RESTART: D:\Users\Desktop\test.py ======================
>>> Exception in thread Thread-1Exception in thread :
Thread-2Traceback (most recent call last):
:
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python39\lib\threading.py", line 973, in _bootstrap_inner
Traceback (most recent call last):
      File "C:\Users\Administrator\AppData\Local\Programs\Python\Python39\lib\threading.py", line 973, in _bootstrap_inner
self.run()
      File "C:\Users\Administrator\AppData\Local\Programs\Python\Python39\lib\threading.py", line 910, in run
self.run()    
self._target(*self._args, **self._kwargs)  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python39\lib\threading.py", line 910, in run

      File "D:\Users\Desktop\test.py", line 18, in twrite
self._target(*self._args, **self._kwargs)    
fileobj.write(s[i])  File "D:\Users\Desktop\test.py", line 18, in twrite

    ValueErrorfileobj.write(s[i]): 
I/O operation on closed file.ValueError
: I/O operation on closed file.

由于没有阻塞主线程,导致文件对象先于线程关闭了,线程想要写入的时候,无法应用文件对象。
至于怎么修改,我相信你会的。

线程运行一个类


import threading
import time

class CountdownTask:
    def __init__(self):
        self._running = True
        
    def terminate(self):
        self._running = False
        
    def __call__(self, n):
        while self._running and n > 0:
            print('T-minus', n)
            n -= 1
            time.sleep(5)

            
c = CountdownTask()
t = threading.Thread(target=c, args=(10,))
t.start()
c.terminate()#传入信号,停止线程
t.join()
    

通过继承实现


import threading
import time

class CountdownThread(threading.Thread):
    
    def __init__(self, n):
        super().__init__()#引用父类构造方法        
        self.n = n        
    #重写父类run方法
    def run(self):
        while self.n > 0:
            print('T-minus', self.n)
            self.n -= 1
            time.sleep(2)
                
c = CountdownThread(5)
c.start()

尽管这样也可以工作,但这使得你的代码依赖于 threading 库,所以你的这些代码只能在线程上下文中使用。

线程可以帮助让我们的程序更有效率,但是由于线程会共享当前的程序上下文,这就会造成资源的抢占,导致不可预料的结果。所以就有了同步的问题。

锁对象

原始锁是一个在锁定时不属于特定线程的同步基元组件。在 Python 中,它是能用的最低级的同步基元组件,由_thread 扩展模块直接实现。
原始锁处于” 锁定” 或者” 非锁定” 两种状态之一。它被创建时为非锁定状态。它有两个基本方法, acquire() 和release()。当状态为非锁定时,acquire() 将状态改为锁定并立即返回。当状态是 锁定时,acquire() 将阻塞至其他线程调用release() 将其改为非锁定状态,然后acquire() 调用 重置其为锁定状态并返回。release() 只在锁定状态下调用;它将状态改为非锁定并立即返回。如果尝 试释放一个非锁定的锁,则会引发RuntimeError 异常。
锁同样支持上下文管理协议。 当多个线程在acquire() 等待状态转变为未锁定被阻塞,然后release() 重置状态为未锁定时,只有 一个线程能继续执行;至于哪个等待线程继续执行没有定义,并且会根据实现而不同。
所有方法的执行都是原子性的。

如官方所说,锁对象是同步的基元组件,所以线程里面的大部分同步原语都是用锁对象来实现的。锁的目的就是在竞速抢占资源的时候,如果这个资源对象是不可变的,那么没有问题,如果这个资源是可变的,为了保持资源的稳定性,持有锁即可访问资源,修改完成即释放资源,由其他线程处理,这样保证了共享资源的稳定性。

不用锁的情况:



import threading
import time

def set1(data):
    while True:
        data['foo'] = 1
        if data['foo'] != 1:
            raise ValueError("set1资料不一致:{}".format(str(data)))


    
def set2(data):
    while True:
        data['foo'] = 2
        if data['foo'] != 2:
            raise ValueError("set2资料不一致:{}".format(str(data)))



data = {}

t1 = threading.Thread(target=set1, args=(data,))

t2 = threading.Thread(target=set2, args=(data,))

t1.start()
t2.start()

======================= RESTART: D:\Users\Desktop\test.py ======================
>>> Exception in thread Thread-1:
Traceback (most recent call last):
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python39\lib\threading.py", line 973, in _bootstrap_inner
    self.run()
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python39\lib\threading.py", line 910, in run
    self._target(*self._args, **self._kwargs)
  File "D:\Users\Desktop\test.py", line 9, in set1
    raise ValueError("set1资料不一致:{}".format(str(data)))
ValueError: set1资料不一致:{'foo': 2}


由于资料被改变,导致发生错误,无法预测两个线程哪一个造成的,多运行几次哪一个都有可能。
下面加锁:


import threading
import time

def set1(data):
    while True:
        lock.acquire()
        try:
            data['foo'] = 1
            print(data)
            if data['foo'] != 1:
                raise ValueError("set1资料不一致:{}".format(str(data)))
        finally:
            lock.release()
    
def set2(data):
    while True:
        lock.acquire()
        try:
            data['foo'] = 2
            print(data)
            if data['foo'] != 2:
                raise ValueError("set2资料不一致:{}".format(str(data)))
        finally:
            lock.release()
            
data = {}
lock = threading.Lock()

t1 = threading.Thread(target=set1, args=(data,))

t2 = threading.Thread(target=set2, args=(data,))

t1.start()
t2.start()

也可用上下文管理协议,简化代码:

def set1(data):
    while True:
        with lock:
            data['foo'] = 1
            print(data)
            if data['foo'] != 1:
                raise ValueError("set1资料不一致:{}".format(str(data)))

def set2(data):
    while True:
        with lock:
            data['foo'] = 2
            print(data)
            if data['foo'] != 2:
                raise ValueError("set2资料不一致:{}".format(str(data)))

死锁,由于线程无法取得锁的时候会发生阻塞,不正确的逻辑可能会导致效率低下,甚至死锁,如果有很多资源,交叉使用,就有可能造成死锁现象:

import threading
import time

class Resce:
    def __init__(self,name,resource):
        self.name = name
        self.resource = resource
        self.lock = threading.Lock()

    def action(self):
        with self.lock:
            self.resource += 1
            return self.resource

    def cooper(self,other_res):
        with self.lock:
            other_res.action()
            print("{}整合{}的资源".format(self.name,other_res.name))


def cooperate(a, b):
    for i in range(10):
        a.cooper(b)


res1 = Resce('resource 1', 10)
res2  = Resce("resource 2", 20)

t1 = threading.Thread(target=cooperate, args=(res1,res2) )
t2 = threading.Thread(target=cooperate, args=(res2,res1) )

t1.start()
t2.start()

由于两个进程互相争夺资源,会发生你不解锁t1我就不解锁t2的情况,所以这个时候你可以通过设置锁的timeout参数来设置最多阻塞时间,但是这并不是解决办法,因为你无法预测超时后,共享的可变资源的结果。所以我们还是尽量保证不要让死锁的情况发生。

递归锁对象

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

看着这个定义头都大了,如果你需要用上递归的锁,还是看看别的方法吧,这样的代码写出来,真是错了都不知道怎么错的。

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

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

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

条件对象

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

class threading.Condition(lock=None)
实现条件变量对象的类。一个条件变量对象允许一个或多个线程在被其它线程所通知之前进行等
待。如果给出了非 None 的 lock 参数,则它必须为Lock 或者RLock 对象,并且它将被用作底层锁。否
则,将会创建新的RLock 对象,并将其用作底层锁。
在 3.3 版更改: 从工厂函数变为类。

acquire(*args)
请求底层锁。此方法调用底层锁的相应方法,返回值是底层锁相应方法的返回值。

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

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

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

notify(n=1)
默认唤醒一个等待这个条件的线程。如果调用线程在没有获得锁的情况下调用这个方法,会
引发RuntimeError 异常。这个方法唤醒最多 n 个正在等待这个条件变量的线程;如果没有线程在等待,这是一个空操作。
当前实现中,如果至少有 n 个线程正在等待,准确唤醒 n 个线程。但是依赖这个行为并不安
全。未来,优化的实现有时会唤醒超过 n 个线程。注意:被唤醒的线程并没有真正恢复到它调用的wait() ,直到它可以重新获得锁。因为notify() 不释放锁,其调用者才应该这样做。

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

来看一段示例代码:

import threading
import time


cond = threading.Condition()

carts = []

def product():
    for i in range(10):
         with cond:
             if carts:#如果carts列表不为空,就等待
                 cond.wait()
             else:#如果是空列表就添加数据,并通知消费者
                 carts.append(i)
                 time.sleep(1)
                 cond.notify()
                 print("生产数据",i)
            
def customer():
    for i in range(10):
        with cond:
            if not carts:#如果列表为空就等待
                cond.wait()
            else:#否则就抛出数据
                p=carts.pop()
                time.sleep(0.5)
                cond.notify()
                print("消费数据",p)
            



threading.Thread(target= product,args=()).start()

threading.Thread(target= customer,args=()).start()
======================= RESTART: D:\Users\Desktop\test.py ======================
>>> 生产数据 0
消费数据 0
生产数据 2
消费数据 2
生产数据 4
消费数据 4
生产数据 6
消费数据 6
生产数据 8
消费数据 8

意思大概就是这么个意思,但是为什么输出的数据没有单数,这个你就要问问列表拉,测试一下在循环中删除列表的值,你就会知道答案了。所以列表不是线程安全的,原谅我偷懒,但是写那么多代码也挺累的,大家明白大概意思就行,就是我做好了饭,通知你去吃,你吃完了,又等我做。如此往复。

更简单直观迅速的数据交换方式


import threading
import time
import queue

cond = threading.Condition()
carts = queue.Queue(1)

def product():
    for i in range(10):
           
        carts.put(i)
        time.sleep(0.5)
                 
        print("生产数据",i)
            

def customer():
    for i in range(10):
        time.sleep(0.5)
        p=carts.get()  
        print("消费数据",p)
            
threading.Thread(target= product,args=()).start()

threading.Thread(target= customer,args=()).start()
======================= RESTART: D:\Users\Desktop\test.py ======================
>>> 消费数据生产数据  00

消费数据生产数据  11

消费数据生产数据  22

消费数据生产数据  33

消费数据 生产数据4 
4
消费数据生产数据  55

消费数据生产数据  66

消费数据生产数据  77

消费数据生产数据  88

消费数据生产数据  99

信号量对象

这是计算机科学史上最古老的同步原语之一,早期的荷兰科学家 Edsger W. Dijkstra 发明了它。(他使用名称 P() 和 V() 而不是acquire() 和release() )。
一个信号量管理一个内部计数器,该计数器因acquire() 方法的调用而递减,因release() 方法的调
用而递增。计数器的值永远不会小于零;当acquire() 方法发现计数器为零时,将会阻塞,直到其它线
程调用release() 方法。

class threading.Semaphore(value=1)
该类实现信号量对象。信号量对象管理一个原子性的计数器,代表release() 方法的调用次数减
去acquire() 的调用次数再加上一个初始值。如果需要,acquire() 方法将会阻塞直到可以返
回而不会使得计数器变成负数。在没有显式给出 value 的值时,默认为 1。
可选参数 value 赋予内部计数器初始值,默认值为 1 。如果 value 被赋予小于 0 的值,将会引
发ValueError 异常。
在 3.3 版更改: 从工厂函数变为类。

acquire(blocking=True, timeout=None)
获取一个信号量。
在不带参数的情况下调用时:
• 如果在进入时内部计数器的值大于零,则将其减一并立即返回 True。
• 如果在进入时内部计数器的值为零,则将会阻塞直到被对release() 的调用唤醒。一旦
被唤醒(并且计数器的值大于 0),则将计数器减 1 并返回 True。每次对release() 的
调用将只唤醒一个线程。线程被唤醒的次序是不可确定的。
当发起调用时将 blocking 设为假值,则不进行阻塞。如果一个无参数调用将要阻塞,则立即返
回 False;在其他情况下,执行与无参数调用时一样的操作,然后返回 True。
当发起调用时如果 timeout 不为 None,则它将阻塞最多 timeout 秒。请求在此时段时未能成功
完成获取则将返回 False。在其他情况下返回 True。
在 3.2 版更改: 新的 timeout 形参。

release(n=1)
释放一个信号量,将内部计数器的值增加 n。当进入时值为零且有其他线程正在等待它再次变
为大于零时,则唤醒那 n 个线程。
在 3.9 版更改: 增加了 n 形参以一次性释放多个等待线程。

class threading.BoundedSemaphore(value=1)
该类实现有界信号量。有界信号量通过检查以确保它当前的值不会超过初始值。如果超过了初始
值,将会引发ValueError 异常。在大多情况下,信号量用于保护数量有限的资源。如果信号量被
释放的次数过多,则表明出现了错误。没有指定时,value 的值默认为 1。
在 3.3 版更改: 从工厂函数变为类。

信号量通常用于保护数量有限的资源,例如数据库服务器。在资源数量固定的任何情况下,都应该使用
有界信号量。在生成任何工作线程前,应该在主线程中初始化信号量。

import  threading 
import time

# Worker thread
def worker(n, sema):
    # 等待信号
    sema.acquire()
    
    print('Working', n)
    
sema = threading.Semaphore(0)
nworkers = 10

for n in range(nworkers):
    t = threading.Thread(target=worker, args=(n, sema,))
    t.start()

for i in range(10):
    time.sleep(2)
    sema.release()
    print("释放一个信号量\n")
======================= RESTART: D:\Users\Desktop\test.py ======================
Working释放一个信号量
 
0
释放一个信号量
Working
 1
释放一个信号量
Working
 2
释放一个信号量
Working
 3
释放一个信号量
Working
 4
释放一个信号量
Working
 5
释放一个信号量
Working
 6
释放一个信号量
Working
 7
释放一个信号量
Working
 8
Working释放一个信号量
 
9
>>> 

编写涉及到大量的线程间同步问题的代码会让你痛不欲生。比较合适的方式是使用队列来进行线程间通信。

事件对象

这是线程之间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号。
一个事件对象管理一个内部标识,调用set() 方法可将其设置为 true ,调用clear() 方法可将其设置
为 false ,调用wait() 方法将进入阻塞直到标识为 true 。

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

is_set()
当且仅当内部标识为 true 时返回 True 。
isSet 方法是此方法的已弃用别名。

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

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

wait(timeout=None)
阻塞线程直到内部变量为 true 。如果调用时内部标识为 true,将立即返回。否则将阻塞线程,
直到调用set() 方法将标识设置为 true 或者发生可选的超时。
from threading import Thread, Event
import time

def countdown(n, started_evt):
	print('countdown starting')
	started_evt.set()#标志事件完成
	while n > 0:
		print('T-minus', n)
		n -= 1
		time.sleep(5)
#建立事件对象
started_evt = Event()
print('开始执行countdown')
t = Thread(target=countdown, args=(10,started_evt))
t.start()
# 等待线程开始
started_evt.wait()
print('countdown is running')

========================= RESTART: D:\Users\Desktop\test.py ========================
开始执行countdown
countdown starting
T-minuscountdown is running 
10
>>> 
T-minus 9
T-minus 8
T-minus 7
T-minus 6
T-minus 5
T-minus 4
T-minus 3
T-minus 2
T-minus 1

event 对象最好单次使用,就是说,你创建一个 event 对象,让某个线程等待这个对象,一旦这个对象被设置为真,你就应该丢弃它。尽管可以通过 clear() 方法来重置 event 对象,但是很难确保安全地清理 event 对象并对它重新赋值。很可能会发生错过事件、死锁或者其他问题(特别是,你无法保证重置 event 对象的代码会在线程再次等待这个 event对象之前执行)。如果一个线程需要不停地重复使用 event 对象,你最好使用 Condition对象来代替。

event对象的一个重要特点是当它被设置为真时会唤醒所有等待它的线程。如果你只想唤醒单个线程,最好是使用信号量或者 Condition 对象来替代。

定时器对象

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

class threading.Timer(interval, function, args=None, kwargs=None)
创建一个定时器,在经过 interval 秒的间隔事件后,将会用参数 args 和关键字参数 kwargs 调用
function。如果 args 为 None(默认值),则会使用一个空列表。如果 kwargs 为 None(默认值),则
会使用一个空字典。
在 3.3 版更改: 从工厂函数变为类。
cancel()
停止定时器并取消执行计时器将要执行的操作。仅当计时器仍处于等待状态时有效。

import threading
import time
import queue


def foo(name):
    print("你好",name)

for n in range(3):
    print(f"间隔{n}秒")
    t = threading.Timer(n,foo,args=("李白",))
    t.start()
======================= RESTART: D:\Users\Desktop\test.py ======================
间隔0秒
你好间隔1秒 
李白间隔2>>> 你好 李白
你好 李白

栅栏对象

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

b = threading.Barrier(2, timeout=5)
def server():
	start_server()
	b.wait()
	while True:
		connection = accept_connection()
		process_server_connection(connection)
		
def client():
	b.wait()
	while True:
		connection = make_connection()
		process_client_connection(connection)
class threading.Barrier(parties, action=None, timeout=None)
创建一个需要 parties 个线程的栅栏对象。如果提供了可调用的 action 参数,它会在所有线程被释放时在其
中一个线程中自动调用。timeout 是默认的超时时间,如果没有在wait() 方法中指定超时时间的话。

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

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

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

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

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

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

exception threading.BrokenBarrierError
异常类,是RuntimeError 异常的子类,在Barrier 对象重置时仍有线程阻塞时和对象进入破损
态时被引发。
import  threading 
import time


bar = threading.Barrier(4)

def df(n):
    bar.wait()
    print(f'东风{n}号发射')
    

for i in range(1,4):

    t  =threading.Thread(target= df,args=(i,)).start()

n = 10
while n:
    print(f"导弹发射倒计时{n}")
    n -= 1
    time.sleep(1)

bar.wait()
======================= RESTART: D:\Users\Desktop\test.py ======================
导弹发射倒计时10
导弹发射倒计时9
导弹发射倒计时8
导弹发射倒计时7
导弹发射倒计时6
导弹发射倒计时5
导弹发射倒计时4
导弹发射倒计时3
导弹发射倒计时2
导弹发射倒计时1
东风1号发射东风3号发射东风2号发射

从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put() 和 get() 操作来向队列中添加或者删除元素。
Queue 对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。当使用队列时,协调生产者和消费者的关闭问题可能会有一些麻烦。一个通用的解决方法是在队列中放置一个特殊的标识,当消费者读到这个值的时候,终止执行。

from queue import Queue
from threading import Thread

def producter(out):
    data = 10
    while data: 
        out.put(data)
        print("放入数据",data)
        data -= 1
    #数据都放入后,放入结束标志
    out.put(_d)

def consumer(in_q):
    while True:
        data = in_q.get()
        #检查标志,如果是,则再把标志放回,供其他线程获取,并结束进程
        if data is _d:
            in_q.put(_d)
            print("没有数据了")
            break
        print("取出数据",data)
        
#设置一个特别的对象,作为结束标志
_d = object()

q = Queue()

t1 = Thread(target=producter, args=(q,))
t2 = Thread(target=consumer, args=(q,))

t1.start()
t2.start()
=================== RESTART: /home/fujp/Documents/test.py ===================
>>> 放入数据取出数据  1010

放入数据取出数据  99

放入数据取出数据  88

放入数据取出数据  77

放入数据取出数据  66

放入数据取出数据  55

放入数据取出数据  44

放入数据取出数据  33

放入数据取出数据  22

放入数据取出数据  11

没有数据了


消费者在读到这个特殊值之后立即又把它放回到队列中,将之传递下去。这样,所有监听这个队列的消费者线程就可以全部关闭了。尽管队列是最常见的线程间通信机制,但是仍然可以自己通过创建自己的数据结构并添加所需的锁和同步机制来实现线程间通信。

使用队列来进行线程间通信是一个单向、不确定的过程。通常情况下,你没有办法知道接收数据的线程是什么时候接收到的数据并开始工作的。不过队列对象提供一些基本完成的特性,比如下边这个例子中的 task_done() 和 join() :

Queue.task_done()
表示前面排队的任务已经被完成。被队列的消费者线程使用。每个get() 被用于获取一个任务,后续调用task_done() 告诉队列,该任务的处理已经完成。如果join() 当前正在阻塞,在所有条目都被处理后,将解除阻塞 (意味着每个put() 进队列的条目的task_done() 都被收到)。如果被调用的次数多于放入队列中的项目数量,将引发ValueError 异常。

Queue.join()
阻塞至队列中所有的元素都被接收和处理完毕。当条目添加到队列的时候,未完成任务的计数就会增加。每当消费者线程调用task_done() 表示这个条目已经被回收,该条目所有工作已经完成,未完成计数就会减少。当未完成计数降到零的时候,join() 阻塞被解除。

from queue import Queue
from threading import Thread

def producter(out):
    data = 10
    while data: 
        out.put(data)
        print("放入数据",data)
        data -= 1
  
def consumer(in_q):
    while True:
        data = in_q.get()
        print("取出数据",data)
        in_q.task_done()
        
q = Queue()

t1 = Thread(target=producter, args=(q,))
t2 = Thread(target=consumer, args=(q,))

t1.start()
t2.start()

q.join()

print("程序执行完成")
=================== RESTART: /home/fujp/Documents/test.py ===================
放入数据取出数据  1010

放入数据取出数据  99

放入数据取出数据  88

放入数据取出数据  77

放入数据取出数据  66

放入数据取出数据  55

放入数据取出数据  44

放入数据取出数据  33

放入数据取出数据  22

放入数据取出数据  11

程序执行完成

如果一个线程需要在一个“消费者”线程处理完特定的数据项时立即得到通知,你可以把要发送的数据和一个 Event 放到一起使用,这样“生产者”就可以通过这个Event 对象来监测处理的过程了。示例如下:

from queue import Queue
from threading import Thread, Event

def producter(out):
    data = 10
    while data:
        evt = Event()
        out.put((data,evt))
        print("放入数据,和事件对象",data)
        #等待消费者处理完成
        data -= 1
        evt.wait()
  
def consumer(in_q):
    while True:
        data, evt = in_q.get()
        print("取出数据",data)
        #处理完成,通知生产者
        evt.set()
        
q = Queue()
t1 = Thread(target=producter, args=(q,))
t2 = Thread(target=consumer, args=(q,))

t1.start()
t2.start()

基于简单队列编写多线程程序在多数情况下是一个比较明智的选择。从线程安全队列的底层实现来看,你无需在你的代码中使用锁和其他底层的同步机制,这些只会把你的程序弄得乱七八糟。此外,使用队列这种基于消息的通信机制可以被扩展到更大的应用范畴,比如,你可以把你的程序放入多个进程甚至是分布式系统而无需改变底层的队列结构。使用线程队列有一个要注意的问题是,向队列中添加数据项时并不会复制此数据项,线程间通信实际上是在线程间传递对象引用。如果你担心对象的共享状态,那你最好只传递不可修改的数据结构(如:整型、字符串或者元组)或者一个对象的深拷贝。

Queue 对象提供一些在当前上下文很有用的附加特性。比如在创建 Queue 对象时提供可选的 size 参数来限制可以添加到队列中的元素数量。对于“生产者”与“消费者”速度有差异的情况,为队列中的元素数量添加上限是有意义的。比如,一个“生产者”产生项目的速度比“消费者”“消费”的速度快,那么使用固定大小的队列就可以在队列已满的时候阻塞队列,以免未预期的连锁效应扩散整个程序造成死锁或者程序运行失常。在通信的线程之间进行“流量控制”是一个看起来容易实现起来困难的问题。如果你发现自己曾经试图通过摆弄队列大小来解决一个问题,这也许就标志着你的程序可能存在脆弱设计或者固有的可伸缩问题。get() 和 put() 方法都支持非阻塞方式和设定超时,例如:

from queue import Queue
from threading import Thread, Event
import time

def producter(out):
    data = 10
    while data:
        time.sleep(0.5)
        try:
            out.put(data, block=False)
            print("放入数据,和事件对象",data)
            data -= 1
        except :
            print("目前队列满员")
            consumer(q)
    

def consumer(in_q):
    while True:
        try:
            data = in_q.get(block=False)
            print("取出数据",data)
        except :
            print("目前队列为空")
            producter(q)
        
                
q = Queue(10)

t1 = Thread(target=producter, args=(q,))
t2 = Thread(target=consumer, args=(q,))

t1.start()
t2.start()
=================== RESTART: /home/fujp/Documents/test.py ===================
放入数据取出数据  1010

放入数据取出数据  99

放入数据取出数据  88

放入数据取出数据  77

放入数据取出数据  66

放入数据取出数据  55

放入数据取出数据  44

放入数据取出数据  33

放入数据取出数据  22

放入数据取出数据  11

目前队列为空

这些操作都可以用来避免当执行某些特定队列操作时发生无限阻塞的情况,比如,一个非阻塞的 put() 方法和一个固定大小的队列一起使用,这样当队列已满时就可以执行不同的代码。比如输出一条日志信息并丢弃。

最后,有 q.qsize() ,q.full() ,q.empty() 等实用方法可以获取一个队列的当前
大小和状态。但要注意,这些方法都不是线程安全的。可能你对一个队列使用 empty()判断出这个队列为空,但同时另外一个线程可能已经向这个队列中插入一个数据项。所以,你最好不要在你的代码中使用这些方法。

现代操作系统可以很轻松的创建几千个线程的线程池。甚至,同时几千个线程等待工作并不会对其他代码产生性能影响。当然了,如果所有线程同时被唤醒并立即在 CPU 上执行,那就不同了——特别是有了全局解释器锁 GIL。通常,你应该只在 I/O 处理相关代码中使用线程池。创建大的线程池的一个可能需要关注的问题是内存的使用。例如,如果你在 OS X系统上面创建 2000 个线程,系统显示 Python 进程使用了超过 9GB 的虚拟内存。不过,这个计算通常是有误差的。当创建一个线程时,操作系统会预留一个虚拟内存区域来放置线程的执行栈(通常是 8MB 大小)。但是这个内存只有一小片段被实际映射到真实内存中。因此,Python 进程使用到的真实内存其实很小(比如,对于 2000 个线程来讲,只使用到了 70MB 的真实内存,而不是 9GB)。如果你担心虚拟内存大小,可以使用 threading.stack_size() 函数来降低它。

import threading
threading.stack_size(65536)

如果你加上这条语句并再次运行前面的创建 2000 个线程试验,你会发现 Python进程只使用到了大概 210MB 的虚拟内存,而真实内存使用量没有变。注意线程栈大小必须至少为 32768 字节,通常是系统内存页大小(4096、8192 等)的整数倍。

尽管 Python 完全支持多线程编程,但是解释器的 C 语言实现部分在完全并行执行时并不是线程安全的。实际上,解释器被一个全局解释器锁保护着,它确保任何时候都只有一个 Python 线程执行。GIL 最大的问题就是 Python 的多线程程序并不能利用多核 CPU 的优势(比如一个使用了多个线程的计算密集型程序只会在一个单 CPU 上面运行)。在讨论普通的 GIL 之前,有一点要强调的是 GIL 只会影响到那些严重依赖 CPU的程序(比如计算型的)。如果你的程序大部分只会涉及到 I/O,比如网络交互,那么使用多线程就很合适,因为它们大部分时间都在等待。实际上,你完全可以放心的创建几千个 Python 线程,现代操作系统运行这么多线程没有任何压力,没啥可担心的。而对于依赖 CPU 的程序,你需要弄清楚执行的计算的特点。例如,优化底层算法要比使用多线程运行快得多。类似的,由于 Python 是解释执行的,如果你将那些性能瓶颈代码移到一个 C 语言扩展模块中,速度也会提升的很快。如果你要操作数组,那
么使用 NumPy 这样的扩展会非常的高效。最后,你还可以考虑下其他可选实现方案,比如 PyPy,它通过一个 JIT 编译器来优化执行效率(不过在写这本书的时候它还不能支持 Python 3)。还有一点要注意的是,线程不是专门用来优化性能的。一个 CPU 依赖型程序可能会使用线程来管理一个图形用户界面、一个网络连接或其他服务。这时候,GIL 会产生一些问题,因为如果一个线程长期持有 GIL 的话会导致其他非 CPU 型线程一直等待。事实上,一个写的不好的 C 语言扩展会导致这个问题更加严重,尽管代码的计算部分会比之前运行的更快些。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~白+黑

真乃人中龙凤,必成大器,

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值