一文速通 Python 并行计算:04 Python 多线程编程-多线程同步(下)—基于条件变量、事件和屏障

摘要:

本文介绍了 Python 多线程同步的三种机制:条件变量(Condition)、事件(Event)和屏障(Barrier),条件变量指的是线程等待特定条件满足后执行,适用于生产者-消费者模型;Event 指的是线程通过事件标志进行同步,适用于线程间简单通信;Barrier 指的是多个线程需同步到同一阶段时使用,适用于并行任务的分阶段执行。

关于我们更多介绍可以查看云文档:Freak 嵌入式工作室云文档,或者访问我们的 wiki:****https://github.com/leezisheng/Doc/wik

原文链接:

FreakStudio的博客

往期推荐:

学嵌入式的你,还不会面向对象??!

全网最适合入门的面向对象编程教程:00 面向对象设计方法导论

全网最适合入门的面向对象编程教程:01 面向对象编程的基本概念

全网最适合入门的面向对象编程教程:02 类和对象的 Python 实现-使用 Python 创建类

全网最适合入门的面向对象编程教程:03 类和对象的 Python 实现-为自定义类添加属性

全网最适合入门的面向对象编程教程:04 类和对象的Python实现-为自定义类添加方法

全网最适合入门的面向对象编程教程:05 类和对象的Python实现-PyCharm代码标签

全网最适合入门的面向对象编程教程:06 类和对象的Python实现-自定义类的数据封装

全网最适合入门的面向对象编程教程:07 类和对象的Python实现-类型注解

全网最适合入门的面向对象编程教程:08 类和对象的Python实现-@property装饰器

全网最适合入门的面向对象编程教程:09 类和对象的Python实现-类之间的关系

全网最适合入门的面向对象编程教程:10 类和对象的Python实现-类的继承和里氏替换原则

全网最适合入门的面向对象编程教程:11 类和对象的Python实现-子类调用父类方法

全网最适合入门的面向对象编程教程:12 类和对象的Python实现-Python使用logging模块输出程序运行日志

全网最适合入门的面向对象编程教程:13 类和对象的Python实现-可视化阅读代码神器Sourcetrail的安装使用

全网最适合入门的面向对象编程教程:全网最适合入门的面向对象编程教程:14 类和对象的Python实现-类的静态方法和类方法

全网最适合入门的面向对象编程教程:15 类和对象的 Python 实现-__slots__魔法方法

全网最适合入门的面向对象编程教程:16 类和对象的Python实现-多态、方法重写与开闭原则

全网最适合入门的面向对象编程教程:17 类和对象的Python实现-鸭子类型与“file-like object“

全网最适合入门的面向对象编程教程:18 类和对象的Python实现-多重继承与PyQtGraph串口数据绘制曲线图

全网最适合入门的面向对象编程教程:19 类和对象的 Python 实现-使用 PyCharm 自动生成文件注释和函数注释

全网最适合入门的面向对象编程教程:20 类和对象的Python实现-组合关系的实现与CSV文件保存

全网最适合入门的面向对象编程教程:21 类和对象的Python实现-多文件的组织:模块module和包package

全网最适合入门的面向对象编程教程:22 类和对象的Python实现-异常和语法错误

全网最适合入门的面向对象编程教程:23 类和对象的Python实现-抛出异常

全网最适合入门的面向对象编程教程:24 类和对象的Python实现-异常的捕获与处理

全网最适合入门的面向对象编程教程:25 类和对象的Python实现-Python判断输入数据类型

全网最适合入门的面向对象编程教程:26 类和对象的Python实现-上下文管理器和with语句

全网最适合入门的面向对象编程教程:27 类和对象的Python实现-Python中异常层级与自定义异常类的实现

全网最适合入门的面向对象编程教程:28 类和对象的Python实现-Python编程原则、哲学和规范大汇总

全网最适合入门的面向对象编程教程:29 类和对象的Python实现-断言与防御性编程和help函数的使用

