【Python多线程】

文章目录

  • 前言
  • 一、Python 等待event.set
  • 二、python race condition和lock使用
  • 使用锁 (Lock)
  • 三.python Dead Lock使用等综合运用
  • 总结


前言

这篇技术文章讨论了多线程编程中的几个重要概念。它首先介绍了等待事件的使用,并强调了避免使用“For Loop & Sleep”进行等待的重要性。接着,文档解释了竞态条件,并提供了处理共享资源的建议,即在使用共享资源时进行加锁和解锁操作,并且尽快释放锁。文档还简要提到了死锁和线程同步的问题,并通过图示说明了锁的获取和释放过程。最后,它介绍了信号量的概念,说明了它如何控制对共享资源的访问线程数量。


一、Python 等待event.set

我们来详细了解一下 Python threading 模块中的 event.set() 方法,并举例说明其用法。
event.set() 的作用
在 Python 的多线程编程中,threading.Event 对象是一种简单而有效的线程间通信机制。它内部维护一个标志(flag),这个标志初始时为 False。
event.set() 方法的主要作用是将这个内部标志设置为 True。
与 event.set() 相关的主要方法
理解 event.set() 的作用,还需要了解 Event 对象的其他几个关键方法:
• event.wait(timeout=None):
如果内部标志为 True,则此方法立即返回 True。
如果内部标志为 False,则此方法会阻塞当前线程,直到其他线程调用 event.set() 将标志设置为 True。
如果设置了 timeout 参数(一个浮点数,表示秒数),则线程最多阻塞 timeout 秒。如果在超时前标志被设置为 True,则返回 True;如果超时后标志仍为 False,则返回 False。
• event.clear(): 将内部标志重新设置为 False。
• event.is_set(): 返回内部标志的当前状态(True 或 False),不会阻塞线程。
event.set() 的核心用途
event.set() 通常用于:

  1. 通知其他线程某个条件已经满足或某个事件已经发生。 一个线程可以等待一个 Event 对象,而另一个线程在适当的时候调用 event.set() 来唤醒等待的线程。
  2. 实现线程间的同步。 确保某个操作在另一个操作完成后才开始执行。

代码如下(示例):
下面通过一个例子来演示 event.set() 的使用:

import threading
import time

# 创建一个 Event 对象
event = threading.Event()

def worker_thread(name):
    print(f"线程 {name}: 开始工作,等待事件...")
    # 等待事件被设置 (event.set() 被调用)
    event.wait()
    print(f"线程 {name}: 事件已被触发,继续执行...")
    time.sleep(1) # 模拟一些工作
    print(f"线程 {name}: 工作完成。")

def control_thread():
    print("控制线程: 启动,将在3秒后触发事件。")
    time.sleep(3)
    print("控制线程: 现在触发事件!")
    event.set() # 设置事件标志为 True,唤醒等待的线程

if __name__ == "__main__":
    # 创建并启动工作线程
    thread1 = threading.Thread(target=worker_thread, args=("Worker-1",))
    thread2 = threading.Thread(target=worker_thread, args=("Worker-2",))

    thread1.start()
    thread2.start()

    # 创建并启动控制线程
    controller = threading.Thread(target=control_thread)
    controller.start()

    # 等待所有线程完成
    thread1.join()
    thread2.join()
    controller.join()

    print("所有线程执行完毕。")

小结:
threading.Event 和其 set() 方法是 Python 多线程编程中一种非常实用的同步原语。它允许一个或多个线程等待某个特定条件的发生,而另一个线程则可以在条件满足时通过调用 set() 来通知这些等待的线程。这使得线程间的协作和流程控制更加灵活和可控。
希望这个解释和例子能帮助你理解 event.set() 的用法!

二、python race condition和lock使用

