【Python】锁(Lock)

第一章:并发编程的基石——共享资源与互斥原理

在深入探究Python中的Lock机制之前,我们必须首先理解其产生的根本原因和所要解决的核心问题。这涉及到计算机科学中最基础的并发编程概念,以及多任务环境下对共享资源进行安全访问的挑战。

1.1 CPU、进程、线程的本质与演进

要理解并发,我们首先要从计算机硬件的基石——中央处理器(CPU)以及其上运行的软件实体——进程和线程说起。

1.1.1 CPU的核心与多核架构

早期计算机的CPU只有一个处理核心,同一时刻只能执行一个指令流。为了提高效率,操作系统通过“时间片轮转”等方式,让多个程序轮流使用CPU,从而在宏观上实现了“同时”运行的假象,这就是“并发”。

随着技术发展,CPU制造商开始在单个物理芯片上集成多个处理核心,形成了多核CPU。每个核心都可以独立执行指令流。这意味着,在一个四核CPU上,理论上可以真正地同时执行四个不同的指令流,这就是“并行”。

1.1.2 进程的独立王国

在操作系统层面,**进程(Process)**是程序的一次执行实例。每个进程都拥有独立的内存地址空间、文件句柄、系统资源等。进程之间默认是隔离的,一个进程的崩溃通常不会直接影响到其他进程。操作系统负责管理和调度这些进程,为它们分配CPU时间、内存等资源。

import os # 导入os模块,用于操作系统交互 # This line imports the 'os' module, which provides functions for interacting with the operating system.
import time # 导入time模块,用于时间相关操作 # This line imports the 'time' module, which provides time-related functions.

# 模拟一个进程的函数 # This function simulates the work of a process.
def process_work(name): # 定义一个名为process_work的函数,接受一个name参数 # Defines a function named 'process_work' that takes a 'name' argument.
    pid = os.getpid() # 获取当前进程的ID # Gets the ID of the current process.
    print(f"进程 {
     
     name} (PID: {
     
     pid}) 正在启动...") # 打印进程启动信息 # Prints a message indicating the process is starting.
    for i in range(3): # 循环3次 # Loops 3 times.
        print(f"进程 {
     
     name} (PID: {
     
     pid}) 正在执行任务 {
     
     i+1}...") # 打印任务执行信息 # Prints a message indicating the process is executing task.
        time.sleep(1) # 暂停1秒,模拟耗时操作 # Pauses for 1 second, simulating a time-consuming operation.
    print(f"进程 {
     
     name} (PID: {
     
     pid}) 完成。") # 打印进程完成信息 # Prints a message indicating the process is complete.

# if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
#    print("主进程开始。") # 打印主进程开始信息 # Prints a message indicating the main process has started.
#    # 在这里可以启动子进程,但我们这里只是展示进程的概念 # Child processes could be started here, but this example only illustrates the concept of a process.
#    process_work("A") # 调用函数,模拟进程A的工作 # Calls the function to simulate the work of process A.
#    process_work("B") # 调用函数,模拟进程B的工作 # Calls the function to simulate the work of process B.
#    print("主进程结束。") # 打印主进程结束信息 # Prints a message indicating the main process has ended.

(由于上述代码在一个脚本中顺序执行,它们在逻辑上代表了进程A和进程B的独立工作流程,而不是真正的并发进程启动。真正的进程并发需要multiprocessing模块,但此处旨在解释进程的隔离性。)

1.1.3 线程的轻量化执行单元

在单个进程内部,为了进一步提高程序的并发性,引入了**线程(Thread)**的概念。线程是进程内的一个执行流,是CPU调度的基本单位。与进程不同的是,同一个进程内的所有线程共享该进程的内存地址空间、文件句柄等资源。

这种共享机制使得线程间的通信和数据交换变得更加高效,但也引入了新的问题:当多个线程同时访问和修改同一块共享内存时,可能会出现数据不一致的错误。

import threading # 导入threading模块,用于多线程编程 # This line imports the 'threading' module, which is used for multi-threaded programming.
import time # 导入time模块,用于时间相关操作 # This line imports the 'time' module, which provides time-related functions.

# 模拟一个线程的函数 # This function simulates the work of a thread.
def thread_work(name): # 定义一个名为thread_work的函数,接受一个name参数 # Defines a function named 'thread_work' that takes a 'name' argument.
    thread_id = threading.get_ident() # 获取当前线程的唯一标识符 # Gets the unique identifier of the current thread.
    print(f"线程 {
     
     name} (ID: {
     
     thread_id}) 正在启动...") # 打印线程启动信息 # Prints a message indicating the thread is starting.
    for i in range(2): # 循环2次 # Loops 2 times.
        print(f"线程 {
     
     name} (ID: {
     
     thread_id}) 正在执行任务 {
     
     i+1}...") # 打印任务执行信息 # Prints a message indicating the thread is executing task.
        time.sleep(0.5) # 暂停0.5秒,模拟耗时操作 # Pauses for 0.5 seconds, simulating a time-consuming operation.
    print(f"线程 {
     
     name} (ID: {
     
     thread_id}) 完成。") # 打印线程完成信息 # Prints a message indicating the thread is complete.

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    print("主线程开始。") # 打印主线程开始信息 # Prints a message indicating the main thread has started.
    
    thread1 = threading.Thread(target=thread_work, args=("T1",)) # 创建一个线程实例,目标是thread_work函数,参数是("T1",) # Creates a thread instance, targeting the 'thread_work' function with argument "T1".
    thread2 = threading.Thread(target=thread_work, args=("T2",)) # 创建另一个线程实例,目标是thread_work函数,参数是("T2",) # Creates another thread instance, targeting the 'thread_work' function with argument "T2".
    
    thread1.start() # 启动线程1 # Starts thread 1.
    thread2.start() # 启动线程2 # Starts thread 2.
    
    thread1.join() # 等待线程1完成 # Waits for thread 1 to complete.
    thread2.join() # 等待线程2完成 # Waits for thread 2 to complete.
    
    print("所有子线程完成,主线程结束。") # 打印所有子线程完成信息 # Prints a message indicating all child threads have completed.

在上述线程示例中,你会观察到“线程T1正在执行任务1…”和“线程T2正在执行任务1…”可能会交错出现,这正是线程并发执行的体现。它们共享着主进程的资源,但各自拥有独立的执行路径。

1.1.4 并发与并行的再辨析

  • 并发(Concurrency): 宏观上多个任务似乎在同时进行,微观上在单核CPU上是通过时间片轮转快速切换实现的,在多核CPU上也可以通过线程调度实现。它的核心是管理多个任务在同一时间段内的进展。
  • 并行(Parallelism): 多个任务在物理上真正地同时进行,这需要多核CPU或多台计算机。它的核心是同时执行多个任务以缩短总执行时间。

Python的全局解释器锁(GIL)对Python程序的并行性造成了一定限制。在C/Python解释器中,GIL保证了在任何给定时刻只有一个线程在执行Python字节码。这意味着,即使在多核CPU上,纯Python计算密集型任务也无法实现真正的并行。然而,当Python线程执行I/O操作(如网络请求、文件读写)或调用C扩展库(如NumPy、OpenCV)时,GIL会被释放,允许其他Python线程执行,从而实现并发。本文的重点是锁机制,它主要解决的是并发环境下的数据安全问题,而不是绕过GIL实现并行。

1.2 共享资源与竞态条件:并发编程的核心挑战

在多线程或多进程环境下,当多个执行流(线程或进程)尝试同时访问和修改同一块**共享资源(Shared Resource)时,就可能出现一种称为竞态条件(Race Condition)**的问题。

1.2.1 什么是共享资源?

共享资源可以是任何可以被多个执行流访问和修改的数据或设备。常见的共享资源包括:

  • 全局变量或类成员变量: 在多线程程序中,这些变量通常存在于堆内存中,所有线程都可以访问。
  • 文件: 多个进程或线程可能尝试同时读写同一个文件。
  • 数据库连接: 多个请求可能共用一个数据库连接池中的连接。
  • 网络套接字: 多个线程可能需要发送或接收数据通过同一个网络套接字。
  • 硬件设备: 如打印机、传感器等。

1.2.2 竞态条件的产生机制

竞态条件发生于程序的正确性依赖于事件发生的序列或时机,而这些事件的发生顺序是不可控或非确定的情况下。最经典的例子是“读-改-写”(Read-Modify-Write)操作。

假设有一个全局计数器counter,初始值为0。现在有两个线程T1和T2,它们都要对counter执行“加1”操作。

理想情况下,操作序列应该是:

  1. T1读取counter (0)
  2. T1将counter加1 (1)
  3. T1将结果写回counter (1)
  4. T2读取counter (1)
  5. T2将counter加1 (2)
  6. T2将结果写回counter (2)
    最终counter的值为2。

然而,在没有同步机制的情况下,CPU的调度可能导致以下非预期序列:

  1. T1读取counter (0)
  2. (CPU调度器切换到T2)
  3. T2读取counter (0)
  4. T2将counter加1 (1)
  5. T2将结果写回counter (1)
  6. (CPU调度器切换回T1)
  7. T1将counter加1 (1)
  8. T1将结果写回counter (1)
    最终counter的值为1,而不是预期的2。这就是数据不一致性,是由竞态条件导致的。

1.2.3 经典案例:银行转账问题

假设一个银行系统,账户A有1000元,账户B有200元。现在有两个并发操作:

  1. 操作1:从A转账500元到B。
  2. 操作2:从A转账200元到B。

如果没有适当的同步机制,可能会发生以下情况:

  • 线程1执行:读取A的余额(1000),计算A的新余额(1000-500=500)。
  • (线程1被中断,线程2开始执行)
  • 线程2执行:读取A的余额(此时仍是1000),计算A的新余额(1000-200=800)。
  • 线程2执行:将A的新余额800写入A。读取B的余额(200),计算B的新余额(200+200=400)。将B的新余额400写入B。
  • (线程2完成,线程1恢复执行)
  • 线程1执行:将A的新余额500写入A。读取B的余额(此时是400),计算B的新余额(400+500=900)。将B的新余额900写入B。

