OpenCV摄像头多线程加速与帧率优化实战 - 第4节
4. 线程同步入门
什么是竞态条件?使用 Lock 保护共享资源
在多线程环境中,当多个线程同时访问同一份共享数据(例如摄像头帧缓冲区、计数器),并且至少有一个线程执行写操作时,就会产生竞态条件 (Race Condition)。竞态条件会导致数据不一致、程序崩溃或难以复现的 bug。本章将剖析竞态条件的本质,并演示如何通过 threading.Lock 让共享资源变得安全。
竞态条件:一个经典例子
假设两个线程同时执行 counter += 1。这一行 Python 代码在底层至少包含三步:读取 count、加 1、写回。如果线程 A 读取 count 后,线程 B 也读取了相同的旧值,然后各自加 1 写回,最终 count 只增加了 1 而不是 2。这就是典型的“丢失更新”。
💡 竞态条件三要素: 共享可变资源、多个线程、至少一个写操作。缺少任一条件都不会产生竞态。
使用 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 可以防止两个线程同时更新索引导致帧错乱。下面是一个简化示例:
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 语句管理锁。