我们来讨论 Python 中的“竞争条件”(Race Condition)以及如何通过例子来理解它。
什么是竞争条件 (Race Condition)?
竞争条件发生在多个线程或进程并发地访问和修改共享数据时,最终的结果取决于这些线程或进程执行操作的相对顺序。由于操作系统调度线程/进程的方式具有不确定性,这种相对顺序往往是不可预测的,从而可能导致程序出现意料之外的、不一致的或错误的行为。
简单来说,就是当多个执行路径(线程/进程)试图同时“竞争”去操作同一个资源时,如果缺乏适当的同步机制,就会导致混乱。
竞争条件发生的关键要素:

  1. 共享资源 (Shared Resource): 多个线程/进程共同访问或修改的数据或对象。
  2. 并发访问 (Concurrent Access): 多个线程/进程在同一时间段内尝试操作这个共享资源。
  3. 修改操作 (Modification): 至少有一个操作是修改共享资源的状态。如果所有操作都只是读取,通常不会产生竞争条件(但可能会有其他并发问题,如数据不一致)。
  4. 缺乏同步 (Lack of Synchronization): 没有使用锁(Locks)、信号量(Semaphores)、事件(Events)或其他同步原语来协调对共享资源的访问。
    Python 中的竞争条件示例
    在 Python 中,由于全局解释器锁(GIL)的存在,单个 Python 进程内的多线程在执行 CPU 密集型任务时并不能实现真正的并行(同时在多个核心上运行)。但是,GIL 仍然允许在 I/O 密集型操作或执行某些内置函数/C扩展时释放锁,从而让其他线程运行。这意味着即使有 GIL,竞争条件依然是 Python 多线程编程中需要重点关注的问题,尤其是在修改共享的可变对象时。

代码如下(示例):

下面是一个经典的例子,演示了多线程在没有同步的情况下修改共享变量时如何产生竞争条件:

import threading
import time

# 共享变量
shared_counter = 0
# 迭代次数
ITERATIONS = 100000

def increment_counter():
    global shared_counter
    for _ in range(ITERATIONS):
        # 这里是竞争条件发生的核心区域
        current_value = shared_counter
        # 模拟一些处理时间,增加竞争条件发生的概率
        # time.sleep(0.0000001) # 即使是很短的时间也可能导致问题
        shared_counter = current_value + 1

if __name__ == "__main__":
    threads = []
    num_threads = 5

    print(f"初始计数器值: {shared_counter}")
    print(f"每个线程将计数器增加 {ITERATIONS} 次")
    print(f"理论上的最终计数器值应为: {num_threads * ITERATIONS}")

    for i in range(num_threads):
        thread = threading.Thread(target=increment_counter)
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

    print(f"实际的最终计数器值: {shared_counter}")
    print(f"与理论值的差额: {(num_threads * ITERATIONS) - shared_counter}")

代码解释与竞争条件的发生:

  1. shared_counter 是一个全局变量,将被多个线程共享和修改。
  2. increment_counter 函数的任务是将 shared_counter 增加 ITERATIONS 次。
  3. 关键的竞争发生在以下两行:
    Python
    current_value = shared_counter # 1. 读取共享变量
    (此时,其他线程可能已经修改了 shared_counter)
    shared_counter = current_value + 1 # 2. 基于旧值计算并写回
    想象一下场景:
    o 线程 A 读取 shared_counter 的值为 0,所以 current_value 为 0。
    o 在线程 A 计算 current_value + 1 之前,操作系统将 CPU 切换到线程 B。
    o 线程 B 也读取 shared_counter 的值,此时仍然是 0,所以线程 B 的 current_value 也为 0。
    o 线程 B 计算 current_value + 1 得到 1,并将 shared_counter 更新为 1。
    o CPU 切换回线程 A。线程 A 继续执行,它基于它之前读取的 current_value (为 0) 计算 0 + 1 得到 1,并将 shared_counter 更新为 1。
    结果:两个线程都执行了增加操作,但 shared_counter 只从 0 增加到了 1,而不是预期的 2。一次增加操作丢失了!当有大量线程和大量迭代时,这种丢失会累积,导致最终结果远小于预期值。
    如何避免竞争条件:使用锁 (Lock)
    为了解决这个问题,我们需要引入同步机制,确保在任何时候只有一个线程可以修改共享资源。threading.Lock 是一个常用的同步原语。
    如何避免竞争条件:

使用锁 (Lock)

为了解决这个问题,我们需要引入同步机制,确保在任何时候只有一个线程可以修改共享资源。threading.Lock 是一个常用的同步原语。

import threading
import time