最终结果:A的余额是500,B的余额是900。总金额1400。
预期结果:A的余额是1000 - 500 - 200 = 300。B的余额是200 + 500 + 200 = 900。总金额1200。
问题:总金额不守恒,A账户金额错误。这表明由于并发访问和不当的写入顺序,导致了严重的数据损坏。

为了避免竞态条件导致的数据不一致问题,我们需要引入同步机制,确保在任何给定时刻,只有一个线程能够访问和修改共享资源。这就是“锁”诞生的根本原因。

1.3 同步机制的本质:临界区与互斥

为了解决竞态条件问题,计算机科学家提出了同步机制(Synchronization Mechanism)。同步机制的核心思想是确保对共享资源的访问是有序且安全的。

1.3.1 临界区(Critical Section)

在程序中,访问共享资源的代码片段被称为临界区。例如,在计数器示例中,读取计数器值、增加计数器值、写入计数器值的这三步操作共同构成了一个临界区。在银行转账示例中,修改账户A和账户B余额的系列操作构成一个临界区。

同步机制的目标就是确保,在任何时刻,只有一个线程(或进程)能够进入其临界区来操作特定的共享资源。当一个线程正在临界区内执行时,其他试图进入该临界区的线程必须等待,直到当前线程离开临界区。

1.3.2 互斥(Mutual Exclusion)的需求

上述“在任何时刻只有一个线程能够进入其临界区”的原则被称为互斥(Mutual Exclusion)。互斥是实现并发控制最基本也是最重要的原则之一。

一个有效的互斥机制需要满足以下条件:

  1. 互斥性: 任何时候,只有一个进程(或线程)处于临界区中。
  2. 空闲等待: 如果没有进程(或线程)处于临界区中,并且有进程(或线程)请求进入临界区,那么该请求应立即被允许。
  3. 有限等待(避免饥饿): 任何一个希望进入临界区的进程(或线程)在有限时间内都能成功进入,不会无限期地等待。
  4. 死锁避免: 确保多个进程(或线程)不会因为相互等待对方释放资源而都无法继续执行。

1.3.3 抽象地引入锁的概念

为了实现互斥,我们引入了“锁”这一抽象概念。你可以将锁想象成一个房间的门,或者一个唯一的令牌:

  • 门的比喻: 临界区就像一个房间。锁就是这扇门的钥匙。当一个线程想要进入房间(临界区)时,它首先尝试获取钥匙(获取锁)。如果钥匙可用(锁未被占用),它就拿走钥匙,进入房间。其他线程看到钥匙被拿走,就知道房间里有人,必须在门外等待。当房间里的线程完成操作并离开时,它会把钥匙放回原处(释放锁),这时等待的线程中有一个可以拿到钥匙并进入。
  • 令牌的比喻: 有一个唯一的令牌。任何想要访问共享资源的线程必须首先拿到这个令牌。只有拿到令牌的线程才能访问资源。使用完毕后,线程必须归还令牌。

锁的基本工作原理就是:

  1. 获取(Acquire)/上锁: 线程在进入临界区之前,尝试获取锁。
    • 如果锁未被其他线程持有,则当前线程成功获取锁,并继续执行。
    • 如果锁已被其他线程持有,则当前线程会被阻塞(暂停执行),直到锁被释放。
  2. 释放(Release)/解锁: 线程在离开临界区之后,必须释放它所持有的锁,从而允许其他等待的线程获取锁并进入临界区。
# 概念性伪代码示例:锁的使用 # Conceptual pseudocode example: use of a lock.

# 全局共享变量 # Global shared variable.
# shared_data = 0 # 模拟共享数据 # Simulates shared data.

# 创建一个锁对象 # Create a lock object.
# lock = Lock() # 假设Lock()是一个可以创建锁的构造函数 # Assume Lock() is a constructor that creates a lock.

# 线程函数 # Thread function.
# def process_shared_data(): # 定义一个函数,用于处理共享数据 # Defines a function for processing shared data.
#    lock.acquire() # 尝试获取锁,如果锁已被占用,则等待直到获取成功 # Attempts to acquire the lock; if occupied, waits until successful.
#    try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
#        # --- 临界区开始 --- # --- Critical Section Start ---
#        # 读取共享数据 # Read shared data.
#        # 修改共享数据 # Modify shared data.
#        # 写回共享数据 # Write back shared data.
#        print("线程已进入临界区,正在操作共享数据...") # 打印信息表明线程在临界区内操作 # Prints a message indicating the thread is operating within the critical section.
#        time.sleep(0.1) # 模拟操作耗时 # Simulates operation time.
#        # --- 临界区结束 --- # --- Critical Section End ---
#    finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
#        lock.release() # 释放锁,允许其他等待线程获取 # Releases the lock, allowing other waiting threads to acquire it.

# 在多个线程中调用 process_shared_data() 函数 # Call the process_shared_data() function in multiple threads.
# 例如: # For example:
# thread1 = Thread(target=process_shared_data) # thread1 = Thread(target=process_shared_data)
# thread2 = Thread(target=process_shared_data) # thread2 = Thread(target=process_shared_data)
# thread1.start() # thread1.start()
# thread2.start() # thread2.start()
# thread1.join() # thread1.join()
# thread2.join() # thread2.join()

(以上是伪代码,用于概念性说明锁的acquirerelease操作。)

1.4 操作系统层面的同步原语:锁的基石

Python中的threading.Lock并非凭空产生,它底层依赖于操作系统提供的同步原语。理解这些底层机制,有助于我们更深刻地理解锁的本质和性能特点。

1.4.1 禁用中断 (Disabling Interrupts)

在非常低级的操作系统内核层面,尤其是在单处理器系统中,一种简单粗暴的同步方式是禁用中断。当一个CPU核心禁用中断时,它将不会响应任何来自硬件(如定时器、网卡)或软件(如系统调用)的中断请求。这意味着,一旦某个线程进入临界区并禁用中断,它就可以保证在执行完临界区代码之前不会被CPU调度器切换出去。

  • 优点: 实现简单,在单核环境下能有效实现互斥。
  • 缺点:
    • 用户空间不可行: 大多数现代操作系统不允许用户态程序直接禁用中断,这是出于系统稳定性和安全性的考虑。如果用户程序可以随意禁用中断,它可能导致整个系统挂起。
    • 性能开销: 频繁地禁用和启用中断会带来显著的性能开销。
    • 无法扩展到多核: 在多核CPU上,禁用一个核心的中断并不能阻止其他核心继续执行,因此无法保证所有核心上的互斥。
    • 高延迟: 如果临界区代码执行时间过长,禁用中断会导致系统对外部事件的响应延迟。

因此,禁用中断主要用于操作系统内核中非常短小的、对时间敏感的临界区。

1.4.2 TestAndSet (TAS) / CompareAndSwap (CAS) 指令:硬件层面的原子操作

为了在多核处理器上实现高效的同步,现代CPU提供了特殊的原子指令(Atomic Instructions)。原子操作是不可中断的操作,它要么完全执行,要么完全不执行,在执行过程中不会被其他线程中断。即使在多核环境下,这些指令也能保证其操作的完整性。

其中最常见的两种是TestAndSetCompareAndSwap

  • TestAndSet (TAS) 指令
    TestAndSet(memory_address)指令会原子性地完成两个操作:

    1. 读取指定内存地址的当前值。
    2. 将该内存地址的值设置为1(或True)。
    3. 返回读取到的旧值。

    如果旧值是0(或False),则表示之前锁是未被占用的,当前线程成功获取了锁。如果旧值是1,则表示锁已被占用,当前线程没有获取到锁。

    基于TAS指令可以构建一个简单的自旋锁(Spin Lock):

    # 伪代码:基于TestAndSet的自旋锁 # Pseudocode: TestAndSet based spin lock.
    # lock_flag = 0 # 定义一个锁标志,0表示未锁定,1表示已锁定 # Defines a lock flag, 0 means unlocked, 1 means locked.
    
    # acquire_lock(): # 获取锁函数 # acquire_lock() function.
    #    while TestAndSet(&lock_flag) == 1: # 循环,直到TestAndSet返回0(即成功将lock_flag设为1并发现旧值为0) # Loop until TestAndSet returns 0 (i.e., successfully set lock_flag to 1 and found the old value was 0).
    #        # 忙等待 (自旋),不断尝试获取锁 # Busy-wait (spin), continuously trying to acquire the lock.
    #        pass # 空操作,CPU会不断检查锁状态 # No-op, CPU will continuously check the lock status.
    
    # release_lock(): # 释放锁函数 # release_lock() function.
    #    lock_flag = 0 # 将锁标志设为0,释放锁 # Sets the lock flag to 0, releasing the lock.
    

    自旋锁的优点是在锁竞争不激烈时,避免了上下文切换的开销,因为线程会持续占用CPU并检查锁状态。缺点是如果锁竞争激烈,线程会“忙等待”,浪费CPU周期。

  • CompareAndSwap (CAS) 指令
    CAS(memory_address, expected_value, new_value)指令会原子性地完成以下操作:

    1. 读取指定内存地址的当前值。
    2. 如果当前值等于expected_value,则将new_value写入该内存地址。
    3. 返回操作前内存地址的实际值。

    如果返回的实际值等于expected_value,则说明交换成功;否则,说明在读取expected_value后,该内存地址的值被其他线程修改了,交换失败。

    CAS是许多现代并发数据结构和无锁算法(lock-free algorithms)的基础。它可以用来实现比TAS更复杂的同步机制。

    # 伪代码:基于CAS的自旋锁 # Pseudocode: CAS based spin lock.
    # lock_status = 0 # 锁状态,0表示未锁定,1表示已锁定 # Lock status, 0 means unlocked, 1 means locked.
    
    # acquire_lock(): # 获取锁函数 # acquire_lock() function.
    #    while not CAS(&lock_status, 0, 1): # 循环,直到CAS成功将lock_status从0改为1 # Loop until CAS successfully changes lock_status from 0 to 1.
    #        # 忙等待 # Busy-wait.
    #        pass # 空操作 # No-op.
    
    # release_lock(): # 释放锁函数 # release_lock() function.
    #    lock_status = 0 # 将锁状态设为0,释放锁 # Sets lock_status to 0, releasing the lock.
    

    CAS操作的强大之处在于它允许在不使用传统互斥锁的情况下进行更新,这对于构建高性能的并发数据结构至关重要。Python标准库中的threading.Lock通常不会直接暴露这些底层原子指令,而是通过调用操作系统的互斥量API来间接使用它们。

