当前位置: 首页 > news >正文

python Barrier

## 聊聊 Python 里的 Barrier:一种让线程“等等再走”的同步机制

写多线程程序的时候,经常会遇到一种情况:几个线程各自忙活手头的事,但必须等大家都干完某个阶段的话,才能一起进入下一个阶段。比如,几个工人分别组装一台机器的不同部件,所有部件都装好了,才能一起开始测试整机。如果各干各的,没有协调,可能有的部件还没装好,测试就开始了,结果肯定要出问题。

在 Python 的threading模块里,就提供了一个专门解决这种问题的工具,叫做Barrier,中文可以理解为“栅栏”或者“屏障”。它就像赛跑时起跑线上的那道栏杆,所有运动员都准备好了,栏杆才抬起,大家才能同时起跑。

Barrier 是什么

简单说,Barrier 是一个同步原语,它允许一组线程彼此等待,直到所有线程都到达某个预设的点,然后才一起继续执行。创建 Barrier 对象时,需要指定一个数字,比如n,这就表示我们要等待n个线程。每个线程在工作过程中,调用barrier.wait()方法,就相当于这个线程说:“我到了,在起跑线等着了。” 当第n个线程也调用了wait(),栅栏就会打开,所有等待的线程被同时释放,继续执行后面的代码。

它内部有个计数器。线程调用wait(),计数器就减一。当计数器减到零,说明所有线程都到齐了,之前所有在wait()上阻塞的线程就都被唤醒。之后,这个 Barrier 的计数器会自动重置,又可以开始新的一轮等待。所以,一个 Barrier 对象是可以重复使用的。

它能做什么

它的核心用途就是“同步点”或“集结”操作。想象几个现实中的场景:

  • 数据分阶段处理:一个数据处理任务被拆成几个独立的子任务,由不同线程并行处理。第一阶段,各自读取数据;第二阶段,各自进行初步计算。必须确保所有线程都完成了数据读取,才能一起开始计算,否则计算线程可能读到空数据。这里,第一阶段结束就是一个同步点。
  • 模拟系统:比如模拟一个交通路口,有来自四个方向的车流线程。每一轮模拟中,各自更新车辆位置,然后需要等所有方向都更新完毕,才能基于新的全局状态开始下一轮的位置计算。否则,某个方向的车可能已经“穿越”到下一秒,而另一个方向的车还停留在上一秒的位置,碰撞检测就会出错。
  • 并行算法:很多并行计算算法,如一些迭代求解方法,每一轮迭代都需要所有并行任务完成当前轮次的计算,交换边界数据后,才能开始下一轮。

在这些场景下,用简单的Thread.join()是不行的,因为join()是让主线程等子线程结束,而 Barrier 是让并行的子线程之间互相等待,并且它们等待后并不结束,而是继续携手干后面的活。

怎么使用

使用 Barrier 的代码模式通常很固定。先导入,创建对象,然后在线程的目标函数里在需要同步的地方调用wait()

来看一个简单的例子,模拟三个下载线程和一个解压线程的协作。假设有三个大文件分块下载,必须全部下载完成,才能开始统一解压。

importthreadingimporttimeimportrandomdefdownload_file(part_id,barrier):"""模拟下载文件的一个部分"""download_time=random.uniform(0.5,2.0)time.sleep(download_time)print(f"下载部分{part_id}完成,耗时{download_time:.2f}秒")# 下载完了,到同步点等着barrier.wait()# 等所有部分都下载完后,才会执行下面的打印(实际中可能是通知解压线程)print(f"部分{part_id}已就绪,等待解压指令")defextract_all(barrier):"""模拟解压线程,它也在等这个同步点"""print("解压线程:等待所有文件部分下载完成...")barrier.wait()# 当所有下载线程都到达后,栅栏打开,解压线程继续print("解压线程:所有部分已就绪,开始解压!")time.sleep(1)# 模拟解压耗时print("解压完成!")# 创建一个Barrier,等待4个线程:3个下载 + 1个解压barrier=threading.Barrier(parties=4)# 创建并启动线程threads=[]foriinrange(3):t=threading.Thread(target=download_file,args=(i,barrier))threads.append(t)t.start()# 解压线程也参与同一道栅栏的等待extract_thread=threading.Thread(target=extract_all,args=(barrier,))threads.append(extract_thread)extract_thread.start()# 等待所有线程结束fortinthreads:t.join()print("所有任务结束。")

运行这段代码,你会看到三个下载线程完成时间各不相同,但“已就绪,等待解压指令”这条打印,会在所有下载完成、解压线程打印“开始解压”之后才出现。这正是因为barrier.wait()把大家拦住了,直到最后一个(第四个)线程也调用了wait()

Barrier 的wait()方法还会返回一个 0 到n-1之间的整数,代表是第几个到达栅栏的线程,这有时可以用来分配后续的不同任务。此外,创建 Barrier 时还可以指定一个可选的action参数,这是一个函数,它会在所有线程被释放前,由其中一个线程执行一次,适合做一些统一的准备工作。

一些实践中的细节点

使用 Barrier 时,有几个地方需要留心。

首先是线程数量必须匹配。如果创建的 Barrier 指定了parties=5,但实际只有4个线程调用了wait(),那么程序就会永远卡在那里,第5个线程永远不会来。这是一种典型的死锁。所以,在设计时一定要确保参与同步的线程数是确定的,并且都会正确执行到wait()调用。