# 共享变量
shared_counter = 0
ITERATIONS = 100000

# 创建一个锁对象
lock = threading.Lock()

def increment_counter_safe():
    global shared_counter
    for _ in range(ITERATIONS):
        # 在访问共享资源前获取锁
        lock.acquire()
        try:
            # 现在这部分代码是互斥的,只有一个线程能执行
            current_value = shared_counter
            shared_counter = current_value + 1
        finally:
            # 确保在任何情况下都释放锁
            lock.release()

if __name__ == "__main__":
    threads = []
    num_threads = 5

    print(f"初始计数器值: {shared_counter}")
    print(f"每个线程将计数器增加 {ITERATIONS} 次")
    print(f"理论上的最终计数器值应为: {num_threads * ITERATIONS}")

    for i in range(num_threads):
        # 使用带锁的函数
        thread = threading.Thread(target=increment_counter_safe)
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

    print(f"使用锁后,实际的最终计数器值: {shared_counter}")
    print(f"与理论值的差额: {(num_threads * ITERATIONS) - shared_counter}")

    # 也可以使用 with 语句来自动管理锁的获取和释放,更推荐
    shared_counter = 0 # 重置计数器进行演示
    threads = []
    print("\n使用 'with lock' 语句的演示:")

    def increment_counter_with_statement():
        global shared_counter
        for _ in range(ITERATIONS):
            with lock: # 自动获取和释放锁
                current_value = shared_counter
                shared_counter = current_value + 1

    for i in range(num_threads):
        thread = threading.Thread(target=increment_counter_with_statement)
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

    print(f"使用 'with lock' 后,实际的最终计数器值: {shared_counter}")
    print(f"与理论值的差额: {(num_threads * ITERATIONS) - shared_counter}")

使用锁后的代码解释:

  1. lock = threading.Lock(): 创建了一个锁对象。
  2. lock.acquire(): 线程在进入“临界区”(访问共享资源的代码块)之前尝试获取锁。如果锁已被其他线程持有,则当前线程会阻塞,直到锁被释放。
  3. try…finally…lock.release(): 这是一个非常重要的模式。lock.release() 用于释放锁,以便其他等待的线程可以获取它。将 lock.release() 放在 finally 块中确保即使在 try 块中发生异常,锁也总是会被释放,避免死锁。
  4. with lock:: 这是一个更 Pythonic 和安全的方式来使用锁。进入 with 语句块时会自动调用 lock.acquire(),退出语句块时(无论是正常退出还是发生异常)会自动调用 lock.release()。强烈推荐使用这种方式。
    通过使用锁,我们确保了 current_value = shared_counter 和 shared_counter = current_value + 1 这两步操作作为一个原子单元执行,不会被其他线程打断。因此,每次增加操作都能正确完成,最终的 shared_counter 值会符合预期。
    小结:
    竞争条件是并发编程中一个常见且棘手的问题。理解其发生的原因,并学会使用适当的同步原语(如锁)来保护共享资源,是编写健壮、正确的并发程序的关键。在 Python 中,即使有 GIL,对于共享可变状态的操作,仍然需要警惕竞争条件并采取同步措施。

三.python Dead Lock使用等综合运用

我们来讨论 Python 中的“死锁”(Deadlock)现象,并举例说明。
什么是死锁 (Deadlock)?
死锁是并发编程中一种非常棘手的问题。当两个或多个线程(或进程)无限期地等待一个只有其他等待中的线程才能释放的资源时,就会发生死锁。这导致这些线程都无法继续执行,程序陷入停滞状态。
想象一下这个场景:
• 线程 A 持有资源 X,并等待资源 Y。
• 线程 B 持有资源 Y,并等待资源 X。
在这种情况下,线程 A 永远等不到资源 Y(因为线程 B 持有它并且在等待资源 X),线程 B 也永远等不到资源 X(因为线程 A 持有它并且在等待资源 Y)。双方都在互相等待对方先释放资源,从而形成死锁。
发生死锁的四个必要条件 (Coffman conditions):
只有当以下四个条件同时满足时,才可能发生死锁:

  1. 互斥 (Mutual Exclusion): 至少有一个资源必须以非共享模式持有,即一次只有一个线程可以使用该资源。如果其他线程请求该资源,则请求线程必须等待直到资源被释放。 (在 Python 中,threading.Lock 就是互斥的)
  2. 占有并等待 (Hold and Wait): 一个线程必须至少持有一个资源,并且正在等待获取其他线程当前持有的额外资源。
  3. 不可抢占 (No Preemption): 资源不能被强制从持有它的线程中抢占。资源只能由持有它的线程在完成任务后自愿释放。
  4. 循环等待 (Circular Wait): 必须存在一组等待资源的线程 {T0, T1, …, Tn},使得 T0 正在等待一个由 T1 持有的资源,T1 正在等待一个由 T2 持有的资源,…,Tn-1 正在等待一个由 Tn 持有的资源,而 Tn 正在等待一个由 T0 持有的资源。