全网最适合入门的面向对象编程教程:30 Python的内置数据类型-object根类

全网最适合入门的面向对象编程教程:31 Python的内置数据类型-对象Object和类型Type

全网最适合入门的面向对象编程教程:32 Python的内置数据类型-类Class和实例Instance

全网最适合入门的面向对象编程教程:33 Python的内置数据类型-对象Object和类型Type的关系

全网最适合入门的面向对象编程教程:34 Python的内置数据类型-Python常用复合数据类型:元组和命名元组

全网最适合入门的面向对象编程教程:35 Python的内置数据类型-文档字符串和__doc__属性

全网最适合入门的面向对象编程教程:36 Python的内置数据类型-字典

全网最适合入门的面向对象编程教程:37 Python常用复合数据类型-列表和列表推导式

全网最适合入门的面向对象编程教程:38 Python常用复合数据类型-使用列表实现堆栈、队列和双端队列

全网最适合入门的面向对象编程教程:39 Python常用复合数据类型-集合

全网最适合入门的面向对象编程教程:40 Python常用复合数据类型-枚举和enum模块的使用

全网最适合入门的面向对象编程教程:41 Python常用复合数据类型-队列(FIFO、LIFO、优先级队列、双端队列和环形队列)

全网最适合入门的面向对象编程教程:42 Python常用复合数据类型-collections容器数据类型

全网最适合入门的面向对象编程教程:43 Python常用复合数据类型-扩展内置数据类型

全网最适合入门的面向对象编程教程:44 Python内置函数与魔法方法-重写内置类型的魔法方法

全网最适合入门的面向对象编程教程:45 Python实现常见数据结构-链表、树、哈希表、图和堆

全网最适合入门的面向对象编程教程:46 Python函数方法与接口-函数与事件驱动框架

全网最适合入门的面向对象编程教程:47 Python函数方法与接口-回调函数Callback

全网最适合入门的面向对象编程教程:48 Python函数方法与接口-位置参数、默认参数、可变参数和关键字参数

全网最适合入门的面向对象编程教程:49 Python函数方法与接口-函数与方法的区别和lamda匿名函数

全网最适合入门的面向对象编程教程:50 Python函数方法与接口-接口和抽象基类

全网最适合入门的面向对象编程教程:51 Python函数方法与接口-使用Zope实现接口

全网最适合入门的面向对象编程教程:52 Python函数方法与接口-Protocol协议与接口

全网最适合入门的面向对象编程教程:53 Python字符串与序列化-字符串与字符编码

全网最适合入门的面向对象编程教程:54 Python字符串与序列化-字符串格式化与format方法

全网最适合入门的面向对象编程教程:55 Python字符串与序列化-字节序列类型和可变字节字符串

全网最适合入门的面向对象编程教程:56 Python字符串与序列化-正则表达式和re模块应用

全网最适合入门的面向对象编程教程:57 Python字符串与序列化-序列化与反序列化

全网最适合入门的面向对象编程教程:58 Python字符串与序列化-序列化Web对象的定义与实现

全网最适合入门的面向对象编程教程:59 Python并行与并发-并行与并发和线程与进程

一文速通Python并行计算:00 并行计算的基本概念

一文速通Python并行计算:01 Python多线程编程-基本概念、切换流程、GIL锁机制和生产者与消费者模型

一文速通Python并行计算:02 Python多线程编程-threading模块、线程的创建和查询与守护线程

一文速通Python并行计算:03 Python多线程编程-多线程同步(上)—基于互斥锁、递归锁和信号量

更多精彩内容可看:

给你的 Python 加加速:一文速通 Python 并行计算

一文搞懂 CM3 单片机调试原理

肝了半个月,嵌入式技术栈大汇总出炉

电子计算机类比赛的“武林秘籍”

一个MicroPython的开源项目集锦:awesome-micropython,包含各个方面的Micropython工具库

Avnet ZUBoard 1CG开发板—深度学习新选择