1.4.3 信号量 (Semaphore)

信号量是荷兰计算机科学家Dijkstra提出的一个同步原语。它是一个非负整数变量,除了初始化外,只能通过两个原子操作来访问:

  • P操作 (或 wait / acquire): 尝试将信号量的值减1。如果信号量的值大于0,则减1成功,线程继续执行。如果信号量的值等于0,则线程被阻塞,直到信号量的值大于0。
  • V操作 (或 signal / release): 将信号量的值加1。如果有线程因为P操作而被阻塞,V操作会唤醒其中一个线程。

信号量可以用于控制对共享资源的访问数量。

  • 计数信号量 (Counting Semaphore):
    可以管理具有多个实例的共享资源。例如,如果有一个资源池(如数据库连接池)有N个可用连接,就可以用一个初始值为N的信号量来管理。每当一个线程获取一个连接,信号量减1;释放一个连接,信号量加1。当信号量为0时,表示所有连接都在使用中,其他线程需要等待。

  • 二值信号量 (Binary Semaphore):
    当信号量的值只能是0或1时,它被称为二值信号量。二值信号量实际上就是一种特殊的互斥锁(Mutex)。

    • 初始值为1:表示资源可用。
    • P操作:获取锁,信号量变为0。
    • V操作:释放锁,信号量变为1。
      二值信号量可以用于实现对临界区的互斥访问。

P/V操作的内部逻辑(概念性)

\[
P(S): \\
\quad \text{S.value} = \text{S.value} - 1 \\
\quad \text{if S.value} < 0: \\
\quad \quad \text{add this thread to S.queue} \\
\quad \quad \text{block this thread} \\

V(S): \\
\quad \text{S.value} = \text{S.value} + 1 \\
\quad \text{if S.value} \le 0: \\
\quad \quad \text{remove a thread from S.queue} \\
\quad \quad \text{wakeup that thread}
\]

(这组公式表示了信号量P和V操作的伪代码逻辑。P操作会尝试减少信号量的值,如果值变为负数,则当前线程被加入等待队列并阻塞。V操作会增加信号量的值,如果值小于或等于0,则唤醒一个等待线程。在实际系统中,S.value是原子操作。)

信号量的优点是它们比禁用中断更灵活,并且能够通过阻塞线程来避免忙等待,从而更有效地利用CPU。Python的threading.Semaphore就是对操作系统信号量的封装。

1.4.4 互斥量 (Mutex)

**互斥量(Mutex,Mutual Exclusion的缩写)**是一种特殊的二值信号量,但它比普通二值信号量具有更严格的所有权概念。互斥量旨在保护临界区,确保在任何时候只有一个线程可以访问受保护的资源。

互斥量与二值信号量的区别:

  • 所有权: 互斥量具有所有权概念。只有获取(上锁)了互斥量的线程才能释放(解锁)它。尝试释放一个由其他线程持有的互斥量会导致错误或未定义行为。而二值信号量没有所有权概念,任何线程都可以执行V操作来增加信号量的值,即使它没有执行P操作。
  • 用途: 互斥量主要用于实现互斥访问,保护临界区。信号量则更通用,可以用于实现互斥,也可以用于资源计数,或者实现线程间的信号通知(如生产者-消费者模型)。

操作系统提供的互斥量通常支持线程优先级反转(Priority Inversion)的解决方案(如优先级继承、优先级上限协议),以及死锁检测和避免机制。

递归互斥量 (Reentrant Mutex / RLock)

普通的互斥量有一个问题:如果一个线程已经持有了某个互斥量,然后它在不释放该互斥量的情况下,又尝试再次获取该互斥量,就会导致该线程自身死锁。这是因为acquire操作会阻塞,而它又在等待自己释放锁。