代码如下(示例):
下面是一个经典的死锁例子,涉及两个线程和两个锁:

import threading
import time

# 创建两个锁对象
lock_a = threading.Lock()
lock_b = threading.Lock()

def thread_1_logic():
    print("线程 1: 尝试获取 lock_a...")
    lock_a.acquire()
    print("线程 1: 已获取 lock_a。")
    print("线程 1: 休眠 1 秒,模拟一些工作...")
    time.sleep(1) # 给线程 2 足够的时间去获取 lock_b

    print("线程 1: 尝试获取 lock_b...")
    lock_b.acquire() # 此处可能发生死锁
    print("线程 1: 已获取 lock_b。")

    # 释放锁 (理论上,如果能执行到这里)
    lock_b.release()
    lock_a.release()
    print("线程 1: 已释放所有锁。")

def thread_2_logic():
    print("线程 2: 尝试获取 lock_b...")
    lock_b.acquire()
    print("线程 2: 已获取 lock_b。")
    print("线程 2: 休眠 1 秒,模拟一些工作...")
    time.sleep(1) # 给线程 1 足够的时间去获取 lock_a

    print("线程 2: 尝试获取 lock_a...")
    lock_a.acquire() # 此处可能发生死锁
    print("线程 2: 已获取 lock_a。")

    # 释放锁 (理论上,如果能执行到这里)
    lock_a.release()
    lock_b.release()
    print("线程 2: 已释放所有锁。")

if __name__ == "__main__":
    print("启动死锁演示...")

    t1 = threading.Thread(target=thread_1_logic)
    t2 = threading.Thread(target=thread_2_logic)

    t1.start()
    t2.start()

    t1.join() # 主线程等待 t1 结束
    t2.join() # 主线程等待 t2 结束

    print("死锁演示结束 (如果程序能正常结束的话)。")

代码解释与死锁的发生:

  1. 我们创建了两个锁:lock_a 和 lock_b。
  2. thread_1_logic:
    o 首先尝试获取 lock_a。
    o 获取 lock_a 后,它会休眠一小段时间,这是为了给 thread_2 一个机会运行并获取 lock_b。
    o 然后,它尝试获取 lock_b。
  3. thread_2_logic:
    o 首先尝试获取 lock_b。
    o 获取 lock_b 后,它也会休眠一小段时间,给 thread_1 机会获取 lock_a。
    o 然后,它尝试获取 lock_a。
    死锁如何发生:
    一种可能的执行顺序导致死锁:
  4. 线程 1 启动,获取 lock_a。 (线程 1 持有 lock_a)
  5. 操作系统切换到 线程 2。
  6. 线程 2 启动,获取 lock_b。 (线程 2 持有 lock_b)
  7. 线程 1 继续执行 (从休眠中唤醒),尝试获取 lock_b。但是 lock_b 正被 线程 2 持有,所以 线程 1 进入等待状态,等待 lock_b 被释放。
  8. 线程 2 继续执行 (从休眠中唤醒),尝试获取 lock_a。但是 lock_a 正被 线程 1 持有,所以 线程 2 进入等待状态,等待 lock_a 被释放。
    现在:
    • 线程 1 持有 lock_a 并等待 lock_b。
    • 线程 2 持有 lock_b 并等待 lock_a。
    这就形成了一个循环等待,两个线程都无法继续执行,程序卡住,这就是死锁。如果你运行上面的代码,很大概率会看到程序打印出获取第一个锁的消息后就再也没有输出了,并且程序不会自行结束。
    如何避免死锁?
    避免死锁有几种常见的策略:
  9. 锁排序 (Lock Ordering / Resource Hierarchy): 这是最常用也是最有效的预防死锁的方法。要求所有线程都以相同的固定顺序获取锁。 例如,在上面的例子中,如果我们规定所有线程都必须先获取 lock_a,然后再获取 lock_b,那么死锁就不会发生。
    线程 1: 获取 lock_a -> 获取 lock_b
    线程 2: 也必须是 获取 lock_a -> 获取 lock_b 这样,如果 线程 1 获取了 lock_a,线程 2 在尝试获取 lock_a 时就会阻塞,直到 线程 1 释放 lock_a (以及 lock_b)。它不会先去获取 lock_b 从而造成循环等待。
    修改后的 thread_2_logic (遵循锁排序):
