Skip to main content

Python 进程与线程

Python 进程同步机制

本章节主要讲解 Python中进程同步机制的实现,进程同步的概念,锁(Lock)的使用,锁的基本原理,锁的代码示例,锁的注意事项,信号量(Semaphore)的使用,信号量的基本原理,信号量的代码示例,信号量的注意事项,锁与信号量的比较,高级应用:结合锁和信号量。

1. 进程同步的概念

在多进程编程中,多个进程可能同时访问共享资源(如文件、内存变量等),这可能导致数据不一致或“竞争条件”(race condition)。进程同步机制用于协调多个进程的执行顺序,确保共享资源的安全访问。

Python的multiprocessing模块提供了多种同步工具,其中 锁(Lock)和信号量(Semaphore)是最常用的两种。

假设两个进程同时修改一个共享变量,未经同步可能导致不可预测的结果。例如,一个进程读取变量值并准备更新时,另一个进程可能同时修改该值,导致数据覆盖或错误。同步机制通过限制访问权限或控制并发数量来解决此类问题。

2. 锁(Lock)的使用

锁是最简单的进程同步工具,同一时间只允许一个进程持有锁,访问共享资源,其他进程必须等待锁释放。Python的multiprocessing.Lock类提供了这种功能。

2.1 锁的基本原理

  • 获取锁:进程通过lock.acquire()尝试获取锁。如果锁已被占用,进程会阻塞直到锁可用。
  • 释放锁:进程完成操作后通过lock.release()释放锁,允许其他进程获取。
  • 适用场景:适合需要独占访问的场景,如修改共享变量或写入文件。

2.2 锁的代码示例

以下示例展示两个进程同时尝试增加共享计数器,使用锁避免竞争条件。

from multiprocessing import Process, Lock, Value
import time

def increment_counter(counter, lock, name):
    for _ in range(5):
        with lock:  # 使用with语句自动获取和释放锁
            current = counter.value
            print(f"{name} 读取计数器: {current}")
            counter.value = current + 1
            print(f"{name} 更新计数器: {counter.value}")
            time.sleep(0.1)  # 模拟耗时操作

if __name__ == "__main__":
    counter = Value('i', 0)  # 共享整数变量
    lock = Lock()  # 创建锁对象
    p1 = Process(target=increment_counter, args=(counter, lock, "进程1"))
    p2 = Process(target=increment_counter, args=(counter, lock, "进程2"))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print(f"最终计数器值: {counter.value}")

说明

  • Value('i', 0)创建一个共享整数变量,初始值为0。
  • with lock语法确保锁在操作完成后自动释放,即使发生异常。
  • 每个进程循环5次,每次增加计数器值。锁保证每次只有一个进程修改counter
  • 输出显示计数器按顺序增加,最终值为10。

2.3 锁的注意事项

  • 死锁风险:如果多个锁使用不当,可能导致进程互相等待,程序卡死。应确保锁的获取和释放顺序一致。
  • 性能影响:锁会降低并发效率,仅适合关键区域(critical section)较小的场景。
  • 非阻塞模式lock.acquire(block=False)可尝试获取锁而不阻塞,若失败返回False

3. 信号量(Semaphore)的使用

信号量是一种更灵活的同步机制,允许指定数量的进程同时访问资源。Python的multiprocessing.Semaphore类实现此功能。

3.1 信号量的基本原理

  • 计数器:信号量维护一个内部计数器,表示可用资源的数量。
  • 获取信号量:通过semaphore.acquire()减少计数器。若计数器为0,进程阻塞。
  • 释放信号量:通过semaphore.release()增加计数器,唤醒等待的进程。
  • 适用场景:适合控制有限资源的并发访问,如限制同时运行的任务数量。

3.2 信号量的代码示例

以下示例模拟5个进程竞争3个数据库连接资源,使用信号量控制并发。

from multiprocessing import Process, Semaphore
import time
import random

def access_database(semaphore, name):
    print(f"{name} 尝试获取数据库连接...")
    with semaphore:  # 自动获取和释放信号量
        print(f"{name} 获取到数据库连接")
        time.sleep(random.uniform(0.5, 2))  # 模拟数据库操作
        print(f"{name} 完成操作,释放连接")

if __name__ == "__main__":
    semaphore = Semaphore(3)  # 最多允许3个进程同时访问
    processes = []
    for i in range(5):
        p = Process(target=access_database, args=(semaphore, f"进程{i+1}"))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print("所有数据库操作完成")

说明

  • Semaphore(3)创建信号量,允许最多3个进程同时执行with semaphore块。
  • 5个进程竞争资源,但每次最多3个进程进入临界区。
  • time.sleep(random.uniform(0.5, 2))模拟不同进程的操作时间。
  • 输出显示进程按信号量限制顺序访问资源。

3.3 信号量的注意事项

  • 计数器管理:错误释放信号量可能导致计数器溢出,需谨慎使用。
  • 与锁的区别:锁只允许一个进程访问,而信号量允许多个进程(由计数器决定)。
  • 超时支持semaphore.acquire(timeout=seconds)可设置阻塞超时时间。

4. 锁与信号量的比较

特性 锁(Lock) 信号量(Semaphore)
并发数量 仅允许1个进程 允许多个进程(由计数器决定)
使用场景 独占资源访问 控制有限资源的并发访问
复杂度 简单,易于理解 稍复杂,需管理计数器
灵活性 较低 较高

选择建议:

  • 如果需要严格的互斥访问(如修改全局变量),使用Lock
  • 如果需要控制多个进程的并发数量(如线程池、连接池),使用Semaphore

5. 高级应用:结合锁和信号量

在复杂场景中,可能需要结合锁和信号量。例如,多个进程写入日志文件(需要锁保护文件),但只允许有限进程同时运行(用信号量限制)。

from multiprocessing import Process, Lock, Semaphore, Value
import time
import random

def write_log(counter, lock, semaphore, name):
    with semaphore:  # 限制并发进程数量
        for _ in range(3):
            with lock:  # 保护共享计数器
                current = counter.value
                counter.value = current + 1
                print(f"{name} 写入日志,计数器: {counter.value}")
                time.sleep(random.uniform(0.1, 0.5))

if __name__ == "__main__":
    counter = Value('i', 0)  # 共享计数器
    lock = Lock()  # 用于保护计数器
    semaphore = Semaphore(2)  # 限制2个进程同时运行
    processes = []
    for i in range(4):
        p = Process(target=write_log, args=(counter, lock, semaphore, f"进程{i+1}"))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print(f"最终计数器值: {counter.value}")

说明

  • 信号量限制最多2个进程同时运行。
  • 锁保护共享计数器,确保写入操作安全。
  • 输出显示最多2个进程同时写入,且计数器按顺序增加,最终值为12。

6. 总结

  • 适合简单互斥场景,确保资源独占访问。
  • 信号量适合控制并发数量,管理有限资源。
  • 调试技巧:记录锁或信号量的获取和释放日志,便于排查问题。
  • 避免死锁:确保锁的获取顺序一致,避免循环等待。
  • 性能优化:尽量缩小临界区范围,减少阻塞时间。