Python 多线程同步

Python 多线程同步是确保多个线程安全、有序地访问共享资源,避免数据竞争(Race Condition)和保证数据一致性的关键技术。Python 的 threading 模块提供了多种同步原语,每种都有其特定的适用场景。以下将详细介绍这些核心机制、使用方法和最佳实践。

一、线程锁(Lock)

线程锁是最基础、最常用的同步工具,用于确保在任意时刻只有一个线程能进入被保护的代码段(临界区)。

基本用法 : 通过 threading.Lock() 创建锁对象,使用 acquire() 获取锁,release() 释放锁。推荐使用 with 语句自动管理锁的获取和释放,避免因异常导致锁无法释放。

复制代码
import threading

# 创建锁
lock = threading.Lock()
shared_data = 0

def increment():
    global shared_data
    for _ in range(100000):
        # 获取锁
        lock.acquire()
        try:
            shared_data += 1
        finally:
            # 释放锁
            lock.release()

# 使用 with 语句更安全
def increment_safe():
    global shared_data
    for _ in range(100000):
        with lock:  # 自动获取和释放锁
            shared_data += 1

# 创建线程
t1 = threading.Thread(target=increment_safe)
t2 = threading.Thread(target=increment_safe)

t1.start()
t2.start()
t1.join()
t2.join()

print(f"最终结果: {shared_data}")  # 应该是 200000

可重入锁(RLock) : 普通锁(Lock)在同一线程内不可重入,连续调用 acquire() 会导致死锁。threading.RLock()(可重入锁)允许同一线程多次获取锁,通常用于递归函数或多次调用需要同步的同一方法。

复制代码
import threading

rlock = threading.RLock()

def recursive_func(n):
    with rlock:
        print(f"Entering Level {n}")
        if n > 0:
            recursive_func(n - 1)
        print(f"Exiting Level {n}")

print("Starting thread with recursive RLock example")
thread = threading.Thread(target=recursive_func, args=(3,))
thread.start()
thread.join()
print("Thread completed successfully")

二、信号量(Semaphore)

信号量用于控制同时访问某个共享资源的线程数量,其内部维护一个计数器。threading.Semaphore(value=N) 初始化时指定最大并发数,acquire() 减少计数,release() 增加计数。

复制代码
import threading
import time

# 最多允许3个线程同时访问
sem = threading.Semaphore(3)

def access_resource(thread_id):
    print(f"Thread {thread_id} is waiting to access")
    with sem:
        print(f"Thread {thread_id} has acquired the semaphore and is working")
        time.sleep(1)
        print(f"Thread {thread_id} is releasing the semaphore")

print("Starting threads with semaphore limit of 3...")
threads = []
for i in range(10):
    t = threading.Thread(target=access_resource, args=(i,))
    threads.append(t)
    t.start()
    time.sleep(0.1)  # 稍微延迟创建线程,便于观察

for t in threads:
    t.join()

print("All threads completed")

threading.BoundedSemaphore 是信号量的变体,可防止计数超过初始值。

python 复制代码
import threading
import time

# 创建一个有界信号量,最多允许3个线程同时访问
bounded_sem = threading.BoundedSemaphore(3)

def normal_access(thread_id):
    """正常使用有界信号量的函数"""
    print(f"Thread {thread_id} is waiting to access")
    with bounded_sem:
        print(f"Thread {thread_id} has acquired the bounded semaphore and is working")
        time.sleep(1)
        print(f"Thread {thread_id} is releasing the bounded semaphore")

def demonstrate_bounded_semaphore_error():
    """演示有界信号量防止过度释放的特性"""
    print("\n演示BoundedSemaphore防止过度释放的特性:")
    # 先获取信号量
    print("尝试获取信号量...")
    bounded_sem.acquire()
    print("信号量获取成功")
    
    # 正常释放一次
    print("正常释放一次信号量...")
    bounded_sem.release()
    print("信号量正常释放")
    
    # 尝试过度释放
    try:
        print("尝试过度释放信号量(这将引发ValueError异常)...")
        bounded_sem.release()  # 这里会抛出ValueError
        print("如果看到这条消息,说明有界信号量没有正常工作!")
    except ValueError as e:
        print(f"成功捕获异常: {e}")
        print("这证明BoundedSemaphore可以防止过度释放的情况发生")