def thread_2_logic_fixed():
    print("线程 2 (修复后): 尝试获取 lock_a...")
    lock_a.acquire() # 先获取 lock_a
    print("线程 2 (修复后): 已获取 lock_a。")
    print("线程 2 (修复后): 休眠 1 秒...")
    time.sleep(1)

    print("线程 2 (修复后): 尝试获取 lock_b...")
    lock_b.acquire() # 再获取 lock_b
    print("线程 2 (修复后): 已获取 lock_b。")

    lock_b.release()
    lock_a.release()
    print("线程 2 (修复后): 已释放所有锁。")
  1. 尝试获取锁并设置超时 (Lock Timeout / Try Lock): 当线程尝试获取锁时,可以设置一个超时时间。如果在超时时间内未能获取锁,线程就放弃获取,并可以执行一些回退操作(例如释放已持有的锁,稍后再试)。 threading.Lock.acquire() 方法有一个 timeout 参数:
if lock_a.acquire(timeout=1): # 尝试获取锁,最多等待1秒
    # 成功获取锁
    try:
        if lock_b.acquire(timeout=1):
            # ...
        else:
            # 获取 lock_b 失败,可能需要释放 lock_a
            print("线程 X: 获取 lock_b 超时")
    finally:
        lock_a.release()
else:
    # 获取 lock_a 失败
    print("线程 X: 获取 lock_a 超时")

这种方法会使代码更复杂,但可以避免无限期等待。
3. 死锁检测和恢复 (Deadlock Detection and Recovery): 一些更复杂的系统可能会有专门的机制来检测死锁的发生(例如,通过分析资源分配图)。一旦检测到死锁,系统可以采取措施来解除死锁,比如终止其中一个或多个线程,或者抢占资源(这违反了不可抢占条件,但有时是必要的)。Python 的 threading 模块本身不提供内置的死锁检测机制。
4. 减少锁的持有时间,避免嵌套锁: 尽量缩短持有锁的时间,并且尽可能避免在一个锁内部去获取另一个锁(即嵌套锁)。如果必须使用嵌套锁,锁排序就变得至关重要。
总结
死锁是并发编程中的一个严重问题,它会导致程序挂起。理解发生死锁的条件以及如何通过锁排序等策略来预防死锁,对于编写可靠的多线程 Python 程序至关重要。在设计并发逻辑时,务必仔细考虑资源获取的顺序。
python 线程同步使用,并举例?
显示思路
好的,Python 的 threading 模块提供了多种线程同步机制,用于解决多线程并发执行时可能出现的资源竞争、数据不一致以及执行顺序协调等问题。
为什么需要线程同步?
当多个线程共享数据或资源时,如果不对它们的访问进行控制,就可能发生:

  1. 竞争条件 (Race Conditions): 多个线程几乎同时修改共享数据,最终结果取决于线程执行的不可预测的顺序,导致结果错误。
  2. 数据损坏 (Data Corruption): 一个线程正在修改数据时,另一个线程可能读取到不一致或不完整的数据。
  3. 死锁 (Deadlocks): 两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行。
    线程同步机制就是用来确保在任何时刻,只有一个线程(或有限数量的线程)可以访问特定的代码段(称为临界区)或共享资源,或者协调线程间的执行顺序。
    Python threading 模块中主要的同步原语:
  4. Lock (互斥锁)
  5. RLock (可重入锁)
  6. Event (事件)
  7. Condition (条件变量)
  8. Semaphore / BoundedSemaphore (信号量)
  9. Barrier (栅栏/屏障 - Python 3.2+)
    下面我们逐个介绍并举例:

  1. Lock (互斥锁)
    • 作用: 实现互斥访问,确保同一时间只有一个线程能进入临界区(持有锁的代码段)。
    • 方法:
    acquire(blocking=True, timeout=-1): 获取锁。如果 blocking 为 True (默认),线程会阻塞直到获取锁。如果设置了 timeout (正数),则最多阻塞 timeout 秒,超时仍未获取则返回 False,否则返回 True。如果 blocking 为 False,则不阻塞,尝试获取,能获取则返回 True,否则返回 False。
    release(): 释放锁。只有持有锁的线程才能释放锁。尝试释放未持有的锁会引发 RuntimeError。
    locked(): 如果锁已被获取,则返回 True,否则返回 False。
    • 最佳实践: 使用 with 语句可以自动管理锁的获取和释放,更安全,推荐使用。

