Day07- 管理并发和并行挑战:竞争条件和死锁
管理并发和并行挑战:竞争条件和死锁
并发和并行在 Python 中提供了显著的性能优势,但它们也引入了诸如竞态条件和死锁等挑战。理解和缓解这些问题对于构建健壮可靠的并发应用程序至关重要。本课程将深入探讨这些挑战,为您提供管理和有效应对它们的知识和工具。
理解竞态条件
当多个线程或进程并发访问和修改共享数据时,如果最终结果取决于这些访问发生的不可预测顺序,就会发生竞态条件。这可能导致意外和错误的结果。
竞态条件是如何发生的
想象有两个线程尝试递增一个共享的计数器变量。每个线程执行以下步骤:
- 读取计数器的当前值。
- 增加值。
- 将新值写回计数器。
如果两个线程同时执行这些步骤,可能会发生以下情况:
- 线程1读取计数器的值(例如,5)。
- 线程2读取计数器的值(例如,5)。
- 线程1将值增加到6。
- 线程2将值增加到6。
- 线程1将值6写回计数器。
- 线程2将值6写回计数器。
计数器没有增加到7(即被增加两次),而是只增加到6,因为两个线程读取了相同的初始值,然后互相覆盖了对方的更新。
未受保护的计数器
import threadingcounter = 0
num_increments = 100000def increment_counter():global counterfor _ in range(num_increments):counter += 1# 创建两个线程
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)#启动线程
thread1.start()
thread2.start()# 等待线程完成
thread1.join()
thread2.join()print(f"Final counter value: {counter}") # 预期:200000,但可能更少
在这个例子中,increment_counter
函数由两个线程执行。由于竞态条件,最终的计数器值可能会小于预期的 200000。
使用锁防止竞态条件
锁(也称为互斥锁)是一种同步机制,它允许同一时间只有一个线程访问共享资源。通过使用锁,我们可以保护访问和修改共享数据的代码的关键部分。
import threadingcounter = 0
num_increments = 100000
lock = threading.Lock() # 创建锁def increment_counter():global counterfor _ in range(num_increments):with lock: # 访问共享资源前获取锁counter += 1 # 临界区# 创建两个线程
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)#启动线程
thread1.start()
thread2.start()# 等待线程完成
thread1.join()
thread2.join()print(f"Final counter value: {counter}") # Expected: 200000
在这个修正的示例中,with lock:
语句确保同一时间只有一个线程可以执行 counter += 1
这行代码。当进入 with
块时锁会自动获取,并在退出时释放,即使发生异常也是如此。这防止了竞态条件,并确保计数器正确地递增。
现实中的竞态条件示例
- 银行系统: 想象两个并发事务尝试更新同一个银行账户余额。如果没有适当的同步,一个事务可能会覆盖另一个事务,导致余额不正确。
- 库存管理: 在一个电子商务系统中,多个用户可能会同时尝试购买库存中的最后一件商品。这可能导致商品被卖给的用户数量超过实际库存量。
假设情景
考虑一个管理共享缓存的多线程应用程序。多个线程可能会同时尝试更新或使缓存条目失效。如果没有适当的锁定机制,缓存可能会变得不一致,导致向用户提供错误的数据。
理解死锁
当两个或更多线程或进程无限期地相互等待释放它们所需的资源时,就会发生死锁。这会导致停滞状态,没有任何进展。
死锁条件
死锁通常在以下四个条件同时满足时发生,这些条件被称为 Coffman 条件:
- 互斥: 资源不可共享,意味着同一时间只有一个线程可以持有该资源。
- 持有等待: 线程在等待获取另一个资源时持有资源。
- 不可抢占: 资源不能被强制从持有它的线程中移除;它们必须被自愿释放。
- 循环等待: 两个或多个线程以循环方式互相等待(例如,线程 A 等待线程 B,线程 B 等待线程 A)。
示例:死锁场景
import threading
import timelock_a = threading.Lock()
lock_b = threading.Lock()def thread_one():lock_a.acquire()print("Thread one acquired lock A")time.sleep(0.1) # 模拟一些工作lock_b.acquire()print("Thread one acquired lock B")lock_b.release()lock_a.release()def thread_two():lock_b.acquire()print("Thread two acquired lock B")time.sleep(0.1) # 模拟一些工作lock_a.acquire()print("Thread two acquired lock A")lock_a.release()lock_b.release()# 创建并启动线程
thread1 = threading.Thread(target=thread_one)
thread2 = threading.Thread(target=thread_two)thread1.start()
thread2.start()thread1.join()
thread2.join()print("Finished")
在这个例子中,thread_one
获取了 lock_a
,然后尝试获取 lock_b
,而 thread_two
获取了 lock_b
,然后尝试获取 lock_a
。如果两个线程在另一个线程之前获取了它们第一个锁,它们都会无限期地被阻塞,等待另一个线程释放它们需要的锁,从而导致死锁。
防止死锁
可以使用多种策略来防止死锁:
- 锁顺序: 建立一致的锁获取顺序。如果所有线程都以相同的顺序获取锁,就可以防止循环等待条件。
- 超时: 在获取锁时使用超时。如果线程在特定时间内无法获取锁,它会释放当前持有的所有锁并稍后重试。这可以防止无限期等待。
- 资源层级: 为资源分配层级,并要求线程按层级升序获取资源。
- 死锁检测与恢复: 检测死锁并采取行动打破它们,例如通过中止其中一个死锁的线程。
示例:通过锁排序防止死锁
import threading
import timelock_a = threading.Lock()
lock_b = threading.Lock()def thread_one():# 按一致的顺序获取锁(先 A 后 B)lock_a.acquire()print("Thread one acquired lock A")time.sleep(0.1)lock_b.acquire()print("Thread one acquired lock B")lock_b.release()lock_a.release()def thread_two():# 按相同顺序获取锁(先 A 后 B)lock_a.acquire() #改为先获取lock_aprint("Thread two acquired lock A")time.sleep(0.1)lock_b.acquire()print("Thread two acquired lock B")lock_b.release()lock_a.release()# 创建并启动线程
thread1 = threading.Thread(target=thread_one)
thread2 = threading.Thread(target=thread_two)thread1.start()
thread2.start()thread1.join()
thread2.join()print("Finished")
在这个修正的例子中,两个线程在获取 lock_a
之前先获取 lock_b
。这消除了循环等待条件,并防止了死锁。
现实中的死锁实例
- 操作系统: 当多个进程竞争内存、文件和 I/O 设备等资源时,操作系统可能会发生死锁。
- 数据库系统: 当多个事务互相等待释放数据库行或表上的锁时,数据库系统可能会发生死锁。