print("=== 有界信号量(BoundedSemaphore)示例程序 ===")
print("1. 首先运行多线程正常访问的场景")

# 创建并启动多个线程
threads = []
for i in range(5):
    t = threading.Thread(target=normal_access, args=(i,))
    threads.append(t)
    t.start()
    time.sleep(0.2)  # 稍微延迟创建线程,便于观察

# 等待所有线程完成
for t in threads:
    t.join()

print("\n所有线程完成正常访问")

# 演示有界信号量的错误检测功能
demonstrate_bounded_semaphore_error()

print("\n=== 程序结束 ===")

三、条件变量(Condition)

条件变量用于复杂的线程间协调,允许一个或多个线程等待特定条件成立后再继续执行。它通常与一个锁关联,提供了 wait()notify()notify_all() 方法。

典型应用:生产者-消费者模型

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
生产者-消费者模型实现

本程序演示使用threading.Condition(条件变量)实现经典的生产者-消费者模型。
生产者线程生成随机数并放入固定大小的缓冲区,消费者线程从缓冲区取出数据。
当缓冲区满时,生产者等待;当缓冲区空时,消费者等待。
"""

import threading
import time
import random

# 共享缓冲区,用于存储生产者生产的物品
buffer = []

# 缓冲区最大容量
MAX_SIZE = 10

# 创建条件变量,用于线程间通信和同步
# Condition内部维护一个锁,提供wait()、notify()等方法进行线程协作
condition = threading.Condition()

class Producer(threading.Thread):
    """
    生产者类,继承自threading.Thread
    负责生成随机数并添加到缓冲区
    """
    def run(self):
        """
        线程运行方法
        使用条件变量确保线程安全的缓冲区访问
        """
        global buffer
        
        # 无限循环生产物品
        while True:
            # 获取条件变量的锁
            with condition:
                # 检查缓冲区是否已满
                # 注意:此处应使用while循环而非if语句,防止虚假唤醒
                while len(buffer) == MAX_SIZE:
                    print("Buffer full, producer waiting")
                    # 释放锁并等待,直到被其他线程通知
                    condition.wait()
                
                # 生产一个随机物品
                item = random.randint(1, 100)
                # 将物品添加到缓冲区
                buffer.append(item)
                print(f"Produced {item}, buffer: {buffer}")
                
                # 通知一个等待的消费者线程
                condition.notify()
            
            # 生产间隔随机时间,模拟生产过程
            time.sleep(random.random())

class Consumer(threading.Thread):
    """
    消费者类,继承自threading.Thread
    负责从缓冲区取出物品进行消费
    """
    def run(self):
        """
        线程运行方法
        使用条件变量确保线程安全的缓冲区访问
        """
        global buffer
        
        # 无限循环消费物品
        while True:
            # 获取条件变量的锁
            with condition:
                # 检查缓冲区是否为空
                # 同样使用while循环防止虚假唤醒
                while not buffer:
                    print("Buffer empty, consumer waiting")
                    # 释放锁并等待,直到被其他线程通知
                    condition.wait()
                
                # 从缓冲区取出第一个物品(FIFO原则)
                item = buffer.pop(0)
                print(f"Consumed {item}, buffer: {buffer}")
                
                # 通知一个等待的生产者线程
                condition.notify()
            
            # 消费间隔随机时间,模拟消费过程
            time.sleep(random.random())

# 程序入口点
if __name__ == "__main__":
    print("生产者-消费者模型演示程序启动...")
    print(f"缓冲区最大容量: {MAX_SIZE}")
    
    # 创建并启动生产者线程
    Producer().start()
    
    # 创建并启动消费者线程
    Consumer().start()
    
    print("线程已启动,按Ctrl+C停止程序")

wait_for(predicate, timeout) 方法可等待某个条件表达式为真,使代码更简洁。

四、事件(Event)

事件是一种简单的线程间通信机制,一个线程发出"事件已发生"的信号,其他线程等待该信号。通过 threading.Event() 创建事件对象,主要方法有 set()clear()wait()

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Threading.Event示例程序

本程序演示了Python中threading.Event的基本用法。
Event对象是线程间同步的一种机制,类似于一个信号标志,
可以控制线程的执行和等待状态。
"""