为了解决这个问题,引入了递归互斥量(Reentrant Lock,Python中对应threading.RLock。递归互斥量允许同一个线程多次获取它所持有的锁,而不会被阻塞。每次获取锁时,锁内部的计数器会加1;每次释放锁时,计数器会减1。只有当计数器减到0时,锁才真正被释放,允许其他线程获取。

递归互斥量在某些设计模式中非常有用,例如当一个函数在持有锁的情况下调用另一个也需要相同锁的函数时。

1.5 Python中锁的抽象与threading.Lock的引入

Python通过其标准库threading模块,为我们提供了高级的并发编程工具,其中就包括对底层操作系统同步原语的封装——threading.Lock

1.5.1 Python如何封装底层同步原语

threading.Lock是Python对操作系统互斥量(Mutex)的抽象和封装。当你在Python中创建一个Lock对象时,解释器会向操作系统请求创建一个底层的互斥量对象。当Python线程调用lock.acquire()时,实际上是调用了操作系统提供的互斥量获取(锁定)API;当调用lock.release()时,则调用了操作系统互斥量释放(解锁)API。

这种封装带来了几个显著的优点:

  1. 平台无关性: 开发者无需关心底层操作系统的差异(Windows的CreateMutex、Linux的pthread_mutex_init等),Python的Lock提供了统一的API。
  2. 安全性与健壮性: 操作系统级别的互斥量是经过高度优化和测试的,它们能处理复杂的调度、优先级、死锁等问题。
  3. 效率: 底层互斥量通常由操作系统内核实现,性能经过高度优化,且通常涉及原子指令,效率较高。

1.5.2 threading模块的地位

threading模块是Python中用于实现多线程并发编程的核心模块。它提供了一套高级API,包括Thread类用于创建和管理线程,以及各种同步原语,如LockRLockConditionSemaphoreEvent等,帮助开发者编写线程安全的代码。

尽管Python的GIL限制了纯Python代码的并行执行,但threading模块仍然在以下场景中发挥着巨大作用:

  • I/O密集型任务: 当程序瓶颈在于等待外部资源(如网络、磁盘I/O)时,threading可以有效利用等待时间,在I/O期间释放GIL,允许其他线程运行。
  • 简化程序结构: 将复杂任务分解为独立的、并发执行的子任务,使得程序逻辑更清晰。
  • 利用C扩展的并行能力: 当Python代码调用底层C/C++库时(如数据科学库NumPy、Pandas等),这些库可以在C层释放GIL,实现真正的多核并行。

1.5.3 threading.Lock的内部实现概览(基于操作系统的互斥量)

当我们在Python中使用threading.Lock时,其行为是直接映射到底层操作系统的互斥量原语的。这意味着:

  • 阻塞机制: 当一个线程尝试获取已被其他线程持有的threading.Lock时,该线程不会像自旋锁那样忙等待,而是会被操作系统置于等待队列中,并进入睡眠状态(阻塞)。当锁被释放时,操作系统会从等待队列中唤醒一个(或多个,具体取决于调度策略)线程,使其有机会重新尝试获取锁。这种阻塞机制避免了CPU资源的浪费。
  • 上下文切换: 线程的阻塞和唤醒涉及到操作系统的上下文切换,这会带来一定的性能开销。因此,锁应该尽可能地保护短小的临界区,以减少线程阻塞和唤醒的频率。
  • 原子性: acquire()release()操作本身是原子性的,由操作系统保证。这意味着你不需要担心在调用这些方法时,会发生竞态条件。
import threading # 导入threading模块,用于多线程编程 # This line imports the 'threading' module, which is used for multi-threaded programming.
import time # 导入time模块,用于时间相关操作 # This line imports the 'time' module, which provides time-related functions.

# 定义一个全局共享变量 # Define a global shared variable.
global_counter = 0 # 初始化一个全局计数器为0 # Initializes a global counter to 0.

# 创建一个threading.Lock实例 # Create a threading.Lock instance.
# lock = threading.Lock() # 这是一个标准的互斥锁,一次只允许一个线程持有 # This is a standard mutex lock, allowing only one thread to hold it at a time.

# 线程函数:模拟对共享变量的非安全操作 # Thread function: simulates unsafe operations on a shared variable.
# def unsafe_increment(): # 定义一个名为unsafe_increment的函数 # Defines a function named 'unsafe_increment'.
#    global global_counter # 声明使用全局变量global_counter # Declares the use of the global variable global_counter.
#    current_value = global_counter # 读取当前global_counter的值 # Reads the current value of global_counter.
#    time.sleep(0.001) # 模拟一些微小的计算延迟,增加竞态条件发生的几率 # Simulates a tiny calculation delay to increase the chance of a race condition.
#    global_counter = current_value + 1 # 将读取到的值加1,然后写回global_counter # Increments the read value by 1, then writes it back to global_counter.

# 线程函数:模拟对共享变量的安全操作 # Thread function: simulates safe operations on a shared variable.
# def safe_increment(): # 定义一个名为safe_increment的函数 # Defines a function named 'safe_increment'.
#    global global_counter # 声明使用全局变量global_counter # Declares the use of the global variable global_counter.
#    lock.acquire() # 获取锁,如果锁已被占用,当前线程将阻塞等待 # Acquires the lock; if the lock is held, the current thread will block and wait.
#    try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
#        current_value = global_counter # 读取当前global_counter的值 # Reads the current value of global_counter.
#        time.sleep(0.001) # 模拟一些微小的计算延迟 # Simulates a tiny calculation delay.
#        global_counter = current_value + 1 # 将读取到的值加1,然后写回global_counter # Increments the read value by 1, then writes it back to global_counter.
#    finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
#        lock.release() # 释放锁,允许其他等待线程获取 # Releases the lock, allowing other waiting threads to acquire it.

# if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
#    print("--- 演示非安全并发操作 ---") # 打印演示信息 # Prints demonstration information.
#    global_counter = 0 # 重置计数器 # Resets the counter.
#    threads = [] # 创建一个空列表来存储线程对象 # Creates an empty list to store thread objects.
#    for _ in range(50): # 创建50个线程 # Creates 50 threads.
#        thread = threading.Thread(target=unsafe_increment) # 创建线程,目标是unsafe_increment函数 # Creates a thread targeting the 'unsafe_increment' function.
#        threads.append(thread) # 将线程添加到列表中 # Appends the thread to the list.
#        thread.start() # 启动线程 # Starts the thread.
#
#    for thread in threads: # 遍历所有线程 # Iterates through all threads.
#        thread.join() # 等待每个线程完成 # Waits for each thread to complete.
#
#    print(f"非安全操作后,最终计数器值: {global_counter} (预期值: 50)") # 打印非安全操作结果 # Prints the result of the unsafe operation.

#    print("\n--- 演示安全并发操作 ---") # 打印演示信息 # Prints demonstration information.
#    global_counter = 0 # 重置计数器 # Resets the counter.
#    lock = threading.Lock() # 重新创建一个新的锁对象 # Recreates a new lock object.
#    threads = [] # 创建一个空列表来存储线程对象 # Creates an empty list to store thread objects.
#    for _ in range(50): # 创建50个线程 # Creates 50 threads.
#        thread = threading.Thread(target=safe_increment) # 创建线程,目标是safe_increment函数 # Creates a thread targeting the 'safe_increment' function.
#        threads.append(thread) # 将线程添加到列表中 # Appends the thread to the list.
#        thread.start() # 启动线程 # Starts the thread.
#
#    for thread in threads: # 遍历所有线程 # Iterates through all threads.
#        thread.join() # 等待每个线程完成 # Waits for each thread to complete.
#
#    print(f"安全操作后,最终计数器值: {global_counter} (预期值: 50)") # 打印安全操作结果 # Prints the result of the safe operation.

(在上面的代码中,如果你实际运行,你会发现unsafe_increment的结果通常不是50,而是小于50的值,这证明了竞态条件的存在。而safe_increment的结果总是50,证明了Lock的有效性。)

第二章:Python threading.Lock的深度解析与实战

在第一章中,我们理解了并发编程中的共享资源、竞态条件以及互斥的必要性。现在,我们将深入探讨Python标准库threading中提供的核心同步原语之一:threading.Lock。它作为互斥量的一种实现,是构建线程安全程序的基石。

2.1 threading.Lock的基本概念与生命周期

threading.Lock对象代表着一个原始锁(primitive lock)或简单互斥锁(simple mutex)。它处于两种状态之一:locked(已锁定/被持有)或unlocked(未锁定/未被持有)。

2.1.1 threading.Lock的初始化

创建一个threading.Lock实例非常简单,因为它不需要任何参数。默认情况下,新创建的锁处于unlocked状态。

import threading # 导入threading模块,用于多线程编程 # This line imports the 'threading' module, which is used for multi-threaded programming.

my_lock = threading.Lock() # 创建一个Lock对象,初始状态为未锁定 # Creates a Lock object, initially in an unlocked state.

2.1.2 锁的状态与所有权

threading.Lock没有所有权概念。这意味着,理论上任何线程都可以释放一个由其他线程持有的Lock(尽管这通常是错误的编程实践,会导致难以调试的问题)。它的核心是确保对临界区的互斥访问,而非追踪哪个线程持有它。这种“无所有权”特性是它与RLock(可重入锁)的一个关键区别。

2.2 acquire()方法:获取锁的艺术

acquire()方法是线程尝试获取锁的关键操作。

2.2.1 阻塞模式 (Blocking Mode)

acquire()方法最常见的用法是阻塞模式。当一个线程调用acquire()时:

  • 如果锁当前处于unlocked状态,该线程会成功获取锁,锁的状态变为lockedacquire()方法立即返回True
  • 如果锁当前处于locked状态(即已被其他线程持有),则调用acquire()的线程会被阻塞(暂停执行),直到持有锁的线程释放它。一旦锁被释放,操作系统调度器会选择一个等待的线程唤醒,该线程将有机会获取锁。acquire()方法在成功获取锁后返回True
import threading # 导入threading模块 # Imports the threading module.
import time # 导入time模块 # Imports the time module.

shared_resource_value = 0 # 定义一个共享资源的值 # Defines a shared resource value.
resource_lock = threading.Lock() # 创建一个Lock实例来保护共享资源 # Creates a Lock instance to protect the shared resource.

def worker_function(thread_id): # 定义一个工作函数,模拟线程对共享资源的访问 # Defines a worker function that simulates a thread accessing a shared resource.
    global shared_resource_value # 声明使用全局变量shared_resource_value # Declares the use of the global variable shared_resource_value.
    print(f"线程 {
     
     thread_id} 尝试获取锁...") # 打印线程尝试获取锁的信息 # Prints information about the thread attempting to acquire the lock.
    
    resource_lock.acquire() # 线程尝试获取锁,如果锁被占用,则阻塞等待 # The thread attempts to acquire the lock; if the lock is held, it blocks and waits.
    # 成功获取锁,进入临界区 # Successfully acquired the lock, entering the critical section.
    try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
        print(f"线程 {
     
     thread_id} 已获取锁,进入临界区。") # 打印线程已获取锁的信息 # Prints information about the thread having acquired the lock.
        temp_value = shared_resource_value # 读取共享资源的值 # Reads the value of the shared resource.
        time.sleep(0.1) # 模拟在临界区内进行耗时操作 # Simulates a time-consuming operation within the critical section.
        shared_resource_value = temp_value + 1 # 修改共享资源的值 # Modifies the value of the shared resource.
        print(f"线程 {
     
     thread_id} 修改后共享资源值: {
     
     shared_resource_value}") # 打印修改后的共享资源值 # Prints the modified shared resource value.
    finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
        resource_lock.release() # 线程释放锁 # The thread releases the lock.
        print(f"线程 {
     
     thread_id} 已释放锁。") # 打印线程已释放锁的信息 # Prints information about the thread having released the lock.

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    threads = [] # 创建一个空列表来存储线程对象 # Creates an empty list to store thread objects.
    for i in range(5): # 创建5个线程 # Creates 5 threads.
        thread = threading.Thread(target=worker_function, args=(i,)) # 创建一个线程实例,目标是worker_function,参数是线程ID # Creates a thread instance, targeting the 'worker_function' with the thread ID as an argument.
        threads.append(thread) # 将线程添加到列表中 # Appends the thread to the list.
        thread.start() # 启动线程 # Starts the thread.

    for thread in threads: # 遍历所有线程 # Iterates through all threads.
        thread.join() # 等待每个线程完成 # Waits for each thread to complete.

    print(f"\n所有线程完成,最终共享资源值: {
     
     shared_resource_value}") # 打印最终共享资源值 # Prints the final shared resource value.

在上述示例中,你会观察到线程们按顺序进入临界区,每次只有一个线程能够打印“已获取锁,进入临界区”,确保了shared_resource_value的修改是安全的,最终结果总是5。

2.2.2 非阻塞模式 (blocking=False)

acquire()方法接受一个可选参数blocking,默认为True。如果将其设置为False,则acquire()方法将进入非阻塞模式。

  • 如果锁当前处于unlocked状态,该线程会成功获取锁,锁的状态变为lockedacquire()方法立即返回True
  • 如果锁当前处于locked状态,acquire()方法会立即返回False,而不会阻塞当前线程。

这种模式适用于你希望线程尝试获取锁,但如果无法立即获取,则继续执行其他任务,而不是无限期等待的场景。

import threading # 导入threading模块 # Imports the threading module.
import time # 导入time模块 # Imports the time module.
import random # 导入random模块,用于生成随机数 # Imports the random module, used for generating random numbers.

# 定义一个共享资源 # Defines a shared resource.
printer_status = "Idle" # 打印机状态,初始为空闲 # Printer status, initially idle.
printer_lock = threading.Lock() # 打印机锁 # Printer lock.

def print_document(doc_name, thread_id): # 定义打印文档的函数 # Defines a function to print a document.
    global printer_status # 声明使用全局变量printer_status # Declares the use of the global variable printer_status.
    print(f"线程 {
     
     thread_id}: 尝试打印文档 '{
     
     doc_name}'...") # 打印线程尝试打印文档的信息 # Prints information about the thread attempting to print a document.

    # 尝试非阻塞获取锁 # Attempts to acquire the lock in non-blocking mode.
    if printer_lock.acquire(blocking=False): # 如果能够立即获取锁 # If the lock can be acquired immediately.
        try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
            print(f"线程 {
     
     thread_id}: 成功获取打印机锁,开始打印 '{
     
     doc_name}'...") # 打印成功获取锁的信息 # Prints information about successfully acquiring the lock.
            printer_status = f"Printing {
     
     doc_name} by Thread {
     
     thread_id}" # 更新打印机状态 # Updates the printer status.
            time.sleep(random.uniform(0.5, 1.5)) # 模拟打印时间 # Simulates printing time.
            print(f"线程 {
     
     thread_id}: 完成打印 '{
     
     doc_name}'.") # 打印完成打印的信息 # Prints information about completing the print.
        finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
            printer_status = "Idle" # 打印完成后将打印机状态设为空闲 # Sets the printer status to idle after printing.
            printer_lock.release() # 释放打印机锁 # Releases the printer lock.
    else: # 如果无法立即获取锁 # If the lock cannot be acquired immediately.
        print(f"线程 {
     
     thread_id}: 打印机忙,无法立即打印 '{
     
     doc_name}'。稍后重试或处理其他任务。") # 打印无法立即打印的信息 # Prints information about being unable to print immediately.

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    documents = ["报告", "合同", "图片", "备忘录", "演示文稿", "邮件"] # 定义要打印的文档列表 # Defines a list of documents to print.
    threads = [] # 创建一个空列表来存储线程对象 # Creates an empty list to store thread objects.
    
    # 启动多个线程尝试打印文档 # Starts multiple threads to attempt printing documents.
    for i, doc in enumerate(documents): # 遍历文档列表及其索引 # Iterates through the list of documents and their indices.
        thread = threading.Thread(target=print_document, args=(doc, i + 1)) # 创建线程,目标是print_document函数,参数是文档名和线程ID # Creates a thread, targeting the 'print_document' function with the document name and thread ID as arguments.
        threads.append(thread) # 将线程添加到列表中 # Appends the thread to the list.
        thread.start() # 启动线程 # Starts the thread.

    for thread in threads: # 遍历所有线程 # Iterates through all threads.
        thread.join() # 等待每个线程完成 # Waits for each thread to complete.

    print("\n所有打印任务尝试完成。") # 打印所有任务尝试完成的信息 # Prints information that all print tasks have been attempted.