示例 (解决竞争条件):

import threading
import time

shared_variable = 0
ITERATIONS = 100000

# 创建一个锁对象
lock = threading.Lock()

def increment_counter():
    global shared_variable
    for _ in range(ITERATIONS):
        # 方式一:显式获取和释放
        # lock.acquire()
        # try:
        #     temp = shared_variable
        #     temp += 1
        #     shared_variable = temp
        # finally:
        #     lock.release()

        # 方式二:使用 with 语句 (推荐)
        with lock:
            temp = shared_variable
            temp += 1
            shared_variable = temp

def decrement_counter():
    global shared_variable
    for _ in range(ITERATIONS):
        with lock:
            temp = shared_variable
            temp -= 1
            shared_variable = temp

if __name__ == "__main__":
    thread1 = threading.Thread(target=increment_counter)
    thread2 = threading.Thread(target=decrement_counter)

    start_time = time.time()
    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()
    end_time = time.time()

    print(f"最终共享变量的值: {shared_variable}") # 期望值为 0
    print(f"耗时: {end_time - start_time:.4f} 秒")

说明: 如果没有 lock,shared_variable 的最终值将不确定。with lock: 确保了对 shared_variable 的读-改-写操作是原子的。

  1. RLock (可重入锁 / 递归锁)
    • 作用: RLock 允许同一个线程多次获取同一个锁而不会发生死锁。一个线程每 acquire() 一次,内部的递归层级计数就会增加;每 release() 一次,计数就会减少。只有当递归层级降为零时,锁才真正被释放,其他线程才能获取它。
    • 场景: 当一个函数或方法可能递归调用自身,或者一个函数调用了另一个也需要相同锁的函数时,使用 RLock 可以避免死锁。
    示例:
import threading

# 创建一个可重入锁
rlock = threading.RLock()
shared_resource = []

def recursive_function(depth):
    if depth <= 0:
        return

    with rlock: # 同一个线程可以多次获取 rlock
        print(f"线程 {threading.current_thread().name}: 获取 rlock, 深度 {depth}")
        shared_resource.append(depth)
        time.sleep(0.1)
        recursive_function(depth - 1)
        print(f"线程 {threading.current_thread().name}: 释放 rlock, 深度 {depth}")

if __name__ == "__main__":
    thread_r = threading.Thread(target=recursive_function, args=(3,))
    thread_r.start()
    thread_r.join()
    print(f"共享资源: {shared_resource}")
    # 如果用普通的 Lock,第二次 acquire 就会死锁

  1. Event (事件)
    • 作用: 一种简单的线程间通信机制,一个线程可以发出“事件”信号,而其他线程则等待该事件。内部维护一个标志,初始为 False。
    • 方法:
    set(): 将内部标志设置为 True,并唤醒所有等待该事件的线程。
    clear(): 将内部标志重置为 False。
    wait(timeout=None): 如果内部标志为 False,则阻塞当前线程,直到标志变为 True (被其他线程 set()) 或超时。如果标志已经是 True,则立即返回 True。超时返回 False。
    is_set(): 返回内部标志的当前状态。
    示例 (一个线程等待另一个线程完成某个操作):