import threading
import time

# 创建Event对象,初始状态为False
event = threading.Event()

def waiter():
    """
    等待者线程函数
    
    该函数模拟一个需要等待某个事件发生才能继续执行的线程
    当event被设置(set)时,wait()方法才会解除阻塞
    """
    print("Waiter: waiting for event")
    # 阻塞当前线程,直到event被设置
    event.wait()  # 阻塞直到事件被设置
    print("Waiter: event received, proceeding")

def setter():
    """
    设置者线程函数
    
    该函数模拟一个在某个时刻触发事件的线程
    通过调用event.set()方法,将事件标志设置为True,
    唤醒所有等待该事件的线程
    """
    # 模拟耗时操作
    time.sleep(2)
    print("Setter: setting event")
    # 设置事件,唤醒所有等待的线程
    event.set()  # 设置事件,唤醒所有等待的线程

# 创建并启动等待者线程
threading.Thread(target=waiter).start()
# 创建并启动设置者线程
threading.Thread(target=setter).start()

五、其他同步工具与线程安全数据结构

  • 屏障(Barrier):使一组线程必须全部到达某个点后才能继续执行,适用于分阶段任务。
  • 线程本地数据(threading.local):为每个线程创建独立的变量副本,避免共享,从根本上解决同步问题。
  • 队列(Queue)queue 模块提供的 QueueLifoQueuePriorityQueue 是线程安全的,内部已实现锁机制,非常适合生产者-消费者模式,无需显式同步。

六、全局解释器锁(GIL)的影响与最佳实践

GIL 的影响 : Python 的全局解释器锁(GIL)确保同一时刻只有一个线程执行 Python 字节码。这意味着对于 CPU 密集型任务 ,多线程无法利用多核优势,性能提升有限甚至可能下降。但对于 I/O 密集型任务(如网络请求、文件读写),线程在等待 I/O 时会释放 GIL,因此多线程仍能有效提升并发性能。

多线程同步的最佳实践

  1. 选择合适的工具 :简单互斥用 Lock,控制并发数量用 Semaphore,复杂协调用 ConditionEvent,数据传递用 Queue
  2. 避免死锁 :确保多个锁的获取顺序一致;考虑使用带超时的 acquire(timeout);优先使用可重入锁(RLock)或高层抽象(如 Queue)。
  3. 最小化锁的持有时间:只将必须同步的代码放入临界区,尽快释放锁以提高并发性。
  4. 考虑使用线程安全的数据结构 :如 queue.Queue,可减少手动同步的复杂度。
  5. 对于CPU密集型任务考虑多进程 :使用 multiprocessing 模块绕过 GIL 限制,充分利用多核CPU。

总结

Python 提供了丰富的线程同步机制,从基础的 LockSemaphore 到更高级的 ConditionEventBarrier,以及线程安全的 Queue。选择何种机制取决于具体场景:保护简单共享变量可用 Lock;控制资源并发数用 Semaphore;实现线程间依赖和通知用 ConditionEvent;在线程间传递数据则优先使用 Queue。同时,必须注意 GIL 对多线程并行的限制,并遵循避免死锁、最小化锁范围等最佳实践,才能编写出高效、健壮的多线程程序。

相关推荐
阿蔹2 小时前
Python基础语法三---函数和数据容器
开发语言·python
2501_916766542 小时前
【Java】final关键字
java·开发语言
前端不太难2 小时前
RN 列表里的「局部状态」和「全局状态」边界
开发语言·javascript·ecmascript
3824278272 小时前
python3网络爬虫开发实战 第二版:绑定回调
开发语言·数据库·python
星月心城2 小时前
面试八股文-JavaScript(第五天)
开发语言·javascript·ecmascript
dagouaofei2 小时前
培训项目总结 PPT 工具对比评测,哪款更专业
python·powerpoint
wjs20242 小时前
PostgreSQL 时间/日期处理指南
开发语言
Hello eveybody2 小时前
用代码生成你的电影预告片(Python)
python
wniuniu_2 小时前
ceph心跳机制
开发语言·ceph·php