工程师不要迷信开源代码,还要注重基本功

什么?配色个性化的电机驱动模块?!!

什么?XIAO主控新出三款扩展板!

手把手教你实现Arduino发布第三方库

万字长文手把手教你实现MicroPython/Python发布第三方库

文档获取:

可访问如下链接进行对文档下载:

https://github.com/leezisheng/Doc

该文档是一份关于 并行计算Python 并发编程 的学习指南,内容涵盖了并行计算的基本概念、Python 多线程编程、多进程编程以及协程编程的核心知识点:

正文

1.基于条件变量的线程同步

Python 提供的 Condition 对象提供了对复杂线程同步问题的支持。Condition 被称为条件变量,与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

  • 一个线程等待"条件变量的条件成立"而挂起;
  • 另一个线程使 “条件成立”(给出条件成立信号)。

使用 Condition 的主要方式为:线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则wait;如果条件满足,进行一些处理改变条件后,通过notify方法通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件,不断的重复这一过程,从而解决复杂的同步问题。

解释条件机制最好的例子还是生产者-消费者问题,在本例中,只要缓存不满,生产者一直向缓存生产;只要缓存不空,消费者一直从缓存取出(之后销毁);当缓冲队列不为空的时候,生产者将通知消费者;当缓冲队列不满的时候,消费者将通知生产者。

**i**mport threading
import time condition = threading.Condition()
products = 0 class Producer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self) def run(self):
global condition, products
while True:
_# 消费者通过拿到锁来修改共享的资源_
if condition.acquire():
_# 线程首先acquire一个条件变量,然后判断生产是否饱和。_
if products < 10:
_# 如果产品数量小于10,继续生成,并通过notify方法通知消费者_
_# 只要缓存不满,生产者一直向缓存生产;_
products += 1;
print("Producer(%s):deliver one, now products:%s" % (self.name, products))
_# 当缓冲队列不为空的时候,生产者将通知消费者_
condition.notify()
_# 如果已经满了,那么生产者进入等待状态,直到被唤醒_
else:
print("Producer(%s):already 10, stop deliver, now products:%s" % (self.name, products))
condition.wait()
_# 如果队列没有满,就生产1个item,通知状态并释放资源_
condition.release()
time.sleep(2) class Consumer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self) def run(self):
global condition, products
while True:
if condition.acquire():
if products > 1:
_# 只要缓存不空,消费者一直从缓存取出(之后销毁)。_
products -= 1
print("Consumer(%s):consume one, now products:%s" % (self.name, products))
_# 当缓冲队列不满的时候,消费者将通知生产者。_
condition.notify()
else:
print("Consumer(%s):only 1, stop consume, products:%s" % (self.name, products))
_# 缓存空,消费者线程等待_
condition.wait()
condition.release()
time.sleep(2) if __name__ == "__main__":
_# 首先是2个生成者生产products_
for p in range(0, 2):
p = Producer()
p.start()
_# 接下来的10个消费者将会消耗products_
for c in range(0, 10):
c = Consumer()
c.start()

运行的结果如下:

乍一看这段代码好像会死锁,因为 condition.acquire() 之后就在 wait() 了,好像会一直持有锁。其实 wait() 会将锁释放,然后等待其他线程 notify() 之后会重新尝试获得锁。**但是要注意 notify() 并不会自动释放锁,所以代码中有两行,先 notify() 然后再 ****release() **

实际上,条件的检测是在互斥锁的保护下进行的。线程在改变条件状态之前必须首先锁住互斥量。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量 可以被用来实现这两进程间的线程同步。

另外:Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个RLock****;除了 notify 方法外,Condition 对象还提供了 notifyAll 方法,可以通知 waiting 池中的所有线程尝试 acquire 内部锁。由于上述机制,处于 waiting 状态的线程只能通过 notify 方法唤醒,所以 notifyAll 的作用在于防止有线程永远处于沉默状态。