在这个非阻塞示例中,你会看到某些线程会因为打印机忙而无法立即打印,它们会打印出“打印机忙,无法立即打印…”的信息,而不是被阻塞。这展示了非阻塞acquire()的灵活性。

2.2.3 带超时机制 (timeout参数)

acquire()方法还接受一个timeout参数,用于设置等待锁的最大时间(以秒为单位)。

  • 如果timeout为负数,它会被视为无限等待(与blocking=True效果相同)。
  • 如果timeout为0,它会被视为非阻塞模式(与blocking=False效果相同)。
  • 如果timeout为正数:
    • 如果线程在指定的timeout时间内成功获取到锁,acquire()返回True
    • 如果在timeout时间内未能获取到锁,acquire()方法会立即返回False,当前线程不会无限期阻塞。

这个参数在需要避免无限期等待,或者需要在一定时间后采取备用方案的场景中非常有用。

import threading # 导入threading模块 # Imports the threading module.
import time # 导入time模块 # Imports the time module.

shared_data = [] # 定义一个共享列表,作为共享数据 # Defines a shared list to act as shared data.
data_lock = threading.Lock() # 创建一个锁来保护共享数据 # Creates a lock to protect the shared data.

def data_producer(producer_id): # 定义数据生产者函数 # Defines the data producer function.
    global shared_data # 声明使用全局变量shared_data # Declares the use of the global variable shared_data.
    for i in range(3): # 循环3次,生产3个数据 # Loops 3 times to produce 3 pieces of data.
        item = f"数据-{
     
     producer_id}-{
     
     i}" # 生成一个数据项 # Generates a data item.
        print(f"生产者 {
     
     producer_id} 尝试获取锁准备生产 {
     
     item}...") # 打印生产者尝试获取锁的信息 # Prints information about the producer attempting to acquire the lock to produce an item.
        
        # 尝试在5秒内获取锁 # Attempts to acquire the lock within 5 seconds.
        acquired = data_lock.acquire(timeout=5) # 尝试获取锁,最多等待5秒 # Attempts to acquire the lock, waiting for a maximum of 5 seconds.
        
        if acquired: # 如果成功获取到锁 # If the lock was successfully acquired.
            try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
                print(f"生产者 {
     
     producer_id} 成功获取锁,正在添加 {
     
     item}...") # 打印成功获取锁和正在添加数据的信息 # Prints information about successfully acquiring the lock and adding data.
                shared_data.append(item) # 将数据添加到共享列表中 # Appends the data to the shared list.
                time.sleep(random.uniform(0.1, 0.5)) # 模拟生产数据耗时 # Simulates time taken to produce data.
                print(f"生产者 {
     
     producer_id} 完成添加 {
     
     item}. 当前数据: {
     
     shared_data}") # 打印完成添加数据的信息及当前数据 # Prints information about completing data addition and the current data.
            finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
                data_lock.release() # 释放锁 # Releases the lock.
        else: # 如果在超时时间内未能获取到锁 # If the lock could not be acquired within the timeout period.
            print(f"生产者 {
     
     producer_id} 未能在5秒内获取锁,放弃生产 {
     
     item}。") # 打印未能在超时时间内获取锁的信息 # Prints information about failing to acquire the lock within the timeout.
            # 可以在这里执行一些回退操作,比如记录日志、通知其他系统等 # Can perform fallback operations here, such as logging, notifying other systems, etc.
        time.sleep(random.uniform(0.5, 1.0)) # 模拟生产者生产数据的间隔时间 # Simulates the interval between data production by the producer.

def data_consumer(consumer_id): # 定义数据消费者函数 # Defines the data consumer function.
    global shared_data # 声明使用全局变量shared_data # Declares the use of the global variable shared_data.
    for _ in range(3): # 循环3次,尝试消费3个数据 # Loops 3 times, attempting to consume 3 pieces of data.
        print(f"消费者 {
     
     consumer_id} 尝试获取锁准备消费...") # 打印消费者尝试获取锁的信息 # Prints information about the consumer attempting to acquire the lock.
        acquired = data_lock.acquire(timeout=2) # 尝试获取锁,最多等待2秒 # Attempts to acquire the lock, waiting for a maximum of 2 seconds.

        if acquired: # 如果成功获取到锁 # If the lock was successfully acquired.
            try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
                if shared_data: # 如果共享数据不为空 # If the shared data is not empty.
                    item = shared_data.pop(0) # 从共享列表中移除并获取第一个数据项 # Removes and gets the first data item from the shared list.
                    print(f"消费者 {
     
     consumer_id} 成功获取锁,正在消费 {
     
     item}. 剩余数据: {
     
     shared_data}") # 打印成功获取锁、正在消费数据的信息以及剩余数据 # Prints information about successfully acquiring the lock, consuming data, and remaining data.
                    time.sleep(random.uniform(0.1, 0.3)) # 模拟消费数据耗时 # Simulates time taken to consume data.
                else: # 如果共享数据为空 # If the shared data is empty.
                    print(f"消费者 {
     
     consumer_id} 获取锁但共享数据为空,无数据可消费。") # 打印获取锁但无数据可消费的信息 # Prints information about acquiring the lock but having no data to consume.
            finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
                data_lock.release() # 释放锁 # Releases the lock.
        else: # 如果在超时时间内未能获取到锁 # If the lock could not be acquired within the timeout period.
            print(f"消费者 {
     
     consumer_id} 未能在2秒内获取锁,暂时无法消费。") # 打印未能在超时时间内获取锁的信息 # Prints information about failing to acquire the lock within the timeout.
        time.sleep(random.uniform(0.5, 1.0)) # 模拟消费者消费数据的间隔时间 # Simulates the interval between data consumption by the consumer.

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    producer_thread = threading.Thread(target=data_producer, args=(1,)) # 创建生产者线程 # Creates a producer thread.
    consumer_thread1 = threading.Thread(target=data_consumer, args=(1,)) # 创建消费者线程1 # Creates consumer thread 1.
    consumer_thread2 = threading.Thread(target=data_consumer, args=(2,)) # 创建消费者线程2 # Creates consumer thread 2.

    producer_thread.start() # 启动生产者线程 # Starts the producer thread.
    consumer_thread1.start() # 启动消费者线程1 # Starts consumer thread 1.
    consumer_thread2.start() # 启动消费者线程2 # Starts consumer thread 2.

    producer_thread.join() # 等待生产者线程完成 # Waits for the producer thread to complete.
    consumer_thread1.join() # 等待消费者线程1完成 # Waits for consumer thread 1 to complete.
    consumer_thread2.join() # 等待消费者线程2完成 # Waits for consumer thread 2 to complete.

    print("\n所有生产者和消费者任务尝试完成。") # 打印所有任务尝试完成的信息 # Prints information that all producer and consumer tasks have been attempted.
    print(f"最终共享数据: {
     
     shared_data}") # 打印最终共享数据 # Prints the final shared data.

这个生产者-消费者模拟示例,利用了acquire()timeout参数。你会看到,如果生产者或消费者在指定时间内拿不到锁,它们不会无限等待,而是会打印出相应的提示信息,并继续执行(或进行其他处理)。这在实际系统中非常重要,可以防止单个组件的阻塞导致整个系统停滞。

2.3 release()方法:释放锁的责任

release()方法用于释放由线程持有的锁。

2.3.1 释放逻辑

  • 当一个线程调用release()时,如果锁当前处于locked状态,锁的状态会变回unlocked
  • 如果此时有其他线程正在等待该锁,操作系统调度器会从等待队列中选择一个线程唤醒,使其有机会获取锁。
  • 如果锁当前处于unlocked状态,并且你尝试调用release(),则会引发RuntimeError

重要提示: 总是确保你在获取锁后,无论代码执行路径如何(包括发生异常),都能正确地释放锁。这通常通过try...finally块或with语句来实现。

import threading # 导入threading模块 # Imports the threading module.
import time # 导入time模块 # Imports the time module.

shared_count = 0 # 定义一个共享计数器 # Defines a shared counter.
count_lock = threading.Lock() # 创建一个锁来保护计数器 # Creates a lock to protect the counter.