import threading
import time

event = threading.Event()

def worker_task():
    print("工作线程: 正在处理一些事情...")
    time.sleep(3) # 模拟耗时操作
    print("工作线程: 处理完毕,设置事件!")
    event.set() # 发出事件信号

def listener_task():
    print("监听线程: 等待事件...")
    event.wait() # 阻塞直到 event.set() 被调用
    print("监听线程: 收到事件信号,继续执行!")

if __name__ == "__main__":
    t_worker = threading.Thread(target=worker_task)
    t_listener = threading.Thread(target=listener_task)

    t_listener.start() # 先启动监听者
    time.sleep(0.1) # 确保监听者先进入 wait 状态
    t_worker.start()

    t_worker.join()
    t_listener.join()
    print("所有任务完成。")
  1. Condition (条件变量)
    • 作用: Condition 对象通常与一个 Lock (或 RLock) 相关联。它允许一个或多个线程等待,直到另一个线程通知它们某个条件已经满足。比 Event 更复杂,可以精确控制唤醒哪些等待的线程。
    • 场景: 经典的生产者-消费者问题。
    • 方法:
    o acquire(*args) / release(): 获取/释放关联的锁。
    o wait(timeout=None): 释放关联的锁,并阻塞当前线程,直到被 notify() 或 notify_all() 唤醒,或者超时。一旦唤醒,它会重新获取锁,然后 wait() 返回。
    o notify(n=1): 唤醒最多 n 个正在等待该条件的线程。默认唤醒一个。必须在持有锁的情况下调用。
    o notify_all(): 唤醒所有正在等待该条件的线程。必须在持有锁的情况下调用。
    示例 (简单的生产者-消费者模型):
import threading
import time
import random

condition = threading.Condition() # 默认会创建一个 Lock
items = []
MAX_ITEMS = 5

def producer():
    global items
    for i in range(10):
        with condition: # 获取关联的锁
            while len(items) == MAX_ITEMS:
                print("生产者: 缓冲区已满,等待...")
                condition.wait() # 等待,释放锁;被唤醒后重新获取锁
            item = f"物品-{i}"
            items.append(item)
            print(f"生产者: 生产了 {item} (当前数量: {len(items)})")
            condition.notify() # 通知一个等待的消费者
        time.sleep(random.uniform(0.1, 0.5))

def consumer():
    global items
    for i in range(10):
        with condition:
            while not items:
                print("消费者: 缓冲区为空,等待...")
                condition.wait()
            item = items.pop(0)
            print(f"消费者: 消费了 {item} (剩余数量: {len(items)})")
            condition.notify() # 通知可能在等待的生产者(如果缓冲区之前是满的)
        time.sleep(random.uniform(0.1, 0.8))

if __name__ == "__main__":
    t_prod = threading.Thread(target=producer)
    t_cons = threading.Thread(target=consumer)

    t_prod.start()
    t_cons.start()

    t_prod.join()
    t_cons.join()
    print("生产消费完成。")

  1. Semaphore / BoundedSemaphore (信号量)
    • 作用: 维护一个计数器。acquire() 会使计数器减一,release() 会使计数器加一。如果计数器为零,acquire() 会阻塞,直到其他线程调用 release() 使计数器大于零。
    Semaphore: 计数器可以超过初始值(通过 release())。
    BoundedSemaphore: 计数器不允许超过其初始值。尝试 release() 一个已满的 BoundedSemaphore 会引发 ValueError。
    • 场景: 控制对有限数量资源的并发访问(例如,数据库连接池,限制同时下载文件的线程数)。
    示例 (限制同时访问某个资源的线程数量):
import threading
import time
import random

# 创建一个信号量,允许最多3个线程同时访问资源
semaphore = threading.BoundedSemaphore(3) # 使用 BoundedSemaphore 更安全