如果不使用条件变量,也可以不断循环检测缓存是否大于 0,但该方法会造成 CPU 资源的浪费。采用条件变量这一问题就可以迎刃而解!条件变量允许一个线程休眠(阻塞等待)直至获取到另一个线程的通知(收到信号)再去执行自己的操作。

以上消费者-生产者模型过程如下:

以下是另一个有趣的关于条件变量的例子:

原理很简单,就是线程拿到锁先检查是不是自己渴望的状态。比如打印“B”的线程,渴望的状态 current = 'B' 然后打印出 B,将状态改成 C ,这样就成了打印“C”的线程渴望的状态。但是这里不能唤醒指定的线程,只好唤醒所有的线程,让他们自己再检查一遍状态了。

2.基于事件的线程同步

想象这样一个场景,你启动了多个线程,这些线程都要去访问一个资源,但是,这里有一个小小的问题,即将被访问的资源还没有准备好接受访问,那么此时,多个线程去访问,必然得到不响应,你还得处理这种得不到响应的情况。这样的场景下,能否先在主线程里去做试探,确定资源可以访问以后,再让已经启动了的多线程去访问呢?让我们考虑一下如何用 Event 来处理这样的问题。

  1. 创建一个 Event 对象,现在,事件内部标识是 False
  2. 启动多线程,线程里调用 wait 方法,这时,会阻塞;
  3. 主线程去试探,确定资源可访问以后,调用 set 方法,将内置标志设置为 True
  4. Event 会通知所有等待状态的线程恢复运行:已经调用 wait 的线程接手到事件信息,访问资源。

以下为示例代码:

import threading
from threading import Event def worker(event_obj, i):
print('{i}号线程等待事件信号'.format(i=i))
event_obj.wait()
print('{i}号线程收到事件信号'.format(i=i)) event = Event() for i in range(5):
t = threading.Thread(target=worker, args=(event, i))
t.start() print('确认资源可用')
event.set()

以下为代码输出,可以看到在 event.set() 后所有线程恢复运行:

让我们再一次回到生产者-消费者问题上,若要确保如果缓冲区满,生产者不会生成新数据,另外如果缓存区为空,消费者不会查找数据的要求,如何用 event 机制实现?代码如下:

import time
from threading import Thread, Event
import random items = []
event = Event() class consumer(Thread):
def __init__(self, items, event):
_# producer 类初始化时定义了item的list和 Event ,_
_# 与条件对象时候的例子不同,这里的list并不是全局的,而是通过参数传入的_
Thread.__init__(self)
self.items = items
self.event = event def run(self):
while True:
time.sleep(2)
_# 等待元素到达_
_# 当元素到达时,consumer放弃这个锁_
_# 这就允许其他生产者或消费者进入并获得这个锁_
self.event.wait() _# 若consumer放弃这个锁被唤醒,它会重新获得锁_
_# 元素到达时,元素从items列表弹出_
item = self.items.pop()
print('Consumer notify : %d popped from list by %s' % (item, self.name)) class producer(Thread):
def __init__(self, items, event):
Thread.__init__(self)
self.items = items
self.event = event def run(self):
global item
for i in range(100):
time.sleep(2)
item = random.randint(0, 256)
self.items.append(item)
print('Producer notify : item N° %d appended to list by %s' % (item, self.name))
print('Producer notify : event set by %s' % self.name)
_# 添加元素后通知事件_
_# 将内部标识设置为 true 。所有正在等待这个事件的线程将被唤醒。_
_# 当标识为 true 时,调用wait()方法的线程不会被阻塞。_
self.event.set()
print('Produce notify : event cleared by %s '% self.name)
_# 将内部标识设置为 false 。之后调用wait()方法的线程将会被阻塞,_
_# 直到调用set()方法将内部标识再次设置为 true 。_
self.event.clear() if __name__ == '__main__':
t1 = producer(items, event)
t2 = consumer(items, event)
t1.start()
t2.start()
t1.join()
t2.join()