def increment_and_print(thread_name): # 定义一个函数来递增计数器并打印 # Defines a function to increment the counter and print.
    global shared_count # 声明使用全局变量shared_count # Declares the use of the global variable shared_count.
    print(f"{
     
     thread_name} 尝试获取锁...") # 打印线程尝试获取锁的信息 # Prints information about the thread attempting to acquire the lock.
    count_lock.acquire() # 线程获取锁 # The thread acquires the lock.
    try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
        print(f"{
     
     thread_name} 已获取锁,当前计数: {
     
     shared_count}") # 打印线程已获取锁和当前计数的信息 # Prints information about the thread having acquired the lock and the current count.
        time.sleep(0.01) # 模拟一些操作耗时 # Simulates some operation time.
        shared_count += 1 # 递增计数器 # Increments the counter.
        print(f"{
     
     thread_name} 递增后计数: {
     
     shared_count}") # 打印递增后的计数信息 # Prints information about the incremented count.
        
        # 模拟一个可能发生异常的情况 # Simulates a scenario where an exception might occur.
        if thread_name == "Thread-Error" and shared_count == 3: # 如果是特定线程且计数达到特定值 # If it's a specific thread and the count reaches a specific value.
            raise ValueError("模拟异常发生!") # 故意引发一个ValueError异常 # Intentionally raises a ValueError exception.
            
    except ValueError as e: # 捕获可能发生的ValueError异常 # Catches any ValueError exceptions that might occur.
        print(f"{
     
     thread_name} 捕获到异常: {
     
     e}") # 打印捕获到的异常信息 # Prints the caught exception information.
    finally: # 无论try或except块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try or except block.
        count_lock.release() # 线程释放锁 # The thread releases the lock.
        print(f"{
     
     thread_name} 已释放锁。") # 打印线程已释放锁的信息 # Prints information about the thread having released the lock.

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    threads = [] # 创建一个空列表来存储线程对象 # Creates an empty list to store thread objects.
    for i in range(4): # 创建4个普通线程 # Creates 4 regular threads.
        thread = threading.Thread(target=increment_and_print, args=(f"Thread-{
     
     i+1}",)) # 创建线程,目标是increment_and_print函数,参数是线程名 # Creates a thread, targeting the 'increment_and_print' function with the thread name as an argument.
        threads.append(thread) # 将线程添加到列表中 # Appends the thread to the list.
    
    error_thread = threading.Thread(target=increment_and_print, args=("Thread-Error",)) # 创建一个可能引发异常的线程 # Creates a thread that might raise an exception.
    threads.append(error_thread) # 将异常线程添加到列表中 # Appends the error thread to the list.

    for thread in threads: # 遍历所有线程 # Iterates through all threads.
        thread.start() # 启动线程 # Starts the thread.

    for thread in threads: # 遍历所有线程 # Iterates through all threads.
        thread.join() # 等待每个线程完成 # Waits for each thread to complete.

    print(f"\n所有线程完成。最终计数器值: {
     
     shared_count}") # 打印所有线程完成和最终计数器值的信息 # Prints information about all threads completing and the final counter value.

    # 尝试在锁未被持有的情况下释放锁,会引发RuntimeError # Attempting to release an unlocked lock will raise a RuntimeError.
    # print("\n尝试释放未被持有的锁...") # Prints "Attempting to release an unlocked lock...".
    # try: # Uses a try-except block to catch the expected error.
    #    count_lock.release() # Attempts to release the lock.
    # except RuntimeError as e: # Catches the RuntimeError.
    #    print(f"捕获到预期的RuntimeError: {e}") # Prints the caught RuntimeError.

这个示例清晰地展示了try...finally块在确保锁释放方面的至关重要性,即使在临界区内发生异常,锁也总能被正确释放。同时也演示了尝试释放未被持有的锁会引发RuntimeError

2.4 locked()方法:查询锁的状态

locked()方法是一个简单的查询方法,它返回一个布尔值,指示锁当前是否被持有。

  • 如果锁处于locked状态,locked()返回True
  • 如果锁处于unlocked状态,locked()返回False

这个方法通常用于调试或在某些复杂逻辑中,你需要根据锁的状态来决定下一步操作,而不是尝试获取锁。

import threading # 导入threading模块 # Imports the threading module.
import time # 导入time模块 # Imports the time module.

my_mutex = threading.Lock() # 创建一个Lock实例 # Creates a Lock instance.

def check_lock_status(thread_name): # 定义一个函数来检查锁状态 # Defines a function to check the lock status.
    print(f"{
     
     thread_name}: 初始锁状态: {
     
     my_mutex.locked()}") # 打印初始锁状态 # Prints the initial lock status.
    
    # 线程A尝试获取锁 # Thread A attempts to acquire the lock.
    if thread_name == "Thread-A": # 如果是Thread-A # If it's Thread-A.
        print(f"{
     
     thread_name}: 尝试获取锁...") # Prints "Thread-A: Attempting to acquire lock...".
        my_mutex.acquire() # Thread-A 获取锁 # Thread-A acquires the lock.
        print(f"{
     
     thread_name}: 已获取锁。当前锁状态: {
     
     my_mutex.locked()}") # 打印获取锁后的状态 # Prints the status after acquiring the lock.
        time.sleep(1.5) # 模拟持有锁的时间 # Simulates holding the lock.
        my_mutex.release() # Thread-A 释放锁 # Thread-A releases the lock.
        print(f"{
     
     thread_name}: 已释放锁。当前锁状态: {
     
     my_mutex.locked()}") # 打印释放锁后的状态 # Prints the status after releasing the lock.
    
    # 线程B在中间检查锁状态 # Thread B checks the lock status in the middle.
    elif thread_name == "Thread-B": # 如果是Thread-B # If it's Thread-B.
        time.sleep(0.5) # 等待一段时间,让Thread-A有机会获取锁 # Waits for a short period to allow Thread-A to acquire the lock.
        print(f"{
     
     thread_name}: 检查中,当前锁状态: {
     
     my_mutex.locked()}") # 打印检查时的锁状态 # Prints the lock status during the check.
        time.sleep(1.5) # 继续等待,观察锁是否被释放 # Continues to wait, observing if the lock is released.
        print(f"{
     
     thread_name}: 再次检查,当前锁状态: {
     
     my_mutex.locked()}") # 再次打印锁状态 # Prints the lock status again.

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    thread_a = threading.Thread(target=check_lock_status, args=("Thread-A",)) # 创建Thread-A # Creates Thread-A.
    thread_b = threading.Thread(target=check_lock_status, args=("Thread-B",)) # 创建Thread-B # Creates Thread-B.

    thread_a.start() # 启动Thread-A # Starts Thread-A.
    thread_b.start() # 启动Thread-B # Starts Thread-B.

    thread_a.join() # 等待Thread-A完成 # Waits for Thread-A to complete.
    thread_b.join() # 等待Thread-B完成 # Waits for Thread-B to complete.

    print(f"\n主线程结束。最终锁状态: {
     
     my_mutex.locked()}") # 打印主线程结束和最终锁状态 # Prints information about the main thread ending and the final lock status.

通过观察输出,你会看到Thread-BThread-A持有锁时,其locked()调用会返回True,而在Thread-A释放锁后,会返回False。这表明locked()能够准确反映锁的实时状态。

2.5 with语句与锁的上下文管理

Python的with语句提供了一种优雅的方式来管理资源,确保资源在操作完成后被正确地获取和释放。threading.Lock对象支持上下文管理协议,这意味着你可以将其与with语句一起使用,从而自动处理锁的获取和释放,极大地简化了代码并提高了健壮性。

2.5.1 with语句的工作原理

threading.Lock对象与with语句一起使用时:

  1. 进入with块时,会自动调用锁的acquire()方法。
  2. with块中的代码执行完毕(无论是正常退出还是发生异常),都会自动调用锁的release()方法。

这相当于以下代码模式:

# lock.acquire() # Acquire the lock.
# try: # Try block.
#    # code that needs the lock # Code that needs the lock.
# finally: # Finally block.
#    lock.release() # Release the lock.

使用with语句的好处是显而易见的:你无需手动编写try...finally块来确保锁的释放,这不仅减少了代码量,更重要的是防止了因忘记释放锁而导致的死锁或程序挂起问题。

2.5.2 with语句使用示例

import threading # 导入threading模块 # Imports the threading module.
import time # 导入time模块 # Imports the time module.

shared_data_list = [] # 定义一个共享列表 # Defines a shared list.
list_lock = threading.Lock() # 创建一个锁来保护列表 # Creates a lock to protect the list.

def add_item_to_list(item, thread_name): # 定义一个函数来向列表中添加项 # Defines a function to add items to the list.
    global shared_data_list # 声明使用全局变量shared_data_list # Declares the use of the global variable shared_data_list.
    print(f"{
     
     thread_name}: 尝试添加 '{
     
     item}'...") # 打印线程尝试添加项的信息 # Prints information about the thread attempting to add an item.
    
    # 使用with语句管理锁 # Uses the 'with' statement to manage the lock.
    with list_lock: # 进入with块时自动调用list_lock.acquire() # list_lock.acquire() is automatically called when entering the 'with' block.
        print(f"{
     
     thread_name}: 已获取锁,正在添加 '{
     
     item}'。") # 打印线程已获取锁和正在添加项的信息 # Prints information about the thread having acquired the lock and adding an item.
        shared_data_list.append(item) # 将项添加到列表中 # Appends the item to the list.
        time.sleep(0.1) # 模拟添加耗时 # Simulates time taken to add.
        print(f"{
     
     thread_name}: 完成添加 '{
     
     item}'。当前列表: {
     
     shared_data_list}") # 打印完成添加项的信息及当前列表 # Prints information about completing item addition and the current list.
    # 离开with块时自动调用list_lock.release() # list_lock.release() is automatically called when leaving the 'with' block.
    print(f"{
     
     thread_name}: 已释放锁。") # 打印线程已释放锁的信息 # Prints information about the thread having released the lock.

