OpenCV摄像头多线程加速与帧率优化实战 - 第4节

4. 线程同步入门

什么是竞态条件?使用 Lock 保护共享资源

在多线程环境中,当多个线程同时访问同一份共享数据(例如摄像头帧缓冲区、计数器),并且至少有一个线程执行写操作时,就会产生竞态条件 (Race Condition)。竞态条件会导致数据不一致、程序崩溃或难以复现的 bug。本章将剖析竞态条件的本质,并演示如何通过 threading.Lock 让共享资源变得安全。

共享计数器 count 线程 A 线程 B ⚡ 竞态

竞态条件:一个经典例子

假设两个线程同时执行 counter += 1。这一行 Python 代码在底层至少包含三步:读取 count、加 1、写回。如果线程 A 读取 count 后,线程 B 也读取了相同的旧值,然后各自加 1 写回,最终 count 只增加了 1 而不是 2。这就是典型的“丢失更新”。

💡 竞态条件三要素: 共享可变资源、多个线程、至少一个写操作。缺少任一条件都不会产生竞态。
时间 A: 读取 count=5 A: count+1=6 A: 写回 6 B: 读取 count=5 B: count+1=6 B: 写回 6 最终 count=6 (错误)

使用 Lock 保护共享资源

Python 的 threading.Lock 提供互斥锁。线程在访问共享资源前必须获取锁,使用完毕后释放锁。同一时刻只有一个线程能持有锁,从而保证操作的原子性。

import threading

counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(100000):
        lock.acquire()      # 获取锁
        counter += 1        # 临界区
        lock.release()      # 释放锁

# 更推荐使用 with 语句:
def safe_increment_with():
    global counter
    for _ in range(100000):
        with lock:          # 自动 acquire / release
            counter += 1

Lock 的常用方法

方法说明
acquire(blocking=True, timeout=-1)获取锁。blocking=False 时非阻塞;timeout 指定等待秒数。
release()释放锁。必须在持有锁的线程中调用。
locked()返回锁是否已被某个线程持有。
🔒 Lock 线程 A (持有锁) 线程 B (等待)

实际案例:摄像头帧计数器

在多线程摄像头读取中,通常有一个共享的帧计数器或缓冲区索引。使用 Lock 可以防止两个线程同时更新索引导致帧错乱。下面是一个简化示例:

import threading
import cv2

frame_count = 0
count_lock = threading.Lock()

def capture_worker(cam_id):
    global frame_count
    cap = cv2.VideoCapture(cam_id)
    while True:
        ret, frame = cap.read()
        if not ret: break
        with count_lock:
            frame_count += 1
            current = frame_count
        # 处理帧 (例如显示或放入队列)
        print(f"摄像头 {cam_id} 捕获第 {current} 帧")
    cap.release()

使用 with count_lock 确保 frame_count += 1 和读取当前值是一个不可分割的临界区。即使有 4 个摄像头线程并发,计数器也不会出现丢失更新。

竞态条件 vs 死锁

Lock 虽然解决了竞态条件,但使用不当可能引入死锁(两个线程互相等待对方释放锁)。在后续章节中我们会深入讨论死锁的避免策略。现阶段请记住:尽量缩小临界区范围,并始终使用 with 语句管理锁。