上图是运行程序时候的运行结果,线程 t1list 最后添加值,然后设置 event 来通知消费者。消费者通过 wait() 阻塞,直到收到信号的时候从 list 中取出元素消费。

补充一下 wait() 方法:

3.基于屏障的线程同步

屏障用于应对固定数量的线程需要彼此相互等待的情况,与之前介绍 互斥锁Lock/事件Event/定时器Timer 等不同,多线程Barrier会设置一个线程障碍数量parties,如果等待的线程数量没有达到障碍数量 parties,所有线程会处于阻塞状态,当等待的线程到达了这个数量就会唤醒所有的等待线程。

以播放器为例子:

首先一个线程做播放器初始化工作(加载本地文件或者获取播放地址),然后一个线程获取视频画面,一个线程获取视频声音,只有当初始化工作完毕,视频画面获取完毕,视频声音获取完毕,播放器才会开始播放,其中任意一个线程没有完成,播放器会处于阻塞状态直到三个任务都完成!

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

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

使用方法包括:

以下为示例代码,创建三个线程:初始化准备、音频准备、视频准备,当且仅当三个初始化完成,才能启动音乐播放。

_# 导入线程模块_
import threading def plyer_display():
print('初始化通过完成,音视频同步完成,可以开始播放....') _# 设置3个障碍对象_
barrier = threading.Barrier(3, action=plyer_display, timeout=None)
def player_init(statu):
print(statu)
try:
_# 设置超时时间,如果2秒内,没有达到障碍线程数量,_
_# 会进入断开状态,引发BrokenBarrierError错误_
barrier.wait(2)
except Exception as e:
_# 断开状态,引发BrokenBarrierError错误_
print("等待超时了... ")
else:
print("xxxooooxxxxxooooxxxoooo") if __name__ == '__main__':
statu_list = ["init ready", "video ready", "audio ready"]
thread_list = list()
_# 创建三个线程:初始化准备、音频准备、视频准备_
_# 当且仅当三个初始化完成,才能启动音乐播放_
for i in range(0, 3):
t = threading.Thread(target=player_init, args=(statu_list[i],))
t.start()
thread_list.append(t)
for t in thread_list:
t.join()

以下为代码运行结果,注意:如果 barrier.wait(timeout=None) 等待超时,会进入断开状态,引发 BrokenBarrierError 错误,为了程序的健壮性,最好加上异常处理。

一文速通Python并行计算:04 Python多线程编程-多线程同步(上)—基于条件变量、事件和屏障的更多相关文章

  1. Python补充04 Python简史

    原文:Python简史 作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! Python是我喜欢的语言,简洁,优美,容易使用.前两天, ...

  2. 【Python大系】Python快速教程

    感谢原作者:Vamei 出处:http://www.cnblogs.com/vamei 怎么能快速地掌握Python?这是和朋友闲聊时谈起的问题. Python包含的内容很多,加上各种标准库.拓展库, ...

  3. 智普教育Python培训之Python开发视频教程网络爬虫实战项目

    网络爬虫项目实训:看我如何下载韩寒博客文章Python视频 01.mp4 网络爬虫项目实训:看我如何下载韩寒博客文章Python视频 02.mp4 网络爬虫项目实训:看我如何下载韩寒博客文章Pytho ...

  4. Python第一部分--Python简介+第一个程序+Python2和Python3介绍 001-016

    一.Python起源 1.1 解释器(科普) 1.2Python的设计目标 1.3 Python的设计哲学 02.为什么学Python? 代码量少 同一样问题,不用的语言解决,代码量差距还是很多的,一 ...

  5. [b0034] python 归纳 (十九)_线程同步_条件变量

    代码: # -*- coding: utf-8 -*- """ 学习线程同步,使用条件变量 逻辑: 生产消费者模型 一个有3个大小的产品库,一个生产者负责生产,一个消费者 ...

  6. python条件变量之生产者与消费者操作实例分析

    python条件变量之生产者与消费者操作实例分析 本文实例讲述了python条件变量之生产者与消费者操作.分享给大家供大家参考,具体如下: 互斥锁是最简单的线程同步机制,面对复杂线程同步问题,Pyth ...

  7. 一文总结数据科学家常用的Python库(上)

    概述 这篇文章中,我们挑选了24个用于数据科学的Python库. 这些库有着不同的数据科学功能,例如数据收集,数据清理,数据探索,建模等,接下来我们会分类介绍. 您觉得我们还应该包含哪些Python库 ...

  8. Python学习--04条件控制与循环结构

    Python学习--04条件控制与循环结构 条件控制 在Python程序中,用if语句实现条件控制. 语法格式: if <条件判断1>: <执行1> elif <条件判断 ...

  9. ubuntu14.04 python自带版本升级

    ubuntu14.04 python自带版本升级 sudo add-apt-repository ppa:fkrull/deadsnakes-python2. sudo apt-get update ...

  10. AutoPy首页、文档和下载 - 跨平台的Python GUI工具包 - 开源中国社区

    AutoPy首页.文档和下载 - 跨平台的Python GUI工具包 - 开源中国社区 AutoPy是一个简单跨平台的 Python GUI工具包,可以控制鼠标,键盘,匹配颜色和屏幕上的位图.使用纯A ...