def remove_item_from_list(thread_name): # 定义一个函数来从列表中移除项 # Defines a function to remove items from the list.
    global shared_data_list # 声明使用全局变量shared_data_list # Declares the use of the global variable shared_data_list.
    print(f"{
     
     thread_name}: 尝试移除项...") # 打印线程尝试移除项的信息 # Prints information about the thread attempting to remove an item.
    
    with list_lock: # 进入with块时自动调用list_lock.acquire() # list_lock.acquire() is automatically called when entering the 'with' block.
        print(f"{
     
     thread_name}: 已获取锁,正在检查列表。") # 打印线程已获取锁和正在检查列表的信息 # Prints information about the thread having acquired the lock and checking the list.
        if shared_data_list: # 如果列表不为空 # If the list is not empty.
            removed_item = shared_data_list.pop(0) # 移除并获取列表中的第一个项 # Removes and gets the first item from the list.
            print(f"{
     
     thread_name}: 移除了 '{
     
     removed_item}'。剩余列表: {
     
     shared_data_list}") # 打印移除的项和剩余列表的信息 # Prints information about the removed item and the remaining list.
        else: # 如果列表为空 # If the list is empty.
            print(f"{
     
     thread_name}: 列表为空,无法移除。") # 打印列表为空无法移除的信息 # Prints information about the list being empty and unable to remove.
        time.sleep(0.05) # 模拟移除耗时 # Simulates time taken to remove.
    print(f"{
     
     thread_name}: 已释放锁。") # 打印线程已释放锁的信息 # Prints information about the thread having released the lock.

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    threads = [] # 创建一个空列表来存储线程对象 # Creates an empty list to store thread objects.
    
    # 启动添加线程 # Starts threads for adding.
    for i in range(3): # 创建3个添加线程 # Creates 3 adding threads.
        thread = threading.Thread(target=add_item_to_list, args=(f"Item-{
     
     i+1}", f"Adder-{
     
     i+1}")) # 创建线程,目标是add_item_to_list函数 # Creates a thread targeting the 'add_item_to_list' function.
        threads.append(thread) # 将线程添加到列表中 # Appends the thread to the list.
        thread.start() # 启动线程 # Starts the thread.

    # 启动移除线程 # Starts threads for removing.
    for i in range(2): # 创建2个移除线程 # Creates 2 removing threads.
        thread = threading.Thread(target=remove_item_from_list, args=(f"Remover-{
     
     i+1}",)) # 创建线程,目标是remove_item_from_list函数 # Creates a thread targeting the 'remove_item_from_list' function.
        threads.append(thread) # 将线程添加到列表中 # Appends the thread to the list.
        thread.start() # Starts the thread.

    for thread in threads: # 遍历所有线程 # Iterates through all threads.
        thread.join() # 等待每个线程完成 # Waits for each thread to complete.

    print(f"\n所有任务完成。最终列表内容: {
     
     shared_data_list}") # 打印所有任务完成和最终列表内容的信息 # Prints information about all tasks completing and the final list content.

这个示例展示了with语句在多线程环境中保护共享列表的简洁性和有效性。每个线程在访问shared_data_list时都会自动获取和释放锁,确保了操作的原子性和线程安全性。

2.6 threading.Lock的内部状态与挑战

尽管Python的Lock是操作系统互斥量的封装,但从概念上理解其内部管理的状态和可能面临的挑战,对于正确使用和调试锁至关重要。

2.6.1 锁的“状态”位 (Conceptual State Bit)

一个原始锁(threading.Lock)可以被想象成内部维护了一个简单的布尔标志,或者一个计数器,其值非0即1:

  • 0 (或 False): 表示锁处于未锁定状态,可以被获取。
  • 1 (或 True): 表示锁处于锁定状态,已被某个线程持有。

acquire()被调用时,它尝试将这个状态从0变为1。如果成功,则获取锁;如果已经是1,则阻塞或返回False(取决于blocking参数)。
release()被调用时,它将这个状态从1变回0。如果已经是0,则报错。

这个状态位的读写操作,在底层是由操作系统利用CPU的原子指令(如CAS)来保证其原子性的,以防止多个线程同时修改导致的数据损坏。

2.6.2 线程等待队列 (Waiting Queue)

当一个线程尝试获取一个已被占用的Lock并处于阻塞模式时,操作系统会将该线程放入一个与该锁关联的等待队列中。这个队列通常由操作系统内核管理,并可能采用不同的调度策略(如FIFO、优先级调度等)。

  • 阻塞: 线程进入等待队列后,会被操作系统标记为“等待中”(waiting)或“睡眠中”(sleeping)状态,并从CPU调度队列中移除。它不再消耗CPU时间,直到被唤醒。
  • 唤醒: 当持有锁的线程调用release()时,操作系统会检查该锁的等待队列。如果队列中有线程,操作系统会选择一个(或多个)线程将其从“等待中”状态唤醒,使其重新回到CPU调度队列,有机会被调度执行并重新尝试获取锁。

这种阻塞-唤醒机制是互斥锁避免“忙等待”(如自旋锁)从而节省CPU资源的关键。

2.6.3 GIL (Global Interpreter Lock) 的影响

在C/Python解释器中,即使使用了threading.Lock,GIL的存在仍然对并发有深远影响。

  • GIL与threading.Lock的关系: threading.Lock是为了保护用户代码中的共享资源,防止竞态条件。GIL是为了保护Python解释器内部的状态,防止多线程同时修改解释器数据结构。它们服务于不同的目的。
  • GIL的释放: Python线程在执行长时间运行的I/O操作(如文件读写、网络通信)或调用大部分C语言扩展库时,会自动释放GIL。这意味着,在I/O密集型或计算密集型但通过C扩展实现的任务中,多线程Python程序可以获得并发甚至接近并行的性能。
  • GIL的重夺: 当I/O操作完成或C扩展代码返回Python解释器时,线程会尝试重新获取GIL。

因此,即使一个线程持有了threading.Lock,它仍然需要GIL才能执行Python字节码。当它执行I/O并释放GIL时,其他Python线程可以在没有threading.Lock的情况下执行(但它们在需要访问被threading.Lock保护的临界区时仍会受限于threading.Lock)。当它重新获取GIL时,它才可能重新进入被threading.Lock保护的临界区。

2.6.4 threading.Lock的局限性与潜在挑战

尽管threading.Lock是强大的同步工具,但它的使用也带来了一系列挑战:

A. 死锁 (Deadlock)

死锁是多线程编程中最常见且最难以调试的问题之一。当两个或多个线程无限期地相互等待对方释放它们所需的资源(锁)时,就会发生死锁。

死锁的四个必要条件(Coffman条件):

  1. 互斥 (Mutual Exclusion): 至少有一个资源(如锁)是非共享的,一次只能被一个线程占用。(threading.Lock满足此条件)
  2. 占有并等待 (Hold and Wait): 一个线程已经持有了至少一个资源,但又在等待获取其他被其他线程持有的资源。
  3. 不可抢占 (No Preemption): 资源不能被强制从持有它的线程那里抢走,只能由持有者自愿释放。(threading.Lock满足此条件)
  4. 循环等待 (Circular Wait): 存在一个线程集合{T1, T2, ..., Tn},其中T1等待T2持有的资源,T2等待T3持有的资源,…,Tn等待T1持有的资源,形成一个环。

死锁示例:
假设有两个锁lock_Alock_B

  • 线程1执行:acquire(lock_A) -> acquire(lock_B)
  • 线程2执行:acquire(lock_B) -> acquire(lock_A)

如果执行顺序恰好是:

  1. T1获取lock_A
  2. T2获取lock_B
  3. T1尝试获取lock_B(被T2持有,T1阻塞)。
  4. T2尝试获取lock_A(被T1持有,T2阻塞)。
    此时,T1和T2都无限期阻塞,形成死锁。
import threading # 导入threading模块 # Imports the threading module.
import time # 导入time模块 # Imports the time module.

lock_alpha = threading.Lock() # 创建第一个锁 # Creates the first lock.
lock_beta = threading.Lock() # 创建第二个锁 # Creates the second lock.

def thread_function_1(): # 定义线程函数1 # Defines thread function 1.
    print("线程1: 尝试获取 lock_alpha...") # Prints "Thread 1: Attempting to acquire lock_alpha...".
    lock_alpha.acquire() # 线程1获取lock_alpha # Thread 1 acquires lock_alpha.
    print("线程1: 已获取 lock_alpha. 稍作等待...") # Prints "Thread 1: Acquired lock_alpha. Waiting a bit...".
    time.sleep(0.1) # 模拟一些工作 # Simulates some work.
    
    print("线程1: 尝试获取 lock_beta...") # Prints "Thread 1: Attempting to acquire lock_beta...".
    lock_beta.acquire() # 线程1获取lock_beta # Thread 1 acquires lock_beta.
    
    try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
        print("线程1: 已获取 lock_alpha 和 lock_beta。执行关键操作...") # Prints "Thread 1: Acquired lock_alpha and lock_beta. Performing critical operation...".
        # 临界区代码 # Critical section code.
    finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
        lock_beta.release() # 线程1释放lock_beta # Thread 1 releases lock_beta.
        print("线程1: 已释放 lock_beta.") # Prints "Thread 1: Released lock_beta.".
        lock_alpha.release() # 线程1释放lock_alpha # Thread 1 releases lock_alpha.
        print("线程1: 已释放 lock_alpha.") # Prints "Thread 1: Released lock_alpha.".

def thread_function_2(): # 定义线程函数2 # Defines thread function 2.
    print("线程2: 尝试获取 lock_beta...") # Prints "Thread 2: Attempting to acquire lock_beta...".
    lock_beta.acquire() # 线程2获取lock_beta # Thread 2 acquires lock_beta.
    print("线程2: 已获取 lock_beta. 稍作等待...") # Prints "Thread 2: Acquired lock_beta. Waiting a bit...".
    time.sleep(0.1) # 模拟一些工作 # Simulates some work.
    
    print("线程2: 尝试获取 lock_alpha...") # Prints "Thread 2: Attempting to acquire lock_alpha...".
    lock_alpha.acquire() # 线程2获取lock_alpha # Thread 2 acquires lock_alpha.
    
    try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
        print("线程2: 已获取 lock_beta 和 lock_alpha。执行关键操作...") # Prints "Thread 2: Acquired lock_beta and lock_alpha. Performing critical operation...".
        # 临界区代码 # Critical section code.
    finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
        lock_alpha.release() # 线程2释放lock_alpha # Thread 2 releases lock_alpha.
        print("线程2: 已释放 lock_alpha.") # Prints "Thread 2: Released lock_alpha.".
        lock_beta.release() # 线程2释放lock_beta # Thread 2 releases lock_beta.
        print("线程2: 已释放 lock_beta.") # Prints "Thread 2: Released lock_beta.".

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    t1 = threading.Thread(target=thread_function_1) # 创建线程1 # Creates thread 1.
    t2 = threading.Thread(target=thread_function_2) # 创建线程2 # Creates thread 2.

    t1.start() # 启动线程1 # Starts thread 1.
    t2.start() # 启动线程2 # Starts thread 2.

    t1.join() # 等待线程1完成 (如果发生死锁,这里将永远等待) # Waits for thread 1 to complete (if deadlock occurs, it will wait forever here).
    t2.join() # 等待线程2完成 (如果发生死锁,这里将永远等待) # Waits for thread 2 to complete (if deadlock occurs, it will wait forever here).

    print("\n主程序完成。") # Prints "Main program completed.".

运行上述死锁示例,你会发现程序可能会在一段时间后停止响应,因为两个线程都陷入了无限等待。

死锁预防策略:

  • 按序获取锁: 约定所有线程按照相同的顺序获取多个锁。在上述示例中,如果T1和T2都先获取lock_A,再获取lock_B,就不会发生死锁。
  • 一次性获取所有锁: 如果可能,在一个原子操作中获取所有需要的锁。这通常不切实际,但在某些场景下可行。
  • 使用超时机制: 在acquire()时使用timeout参数。如果无法在规定时间内获取所有锁,则释放已持有的锁,并稍后重试。这并不能完全避免死锁,但可以将其转化为“活锁”(livelock),即线程不断尝试获取锁、释放、重试,但永远无法完成任务。
  • 避免不必要的锁: 尽可能缩小临界区,减少锁的粒度。
  • 使用高级同步原语: 例如条件变量(Condition)或信号量(Semaphore),它们提供了更复杂的同步能力,有时可以帮助避免死锁。

B. 性能瓶颈

锁虽然解决了数据安全性问题,但引入了额外的性能开销:

  • 上下文切换: 线程的阻塞和唤醒需要操作系统进行上下文切换,这涉及到保存当前线程的CPU状态并加载另一个线程的状态,开销较大。
  • 锁竞争: 当多个线程频繁地竞争同一个锁时,会导致大量线程阻塞和唤醒,从而降低程序的整体吞吐量。
  • CPU缓存失效: 锁的争用可能导致CPU缓存行在不同核心之间频繁迁移,增加内存访问延迟。

优化策略:

  • 缩小临界区: 只在真正需要保护共享资源的代码段上加锁,使临界区尽可能小。
  • 减少锁粒度: 如果可以将一个大的共享资源拆分为多个小的独立资源,并为每个小资源使用独立的锁,可以允许更高的并发度。
  • 避免频繁加锁和解锁: 尽量减少acquire()release()的调用次数。
  • 使用无锁/免锁数据结构: 对于某些特定的并发场景,可以考虑使用Python的queue模块(其内部已实现线程安全)或其他无锁/免锁算法(例如使用原子操作或CAS,但这在纯Python中实现复杂)。

C. 编程复杂性与错误倾向性

手动管理锁的获取和释放很容易出错。常见的错误包括:

  • 忘记释放锁: 导致其他线程永久阻塞。
  • 重复释放未持有的锁: 导致RuntimeError
  • 在错误的地方释放锁: 可能导致竞态条件重新出现。
  • 锁的粒度不当: 粒度过大导致并发度低,粒度过小可能引入新的竞态条件或增加死锁风险。

使用with语句是避免“忘记释放锁”等常见错误的最有效方式。

2.7 threading.Lockthreading.RLock的区别:重入性

Python的threading模块提供了两种主要的互斥锁:LockRLock。理解它们之间的核心区别至关重要。

2.7.1 threading.Lock (Non-reentrant Lock / Primitive Lock)

如前所述,threading.Lock是一个原始锁,不具备重入性(non-reentrant)。

  • 特点:
    • 一次只能被一个线程持有
    • 不能被同一个线程多次获取:如果一个线程已经持有了Lock,然后它在不释放该Lock的情况下再次尝试acquire()它,该线程会自己阻塞自己,从而导致死锁。这是因为Lock没有内部计数器来跟踪当前持有者的获取次数。
    • 没有所有权概念: 任何线程都可以尝试释放一个Lock,即使它不是持有者(尽管这是不推荐的危险行为,可能导致未定义状态或RuntimeError)。

Lock的自死锁示例:

import threading # 导入threading模块 # Imports the threading module.
import time # 导入time模块 # Imports the time module.

non_reentrant_lock = threading.Lock() # 创建一个非可重入锁 # Creates a non-reentrant lock.

def lock_and_call_again(): # 定义一个函数,它会尝试两次获取同一个锁 # Defines a function that attempts to acquire the same lock twice.
    print("函数开始:尝试获取第一次锁...") # Prints "Function start: Attempting to acquire the lock for the first time...".
    non_reentrant_lock.acquire() # 第一次获取锁 # Acquires the lock for the first time.
    try: # 使用try-finally块确保锁在任何情况下都能被释放 # Uses a try-finally block to ensure the lock is released under all circumstances.
        print("第一次锁已获取。尝试获取第二次锁(会导致死锁)...") # Prints "First lock acquired. Attempting to acquire the lock for the second time (will cause deadlock)...".
        non_reentrant_lock.acquire() # 第二次获取锁,此时该线程已持有锁,会阻塞自己,导致死锁 # Acquires the lock for the second time; at this point, the thread already holds the lock, which will cause it to block itself, leading to a deadlock.
        print("(你永远不会看到这句话,因为已经死锁)第二次锁已获取。") # Prints "(You will never see this message because a deadlock has occurred) Second lock acquired.".
    finally: # 无论try块中是否发生异常,finally块中的代码都会执行 # The code in the finally block will execute regardless of whether an exception occurs in the try block.
        print("尝试释放第一次锁...") # Prints "Attempting to release the first lock...".
        non_reentrant_lock.release() # 释放第一次获取的锁 # Releases the first acquired lock.
        # 如果第二次获取成功(但实际上不会),还需要释放第二次获取的锁 # If the second acquisition were successful (which it won't be), the second acquired lock would also need to be released.
        # non_reentrant_lock.release() 
        print("第一次锁已释放。") # Prints "First lock released.".

if __name__ == "__main__": # 确保这段代码只在作为主程序运行时执行 # Ensures this block of code only runs when the script is executed directly (not imported).
    t = threading.Thread(target=lock_and_call_again) # 创建线程 # Creates a thread.
    t.start() # 启动线程 # Starts the thread.
    t.join(timeout=2) # 等待线程完成,设置超时2秒,以观察死锁现象 # Waits for the thread to complete, with a 2-second timeout, to observe the deadlock phenomenon.

    if t.is_alive(): # 如果线程在超时后仍然存活,说明死锁发生 # If the thread is still alive after the timeout, it means a deadlock occurred.
        print("\n检测到死锁:线程在尝试第二次获取锁时阻塞了自己。") # Prints "Deadlock detected: The thread blocked itself while trying to acquire the lock a second time.".
    else: # 否则,如果线程正常结束 # Otherwise, if the thread ended normally.
        print("\n线程正常结束(不太可能发生此情况)。") # Prints "Thread ended normally (unlikely to happen in this case).".

运行这个示例,你会明确地看到线程在尝试第二次获取锁时发生了自死锁。

2.7.2 threading.RLock (Reentrant Lock / Recursive Lock)

threading.RLock是可重入锁(reentrant lock)或递归锁(recursive lock)。

  • 特点:
    • 可以被同一个线程多次获取。它内部维护一个计数器和一个当前持有者的标识。
    • 计数器: 每当同一个线程成功调用acquire()时,计数器加1。
    • 持有者: 记录当前持有锁的线程。只有持有锁的线程才能释放它。
    • 释放: 只有当持有锁的线程多次调用release(),使得内部计数器归零时,锁才真正被释放,允许其他等待线程获取。
    • 非持有者释放: 如果非持有锁的线程尝试调用release(),会引发RuntimeError

RLock的可重入性示例:

import threading # 导入threading模块 # Imports the threading module.
import time # 导入time module.

reentrant_lock = threading.RLock() # 创建一个可重入锁 # Creates a reentrant lock.

def outer_function(): # 定义外部函数 # Defines the outer function.
    print("外部函数:尝试获取锁...") # Prints "Outer function: Attempting to acquire lock...".
    with reentrant_lock: # 第一次获取锁 # Acquires the lock for the first time.
        print("外部函数:已获取锁。") # Prints "Outer function: Acquired lock.".
        time.sleep(0.01) # 模拟一些工作 # Simulates some work.
        inner_function() # 调用内部函数 # Calls the inner function.
    print("外部函数:已释放锁。") # Prints "Outer function: Released lock.".

def inner_function(): # 定义内部函数 # Defines the inner function.
    print("  内部函数:尝试获取锁...") # Prints "  Inner function: Attempting to acquire lock...".
    with reentrant_lock: # 第二次获取锁,由于是同一个线程,锁被重入 # Acquires the lock for the second time; since it's the same thread, the lock is re-entered.
        print
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值