def access_resource(thread_id):
    print(f"线程 {thread_id}: 尝试访问资源...")
    with semaphore: # acquire()release() 自动管理
        print(f"线程 {thread_id}: 已获取信号量,正在访问资源...")
        time.sleep(random.uniform(1, 3)) # 模拟资源使用
        print(f"线程 {thread_id}: 访问完毕,释放信号量。")

if __name__ == "__main__":
    threads = []
    for i in range(7): # 创建7个线程,但只有3个能同时访问
        thread = threading.Thread(target=access_resource, args=(i,))
        threads.append(thread)
        thread.start()
        time.sleep(0.1) # 稍微错开启动,方便观察

    for thread in threads:
        thread.join()
    print("所有线程任务完成。")

  1. Barrier (栅栏/屏障 - Python 3.2+)
    • 作用: 允许指定数量的线程相互等待,直到所有线程都到达了某个屏障点,然后它们才能同时继续执行。
    • 方法:
    Barrier(parties, action=None, timeout=None): parties 是需要到达屏障的线程数。action 是一个可选的可调用对象,当所有线程都到达屏障后,在它们被释放之前,会由其中一个线程单独执行。timeout 是 wait() 的默认超时时间。
    wait(timeout=None): 线程调用此方法表示已到达屏障。它会阻塞,直到所有 parties 个线程都调用了 wait()。返回值是一个从 0到 parties-1 的整数,对每个线程是唯一的。如果提供了 action,其中一个线程会在其他线程被释放前执行它。如果超时,会引发 BrokenBarrierError。
    reset(): 将屏障恢复到初始的、空的状态。
    abort(): 使屏障进入损坏状态。任何在 wait() 中阻塞或之后调用 wait() 的线程都会引发 BrokenBarrierError。
    parties: 需要的线程数。
    n_waiting: 当前在屏障处等待的线程数。
    broken: 如果屏障处于损坏状态,则为 True。
    示例 (多个线程到达某一点后才一起继续):
    Python

import threading
import time
import random

PARTIES = 3
barrier = threading.Barrier(PARTIES, timeout=10) # 3个线程参与,超时10秒

def race_participant(participant_id):
    print(f"选手 {participant_id}: 准备就绪...")
    time.sleep(random.uniform(0.5, 2)) # 模拟准备时间
    try:
        print(f"选手 {participant_id}: 到达起跑线,等待发令...")
        index = barrier.wait() # 等待其他选手
        print(f"选手 {participant_id} (序号 {index}): 开始跑!")
    except threading.BrokenBarrierError:
        print(f"选手 {participant_id}: 比赛取消或超时!")
    except threading.TimeoutError: # Python 3.2+ for Barrier timeout in wait
        print(f"选手 {participant_id}: 等待发令超时!")


if __name__ == "__main__":
    threads = []
    for i in range(PARTIES):
        thread = threading.Thread(target=race_participant, args=(i,))
        threads.append(thread)
        thread.start()

    # 如果想演示屏障损坏,可以在某个线程调用 wait 之前调用 barrier.abort()
    # time.sleep(1)
    # if random.choice([True, False]):
    # print("裁判:出现意外,比赛中止!")
    #     barrier.abort()


    for thread in threads:
        thread.join()
    print("比赛结束。")

    # 演示 reset
    print("\n--- 准备第二轮比赛 ---")
    barrier.reset() # 重置屏障以便复用
    threads_round2 = []
    for i in range(PARTIES):
        thread = threading.Thread(target=race_participant, args=(i + PARTIES,))
        threads_round2.append(thread)
        thread.start()

    for thread in threads_round2:
        thread.join()
    print("第二轮比赛结束。")

总结

选择哪种同步原语取决于具体的同步需求:
• 互斥访问共享资源: Lock 或 RLock。
• 简单的信号通知: Event。
• 复杂的条件等待与通知: Condition。
• 控制对资源的并发访问数量: Semaphore。
• 多线程集合点同步: Barrier。
合理使用这些同步机制是编写健壮、高效的多线程 Python程序的关键。同时,也要注意避免过度同步导致性能下降,以及不当使用同步原语可能引入的死锁问题。
另外在等待事件中Don’t use ''for loop and sleep"for wait…
感谢大家的点赞收藏转发和关注,谢谢你。

你可能感兴趣的:(Python,python)