随机推荐

  1. .NET Core:架构、特性和优势详解

    .NET Core:架构.特性和优势详解 在软件开发领域,保持领先地位至关重要.随着技术以指数级的速度发展,开发人员不断寻求高效.可扩展且多功能的解决方案来应对现代挑战..NET Core 就是这样一 ...

  2. Solution Set -「NOIP Simu.」20221014

    \(\mathscr{A}\sim\)「Unknown」tothecrazyones   有 \(n\) 堆石子, 第 \(i\) 堆有 \(a_i\) 个. Alice 和 Bob 轮流抓取, Al ...

  3. x86平台SIMD编程入门(2):通用指令

    1.重解释转换 虽然128位的XMM寄存器在硬件上只是256位YMM寄存器的下半部分,但在C++中它们是不同的类型.有一些intrinsic函数可以将它们重新解释为不同的类型,如下表所示,行代表源类型 ...

  4. Golang-基本语法2

    http://c.biancheng.net/golang/syntax/ Go语言变量的声明(使用var关键字) Go语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变 ...

  5. Kotlin:【数字类型】安全转换函数

  6. ZOS对象存储跨域资源访问的实现和使用

    本文分享自天翼云开发者社区<ZOS对象存储跨域资源访问的实现和使用>,作者:对象存储二三事 跨域的定义 跨域指的是从一个域名去请求另外一个域名的资源,即跨域名请求.跨域时,浏览器不能执行其 ...

  7. ClickHouse常用操作

    一.客户端连接1.1 客户端连接ck./clickhouse-client -h 127.0.0.1 --port 9900 -u default --password 123456 -m 1.2 h ...

  8. 告别 DeepSeek 系统繁忙,七个 DeepSeek 曲线救国平替入口,官网崩溃也能用!

    前言 DeepSeek作为一款备受瞩目的国产大模型,以其强大的功能和卓越的性能赢得了众多用户的青睐.然而,随着用户量的激增,DeepSeek官网近期频繁遭遇服务器繁忙甚至崩溃的问题,给广大用户带来了不 ...

  9. 什么是token?token是用来干嘛的?

    从事计算机行业的朋友都听说过token这么个东西,尤其是deepseek爆火后api(大家都知道什么意思吧),但是其他行业的人就很少了解到token,下面就给大家来详细介绍一下token是什么意思?t ...

  10. 【Blender】杂项笔记

    [Blender]杂项笔记 空间坐标系 Blender 中的轴向: Y 轴向前(前视图看向的方向就是前方,其默认向 Y 轴看) Z 轴向上 保持轴向导出到 Unity 时(包括直接保存.导出 FBX ...