【Python】并发编程(一)
目录
进程、线程、协程的区别
同步和异步介绍
线程Thread
线程的创建方式
线程的创建方式(方法包装)
线程的创建方式(类包装)
守护线程
全局锁GIL问题
Python线程同步和互斥锁
什么是线程同步?
什么是互斥锁?
信号量(Semaphore)
Python线程同步机制对比表格
死锁问题和解决方案
死锁产生的四个必要条件
常见的死锁场景:
嵌套锁死锁
递归函数死锁
死锁解决方案
方案1:使用RLock避免递归死锁
方案2:统一锁获取顺序
方案3:使用超时机制
方案4:使用上下文管理器管理多个锁
死锁检测和预防策略
调试死锁的工具和方法
1. 使用threading模块的内置功能
2. 使用第三方工具
串行(serial):一个CPU上,按顺序完成多个任务
并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的
并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
进程、线程、协程的区别
乔布斯想开工厂生产手机,费劲力气,制作一条生产线,这个生产线上有很多的器件以及材料。一条生产线就是一个进程。
只有生产线是不够的,所以找五个工人来进行生产,这个工人能够利用这些材料最终一步步的将手机做出来,这五个工人就是五个线程。
为了提高生产率,想到3种办法:
- 一条生产线上多招些工人,一起来做手机,这样效率是成倍増长,即单进程多线程方式
- 多条生产线,每个生产线上多个工人,即多进程多线程
- 乔布斯深入一线发现工人不是那么忙,有很多等待时间。于是规定:如果某个员工在等待生产线某个零件生产时 ,不要闲着,干点其他工作。也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,这就是:协程方式。
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
- 进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低
- 线程(Thread):拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
- 协程(coroutine):拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度;协程切换任务资源很小,效率高
进程是什么?
进程(Process)是一个具有一定独立功能的程序关于某个数据集合的一次运行活动
现代操作系统比如Mac OS X,Linux,Windows等,都是支持“多任务”的操作系统,叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
线程是什么?
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
有些进程还不止同时干一件事,比如微信,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
并发编程解决方案:
多任务的实现有3种方式:
- 多进程模式
- 多线程模式
- 多进程+多线程模式
- 启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务
- 启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务
- 启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。
协程是什么?
协程,Coroutines,也叫作纤程(Fiber),是一种在线程中,比线程更加轻量级的存在,由程序员自己写程序来管理。
当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。充分利用了IO等待的时间,提高了效率。
同步和异步介绍
同步和异步强调的是消息通信机制。
同步(synchronous):A调用B,等待B返回结果后,A继续执行
异步(asynchronous ):A调用B,A继续执行,不等待B返回结果;B有结果了,通知A,A再做处理。
线程Thread
线程(Thread)特点:
- 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
线程的创建方式
Python的标准库提供了两个模块:_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。
线程的创建可以通过分为两种方式:
1. 方法包装
2. 类包装
线程的执行统一通过start()
方法
线程的创建方式(方法包装)
#encoding=utf-8
#方法方式创建线程
from threading import Thread
from time import sleep
def func1(name):for i in range(3):print(f"thread:{name} :{i}")sleep(1)
if __name__ == '__main__':print("主线程,start")#创建线程t1 = Thread(target=func1,args=("t1",))t2 = Thread(target=func1,args=("t2",))#启动线程t1.start()t2.start()print("主线程,end")
'''
运行结果可能会出现换行问题,是因为多个线程抢夺控制台输出的IO流。
比如,如下的输出换行就没有按照预想的显示:
主线程,start
thread:t1 :0
thread:t2 :0
主线程,end
thread:t2 :1thread:t1 :1
thread:t2 :2
thread:t1 :2
'''
线程的创建方式(类包装)
#encoding=utf-8
#类的方式创建线程
from threading import Thread
from time import sleep
class MyThread(Thread):def __init__(self,name):Thread.__init__(self)self.name =namedef run(self):for i in range(3):print(f"thread:{self.name} :{i}")sleep(1)
if __name__ == '__main__':print("主线程,start")#创建线程(类的方式)t1 = MyThread('t1')t2 = MyThread('t2')#启动线程t1.start()t2.start()print("主线程,end")
以上的代码,主线程不会等待子线程结束。如果想要实现等待子线程结束后,再结束主线程,我们可使用join()方法。
#encoding=utf-8
from threading import Thread
from time import sleep
def func1(name):for i in range(3):print(f"thread:{name} :{i}")sleep(1)
if __name__ == '__main__':print("主线程,start")#创建线程t1 = Thread(target=func1,args=("t1",))t2 = Thread(target=func1,args=("t2",))#启动线程t1.start()t2.start()#主线程会等待t1,t2结束后,再往下执行t1.join()t2.join()print("主线程,end")
守护线程
在行为上还有一种叫守护线程,主要的特征是它的生命周期。主线程死亡,它也就随之死亡。在python中,线程通过setDaemon(True|False)
来设置是否为守护线程。
守护线程的作用:守护线程作用是为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。
#encoding=utf-8
from threading import Thread
from time import sleep
class MyThread(Thread):def __init__(self,name):Thread.__init__(self)self.name =namedef run(self):for i in range(3):print(f"thread:{self.name} :{i}")sleep(1)
if __name__ == '__main__':print("主线程,start")#创建线程(类的方式)t1 = MyThread('t1')#t1设置为守护线程t1.setDaemon(True)#3.10后被废弃,可以直接:t1.daemon=True#启动线程t1.start()print("主线程,end")
全局锁GIL问题
在python中,无论你有多少核,在Cpython解释器中永远都是假象。无论你是4核,8核,还是16核.......不好意思,同一时间执行的线程只有一个线程,它就是这个样子的。这个是python的一个开发时候,设计的一个缺陷,所以说python中的线程是“含有水分的线程”。
Python GIL(Global Interpreter Lock)
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
⚠️GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,就没有GIL的问题。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷
Python线程同步和互斥锁
什么是线程同步?
线程同步是指协调多个线程的执行程序,确保他们能够有序,安全地访问共享资源。就像交通信号灯协调车辆通行一样,线程同步机制协调线程对共享数据的访问。在什么情况下,我们需要实现线程同步呢?
1.当多个线程同时修改同一个数据时,可能导致数据不一样(形成竞争条件)
2.某些操作需要按照特定顺序执行
为什么需要线程同步?可能会出现竞态条件(Race Condition),导致数据不一致或程序行为异常。例如:
import threadingcounter = 0def increment():global counterfor _ in range(100000):counter += 1threads = []
for _ in range(5):t = threading.Thread(target=increment)threads.append(t)t.start()for t in threads:t.join()print(f"Final counter value: {counter}") # 可能不是500000
什么是互斥锁?
互斥锁是最基本的同步机制,用于确保同一时间只有一个线程可以访问共享资源。他的工作原理是:一次只允许一个人进入,线程访问共享资源前必须先要获取锁,使用完后释放锁,让其他线程有机会获取。
竞态条件:这是多线程编程中最常见的问题,当多个线程同时访问和修改共享数据时,由于执行顺序的不确定性,导致程序结果不可预测。
示例情景:假设有两个线程同时执行counter += 1
:
- 线程A读取counter值为10
- 线程B也读取counter值为10
- 线程A将值加1得到11并写回
- 线程B也将值加1得到11并写回
结果应该是12,但因为不同步,最终得到11
Python的threading
模块提供了Lock
类来实现互斥锁:
import threadingcounter = 0
lock = threading.Lock()def increment():global counterfor _ in range(100000):lock.acquire() # 获取锁try:counter += 1finally:lock.release() # 释放锁threads = []
for _ in range(5):t = threading.Thread(target=increment)threads.append(t)t.start()for t in threads:t.join()print(f"Final counter value: {counter}") # 现在是500000
使用with语句简化
def increment():global counterfor _ in range(100000):with lock: # 自动获取和释放锁counter += 1
其他同步原语 RLock(可重入锁) 允许同一个线程多次获取锁:
rlock = threading.RLock()def func():with rlock:# 可以再次获取同一个锁with rlock:print("Nested lock")
信号量(Semaphore)
信号量是限制同时访问资源的线程数量。互斥锁使用后,一个资源同时只有一个线程访问。如果某个资源,我们同时想让N个(指定数值)线程访问?这时候,可以使用信号量。
信号量控制同时访问资源的数量。信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。
应用场景:在读写文件的时候,一般只能只有一个线程在写,而读可以有多个线程同时进行,如果需要限制同时读文件的线程个数,这时候就可以用到信号量了(如果用互斥锁,就是限制同一时刻只能有一个线程读取文件)。或者是在做爬虫抓取数据时。
底层原理信号量底层就是一个内置的计数器。每当资源获取时(调用acquire)计数器-1,资源释放时(调用release)计数器+1。
semaphore = threading.Semaphore(3) # 最多3个线程同时访问def access_resource():with semaphore:print(f"{threading.current_thread().name} accessing resource")# 模拟资源访问time.sleep(1)
信号量例子:
#coding=utf-8
from threading import Thread, Lock
from time import sleep
from multiprocessing import Semaphore
"""
一个房间一次只允许两个人通过
若不使用信号量,会造成所有人都进入这个房子
若只允许一人通过可以用锁-Lock()
"""
def home(name, se):se.acquire() # 拿到一把钥匙print(f'{name}进入了房间')sleep(3)print(f'******************{name}走出来房间')se.release() # 还回一把钥匙
if __name__ == '__main__':se = Semaphore(2) # 创建信号量的对象,有两把钥匙for i in range(7):p = Thread(target=home, args=(f'tom{i}', se))p.start()
'''
执行结果:
tom0进入了房间
tom1进入了房间
******************tom1走出来房间
tom2进入了房间
******************tom0走出来房间
tom3进入了房间
******************tom2走出来房间******************tom3走出来房间
tom4进入了房间
tom5进入了房间
******************tom5走出来房间******************tom4走出来房间
tom6进入了房间
******************tom6走出来房间
Process finished with exit code 0
'''
条件变量(Condition)用于线程间的复杂协调:
condition = threading.Condition()
items = []def producer():for i in range(5):with condition:items.append(i)condition.notify() # 通知等待的消费者time.sleep(1)def consumer():while True:with condition:while not items:condition.wait() # 等待生产者通知item = items.pop(0)print(f"Consumed {item}")
事件(Event)简单的线程间通信机制:主要用于唤醒正在阻塞等待状态的线程
原理 Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,event 对象中的信号标志被设置假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,它将唤醒所有等待个 event 对象的线程。如果一个线程等待一个已经被设置为真的 event 对象,那么它将忽略这个事件,继续执行。
Event()
可以创建一个事件管理标志,该标志(event)默认为False,event对象主要有四种方法可以调用:
方法名 | 说明 |
---|---|
event.wait(timeout=None) | 调用该方法的线程会被阻塞,如果设置了timeout参数,超时后,线程会停止阻塞继续执行; |
event.set() | 将event的标志设置为True,调用wait方法的所有线程将被唤醒 |
event.clear() | 将event的标志设置为False,调用wait方法的所有线程将被阻塞 |
event.is_set() | 判断event的标志是否为True |
event = threading.Event()def waiter():print("Waiting for event")event.wait() # 阻塞直到事件被设置print("Event occurred")def setter():time.sleep(2)print("Setting event")event.set() # 唤醒所有等待的线程
事件(Event)例子:
#coding:utf-8
#小伙伴们,围着吃火锅,当菜上齐了,请客的主人说:开吃!
#于是小伙伴一起动筷子,这种场景如何实现
import threading
import time
def chihuoguo(name):#等待事件,进入等待阻塞状态print(f'{name}已经启动')print(f'小伙伴{name}已经进入就餐状态!')time.sleep(1)event.wait()# 收到事件后进入运行状态print(f'{name}收到通知了.' )print(f'小伙伴{name}开始吃咯!')
if __name__ == '__main__':event = threading.Event()# 创建新线程thread1 = threading.Thread(target=chihuoguo, args=("tom", ))thread2 = threading.Thread(target=chihuoguo, args=("cherry", ))# 开启线程thread1.start()thread2.start()
time.sleep(10)# 发送事件通知print('---->>>主线程通知小伙伴开吃咯!')event.set()
'''
执行结果:
tom已经启动
小伙伴tom已经进入就餐状态!
cherry已经启动
小伙伴cherry已经进入就餐状态!
---->>>主线程通知小伙伴开吃咯!
tom收到通知了.
小伙伴tom开始吃咯!
cherry收到通知了.
小伙伴cherry开始吃咯!
'''
全局解释器锁(GIL)的影响
Python的GIL确保同一时间只有一个线程执行Python字节码,这意味着:
- I/O密集型任务可以从多线程中受益
- CPU密集型任务可能不会从多线程中获得性能提升(考虑使用多进程)
Python线程同步机制对比表格
同步机制 | 主要用途 | 特点 | 适用场景 | 示例代码 |
---|---|---|---|---|
Lock(互斥锁) | 基本的互斥访问 | - 一次只允许一个线程访问 - 最简单的同步机制 - 不支持重入 | 保护简单的共享变量或资源 | with lock: counter += 1 |
RLock(可重入锁) | 同一线程多次获取锁 | - 允许同一线程多次获取 - 需要相同次数的释放 - 避免自死锁 | 递归函数或嵌套调用需要同步的场景 | with rlock: func() |
Semaphore(信号量) | 限制并发访问数量 | - 控制同时访问的线程数 - 有计数器的锁 - 可用于资源池 | 连接池、限流、控制最大并发数 | with semaphore: access_resource() |
Condition(条件变量) | 复杂的线程协调 | - 等待特定条件成立 - 可以通知特定线程 - 结合锁使用 | 生产者-消费者模式、复杂的状态等待 | condition.wait() , condition.notify() |
Event(事件) | 简单的线程通信 | - 线程间信号传递 - 一次性广播机制 - 简单易用 | 启动信号、完成通知、简单协调 | event.wait() , event.set() |
Barrier(屏障) | 同步多个线程执行点 | - 等待指定数量线程到达 - 所有线程同时继续执行 - 同步执行点 | 分阶段处理、多线程同时开始任务 | barrier.wait() |
性能特点对比
机制 | 开销 | 灵活性 | 复杂度 | 推荐使用场景 |
---|---|---|---|---|
Lock | 低 | 低 | 低 | 简单的变量保护 |
RLock | 中 | 中 | 中 | 递归或嵌套同步 |
Semaphore | 中 | 中 | 中 | 资源池、限流 |
Condition | 高 | 高 | 高 | 复杂协调逻辑 |
Event | 低 | 低 | 低 | 简单信号通知 |
Barrier | 中 | 中 | 中 | 阶段同步 |
选择指南
需求 | 推荐机制 | 理由 |
---|---|---|
保护共享变量 | Lock | 简单高效,适合基本互斥 |
递归函数同步 | RLock | 避免同一线程死锁 |
限制并发数 | Semaphore | 内置计数器,控制访问数量 |
等待特定条件 | Condition | 支持复杂的等待/通知机制 |
简单信号通知 | Event | 轻量级,易于使用 |
同步多个线程 | Barrier | 确保所有线程到达同一点 |
内存和性能考虑
机制 | 内存占用 | CPU开销 | 线程阻塞方式 |
---|---|---|---|
Lock | 很低 | 很低 | 忙等待或系统阻塞 |
RLock | 低 | 低 | 系统阻塞 |
Semaphore | 低 | 中 | 系统阻塞 |
Condition | 中 | 高 | 系统阻塞 |
Event | 很低 | 很低 | 系统阻塞 |
Barrier | 中 | 中 | 系统阻塞 |
【示例】多线程操作同一个对象(未使用线程同步)
#encoding=utf-8
from threading import Thread
from time import sleep
class Account:def __init__(self,money,name):self.money = moneyself.name = name
#模拟提款操作
class Drawing(Thread):def __init__(self,drawingNum,account):Thread.__init__(self)self.drawingNum = drawingNumself.account = accountself.expenseTotal = 0def run(self):if self.account.money-self.drawingNum<0:returnsleep(1) #判断完后阻塞。其他线程开始运行。self.account.money -= self.drawingNum;self.expenseTotal += self.drawingNum;print(f"账户:{self.account.name},余额是:{self.account.money}")print(f"账户:{self.account.name},总共取了:{self.expenseTotal}")
if __name__ == '__main__':a1 = Account(100,"gaoqi")draw1 = Drawing(80,a1) #定义取钱线程对象;draw2 = Drawing(80,a1) #定义取钱线程对象;draw1.start() #你取钱draw2.start() #你老婆取钱
由于没有线程同步机制,两个线程同时操作同一个账户对象,竟然从只有100元的账户,轻松取出80*2=160元,账户余额竟然成为了-60。这么大的问题,显然银行不会答应的。
接下来我们添加一个互斥锁,对上面的程序使用线程同步:
#encoding=utf-8
from threading import Thread,Lock
from time import sleep
class Account:def __init__(self,money,name):self.money = moneyself.name = name
#模拟提款操作
class Drawing(Thread):def __init__(self,drawingNum,account):Thread.__init__(self)self.drawingNum = drawingNumself.account = accountself.expenseTotal = 0def run(self):lock1.acquire() if self.account.money-self.drawingNum<0:returnsleep(1) #判断完后阻塞。其他线程开始运行。self.account.money -= self.drawingNum;self.expenseTotal += self.drawingNum;lock1.release()print(f"账户:{self.account.name},余额是:{self.account.money}")print(f"账户:{self.account.name},总共取了:{self.expenseTotal}")
if __name__ == '__main__':a1 = Account(100,"gaoqi")lock1 = Lock()draw1 = Drawing(80,a1) #定义取钱线程对象;draw2 = Drawing(80,a1) #定义取钱线程对象;draw1.start() #你取钱draw2.start() #你老婆取钱
acquire
和release
方法之间的代码同一时刻只能有一个线程去操作- 如果在调用
acquire
方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire
方法会堵塞,直到这个互斥锁释放后才能再次上锁。
死锁问题和解决方案
死锁(Deadlock)是指两个或多个线程彼此等待对方释放资源,导致所有线程都无法继续执行的状态。
死锁产生的四个必要条件
条件 | 描述 | 示例 |
---|---|---|
互斥 | 资源一次只能被一个线程使用 | 锁在同一时间只能被一个线程持有 |
占有且等待 | 线程持有资源并等待其他资源 | 线程A持有锁1,同时请求锁2 |
不可抢占 | 资源只能由持有者主动释放 | 不能强制从线程中夺取锁 |
循环等待 | 存在线程-资源的循环等待链 | A等B,B等C,C等A |
常见的死锁场景:
嵌套锁死锁
import threadinglock1 = threading.Lock()
lock2 = threading.Lock()def thread_a():with lock1:print("Thread A acquired lock1")# 模拟一些工作threading.Event().wait(0.1)with lock2: # 这里会发生死锁!print("Thread A acquired lock2")def thread_b():with lock2:print("Thread B acquired lock2")# 模拟一些工作threading.Event().wait(0.1)with lock1: # 这里会发生死锁!print("Thread B acquired lock1")# 运行这两个线程会导致死锁
t1 = threading.Thread(target=thread_a)
t2 = threading.Thread(target=thread_b)
t1.start()
t2.start()
递归函数死锁
import threadinglock = threading.Lock()def recursive_func(n):with lock:if n > 0:print(f"Level {n}")recursive_func(n - 1) # 这里会发生死锁!# 使用普通Lock会导致死锁
# recursive_func(3) # 这会死锁!
死锁解决方案
方案1:使用RLock避免递归死锁
import threading# 使用RLock代替Lock
rlock = threading.RLock()def safe_recursive_func(n):with rlock: # RLock允许同一线程多次获取if n > 0:print(f"Safe level {n}")safe_recursive_func(n - 1) # 现在安全了safe_recursive_func(3) # 正常工作
方案2:统一锁获取顺序
def safe_thread_a():# 总是先获取lock1,再获取lock2with lock1:print("Safe Thread A acquired lock1")with lock2:print("Safe Thread A acquired lock2")def safe_thread_b():# 同样先获取lock1,再获取lock2with lock1:print("Safe Thread B acquired lock1")with lock2:print("Safe Thread B acquired lock2")
方案3:使用超时机制
def timeout_solution():# 尝试获取锁,设置超时时间if lock1.acquire(timeout=2): # 等待最多2秒try:if lock2.acquire(timeout=1): # 等待最多1秒try:print("成功获取两个锁")finally:lock2.release()finally:lock1.release()else:print("获取锁超时,执行回退操作")
方案4:使用上下文管理器管理多个锁
from contextlib import contextmanager@contextmanager
def acquire_locks(*locks):# 按固定顺序获取所有锁acquired_locks = []try:for lock in sorted(locks, key=id): # 按对象ID排序确保顺序一致lock.acquire()acquired_locks.append(lock)yieldfinally:# 按相反顺序释放锁for lock in reversed(acquired_locks):lock.release()# 使用示例
def safe_with_locks():with acquire_locks(lock1, lock2):print("安全地获取了多个锁")
死锁检测和预防策略
策略 | 方法 | 优点 | 缺点 |
---|---|---|---|
超时机制 | 设置锁获取超时时间 | 简单易实现 | 可能造成性能开销 |
锁顺序 | 统一锁的获取顺序 | 有效预防循环等待 | 需要严格的编程规范 |
锁粒度 | 减小锁的持有范围 | 减少死锁概率 | 可能增加复杂度 |
资源分配 | 一次性获取所有所需资源 | 避免占有且等待 | 可能降低资源利用率 |
调试死锁的工具和方法
1. 使用threading模块的内置功能
import threading
import time# 设置线程超时检测
threading.setprofile(lambda *args: None) # 简单的性能分析# 或者使用信号量超时
def deadlock_detector():time.sleep(5) # 等待5秒print("可能的死锁检测!检查线程状态")for thread in threading.enumerate():print(f"线程 {thread.name} 状态: {thread.is_alive()}")
2. 使用第三方工具
# 使用py-spy检测死锁
pip install py-spy
py-spy dump --pid <PID># 使用gdb调试Python死锁
gdb -p <PID>
死锁是由于“同步块需要同时持有多个锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。