其次,要考虑异常处理。如果某个等待在栅栏上的线程发生了异常,或者被中断了,Barrier 会进入“损坏”(broken)状态。所有已经调用或后续调用wait()的线程,都会收到一个BrokenBarrierError异常。这是为了防止因为一个线程的意外退出,导致其他所有线程无限期等待。一旦 Barrier 损坏,就不能再用了,需要重新创建一个新的实例。

因此,在实际代码中,最好把barrier.wait()放在 try-except 块里,妥善处理BrokenBarrierError和可能的超时(wait()可以设置timeout参数)。

最后,Barrier 适合的是那种循环的、多阶段的同步。如果只是一次性的“集合然后出发”,它很合适。但如果线程之间的等待关系非常复杂,比如A等B,B等C,C又等A,形成了环,那 Barrier 也解决不了,那是死锁的经典模型,需要从设计上避免。

和别的同步工具比比看

Pythonthreading模块里的同步工具不少,各有各的用处。

Lock(锁)是最基础的,用于保证一段代码(临界区)同一时间只能被一个线程执行,解决的是“互斥”问题。比如一个柜台,一次只服务一个顾客。

Event(事件)用于线程间的简单通知。一个线程设置事件(event.set()),其他等待事件(event.wait())的线程就会被唤醒。它像是一个广播开关,不关心有多少线程被唤醒,也不保证它们同时开始。比如下课铃响了,学生们(线程)听到后各自开始活动,但出门时间可能有先后。

Semaphore(信号量)是计数器,用于控制同时访问某个资源的线程数量。比如一个停车场有5个车位,信号量初始值为5,每进一辆车减1,减到0后面的车就得等。它控制的是“并发量”。

Condition(条件变量)则更灵活,它通常结合锁使用,允许线程在某个条件不满足时等待,并在条件可能改变时被通知唤醒。它适合“等待特定状态”的场景。比如消费者线程等待队列不为空。

Barrier的焦点非常明确,就是“全员到齐”。它强调的是一种集体行动的开始,所有参与者地位对等,必须都准备好。这是它和Event一个关键区别:Event可能是一个线程通知其他多个;而Barrier是所有线程互相通知,共同达成一个状态。

所以,选择哪个工具,取决于你想解决的同步问题的本质。如果需要严格的、多阶段的“齐步走”,Barrier的语义是最清晰直接的。用锁或事件去模拟这种“全员集合”的行为,代码会复杂不少,也容易出错。

总的来说,Barrier是一个小巧但语义强大的工具。在编写需要分阶段协同的并行程序时,把它从工具箱里拿出来,往往能让代码的逻辑变得更干净,更贴近我们对于“团队协作”的直觉理解。

http://www.cnnetsun.cn/news/1709508.html

相关文章:

  • 【ROS】深入解析ros-Noetic-desktop-full安装依赖冲突的排查与修复
  • 从电路分析到控制系统:常系数齐次微分方程的特征根法到底有多好用?
  • 告别付费教程!手把手教你用Libero完成FPGA项目仿真与下载(基于Verilog)
  • Fooocus完全指南:零门槛AI图像创作的创新方法(设计师与创作者适用)
  • 如何用iTwin.js快速构建基础设施数字孪生应用?[特殊字符]
  • 3步构建企业级AI应用:无代码开发新范式
  • AGV 自动充电是什么
  • 3分钟掌握OneNote转Markdown:零基础迁移实战指南
  • 8-Bit硬边框UI如何提升AI工具体验?Pixel Fashion Atelier交互反馈机制解析
  • Jupyter Notebook内核切换全攻略:从Anaconda虚拟环境到PyTorch版本管理
  • Trilium Notes 知识管理实战指南:从信息碎片到知识网络的构建方法
  • 3大核心优势解析:开源矢量编辑工具SVG Editor全攻略
  • 2026届毕业生推荐的十大AI论文助手实际效果
  • qmcdump终极指南:轻松解密QQ音乐加密音频的完整教程
  • 新手福音,无需安装python,在快马平台开启你的第一行代码之旅
  • AI教材编写秘籍:低查重策略,让你的教材脱颖而出!
  • 低查重AI教材生成秘籍,轻松掌握AI写教材实用方法!
  • UndertaleModTool实战指南:GameMaker游戏定制的全流程解决方案
  • 短视频 SEO 与文章 SEO 有什么不同_如何制作高质量的短视频提升 SEO
  • Visual Studio残留清理完全指南:从系统隐患到环境重生的专业解决方案
  • 基于stm32的个人健康助手设计[单片机]-计算机毕业设计源码+LW文档
  • LSPosed模块开发全指南:从入门到精通的Android功能扩展技术
  • 解锁游戏自由:Sunshine开源解决方案打造跨设备串流体验
  • 开源硬件管理能力提升实战指南:3步释放你的设备全部潜能
  • BetterJoy 完整指南:让Switch手柄成为你的PC游戏利器
  • 【WinForm进阶实战】TreeView控件:从数据绑定到动态交互的完整指南
  • 利用快马平台快速搭建mybatis项目原型,十分钟搞定基础crud
  • Typora 0.11.18 免费版终极指南:Windows与ArchLinux双平台安装与破解
  • 效率飞跃:利用快马AI将opcore simlify融入你的开发工作流
  • .NET 10 打造 OpenClaw Windows Node