【Python】并发编程(一)

发布于:2025-08-31 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

进程、线程、协程的区别

同步和异步介绍

线程Thread

线程的创建方式

线程的创建方式(方法包装)

线程的创建方式(类包装)

守护线程

全局锁GIL问题

Python线程同步和互斥锁

什么是线程同步?

什么是互斥锁?

信号量(Semaphore)

Python线程同步机制对比表格

死锁问题和解决方案

死锁产生的四个必要条件

常见的死锁场景:

嵌套锁死锁

递归函数死锁

死锁解决方案

方案1:使用RLock避免递归死锁

方案2:统一锁获取顺序

方案3:使用超时机制

方案4:使用上下文管理器管理多个锁

死锁检测和预防策略

调试死锁的工具和方法

1. 使用threading模块的内置功能

2. 使用第三方工具


串行(serial):一个CPU上,按顺序完成多个任务

并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的

并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

进程、线程、协程的区别

乔布斯想开工厂生产手机,费劲力气,制作一条生产线,这个生产线上有很多的器件以及材料。一条生产线就是一个进程。

只有生产线是不够的,所以找五个工人来进行生产,这个工人能够利用这些材料最终一步步的将手机做出来,这五个工人就是五个线程。

为了提高生产率,想到3种办法:

  1. 一条生产线上多招些工人,一起来做手机,这样效率是成倍増长,即单进程多线程方式
  2. 多条生产线,每个生产线上多个工人,即多进程多线程
  3. 乔布斯深入一线发现工人不是那么忙,有很多等待时间。于是规定:如果某个员工在等待生产线某个零件生产时 ,不要闲着,干点其他工作。也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,这就是:协程方式。

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
  4. 调度和切换:线程上下文切换比进程上下文切换要快得多。
  • 进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低
  • 线程(Thread):拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  • 协程(coroutine):拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度;协程切换任务资源很小,效率高

进程是什么?

进程(Process)是一个具有一定独立功能的程序关于某个数据集合的一次运行活动

现代操作系统比如Mac OS X,Linux,Windows等,都是支持“多任务”的操作系统,叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

image-20211204095652863

线程是什么?

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

有些进程还不止同时干一件事,比如微信,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

并发编程解决方案:

多任务的实现有3种方式:

  1. 多进程模式
  2. 多线程模式
  3. 多进程+多线程模式
  • 启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务
  • 启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务
  • 启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

协程是什么?

协程,Coroutines,也叫作纤程(Fiber),是一种在线程中,比线程更加轻量级的存在,由程序员自己写程序来管理。

当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。充分利用了IO等待的时间,提高了效率。

同步和异步介绍

同步和异步强调的是消息通信机制。

同步(synchronous):A调用B,等待B返回结果后,A继续执行

异步(asynchronous ):A调用B,A继续执行,不等待B返回结果;B有结果了,通知A,A再做处理。

线程Thread

线程(Thread)特点:

  1. 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
  2. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  3. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  4. 拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;
  5. 调度和切换:线程上下文切换比进程上下文切换要快得多。

线程的创建方式

Python的标准库提供了两个模块:_threadthreading_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 =name
  def 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 =name
  def 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)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

image-20211204114226986

⚠️GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,就没有GIL的问题。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷

Python线程同步和互斥锁

什么是线程同步?

线程同步是指协调多个线程的执行程序,确保他们能够有序,安全地访问共享资源。就像交通信号灯协调车辆通行一样,线程同步机制协调线程对共享数据的访问。在什么情况下,我们需要实现线程同步呢?
        1.当多个线程同时修改同一个数据时,可能导致数据不一样(形成竞争条件)
        2.某些操作需要按照特定顺序执行

为什么需要线程同步?可能会出现竞态条件(Race Condition),导致数据不一致或程序行为异常。例如:

import threading

counter = 0

def increment():
    global counter
    for _ in range(100000):
        counter += 1

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
什么是互斥锁?

互斥锁是最基本的同步机制,用于确保同一时间只有一个线程可以访问共享资源。他的工作原理是:一次只允许一个人进入,线程访问共享资源前必须先要获取锁,使用完后释放锁,让其他线程有机会获取。

竞态条件:这是多线程编程中最常见的问题,当多个线程同时访问和修改共享数据时,由于执行顺序的不确定性,导致程序结果不可预测。

示例情景:​​假设有两个线程同时执行counter += 1

  1. 线程A读取counter值为10
  2. 线程B也读取counter值为10
  3. 线程A将值加1得到11并写回
  4. 线程B也将值加1得到11并写回
    结果应该是12,但因为不同步,最终得到11

Python的threading模块提供了Lock类来实现互斥锁:

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        lock.acquire()  # 获取锁
        try:
            counter += 1
        finally:
            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 counter
    for _ 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 = money
    self.name = name
​
​
#模拟提款操作
class Drawing(Thread):
  def __init__(self,drawingNum,account):
    Thread.__init__(self)
    self.drawingNum = drawingNum
    self.account = account
    self.expenseTotal = 0
  def run(self):
    if self.account.money-self.drawingNum<0:
      return
    sleep(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 = money
    self.name = name
​
​
#模拟提款操作
class Drawing(Thread):
  def __init__(self,drawingNum,account):
    Thread.__init__(self)
    self.drawingNum = drawingNum
    self.account = account
    self.expenseTotal = 0
  def run(self):
    lock1.acquire() 
    if self.account.money-self.drawingNum<0:
      return
    sleep(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()  #你老婆取钱
  • acquirerelease方法之间的代码同一时刻只能有一个线程去操作
  • 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。

死锁问题和解决方案

死锁(Deadlock)是指两个或多个线程彼此等待对方释放资源,导致所有线程都无法继续执行的状态。

死锁产生的四个必要条件
条件 描述 示例
互斥 资源一次只能被一个线程使用 锁在同一时间只能被一个线程持有
占有且等待 线程持有资源并等待其他资源 线程A持有锁1,同时请求锁2
不可抢占 资源只能由持有者主动释放 不能强制从线程中夺取锁
循环等待 存在线程-资源的循环等待链 A等B,B等C,C等A
常见的死锁场景:
嵌套锁死锁
import threading

lock1 = 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 threading

lock = 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,再获取lock2
    with lock1:
        print("Safe Thread A acquired lock1")
        with lock2:
            print("Safe Thread A acquired lock2")

def safe_thread_b():
    # 同样先获取lock1,再获取lock2
    with 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)
        yield
    finally:
        # 按相反顺序释放锁
        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>

死锁是由于“同步块需要同时持有多个锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。


网站公告

今日签到

点亮在社区的每一天
去签到