在并发编程中,当多个线程同时访问共享数据且执行顺序影响结果时,就会产生竞态条件(Race Condition)。这种不确定性会导致共享数据的最终值不一致,甚至损坏。以下通过经典案例说明:
场景:两个线程对初始值 count = 5 执行操作:
- 线程 A:count \gets count + 1
- 线程 B:count \gets count - 1
问题 :若底层指令交错执行(如
LOAD → ADD → STORE与LOAD → SUB → STORE混合),结果可能是 4、5 或 6,而非逻辑正确的 5。
为解决此问题,需通过同步机制保护临界区(Critical Section)------访问共享资源的代码段。其标准流程包括:
- 进入区:请求进入权限
- 临界区:操作共享数据
- 退出区:释放权限
- 剩余区:执行非共享代码
有效方案需满足三要求:
- 互斥:同一时刻仅一个线程进入临界区
- 进步:系统不会无限推迟进入决策
- 有限等待:等待时间存在上限
一、互斥锁(Mutex Locks)
互斥锁是最基础的同步工具,核心机制可类比为唯一钥匙:
python
def acquire():
while not available: # 自旋等待
pass
available = False # 取走钥匙
def release():
available = True # 归还钥匙
核心机制:唯一的一把钥匙
你可以把互斥锁想象成进入一间更衣室(临界区)的唯一钥匙。
- 获取锁 (acquire()):程序员在进入临界区前,必须调用这个函数来请求"钥匙"。
- 如果锁是空闲的(available 为 true),线程就拿走钥匙,并将状态改为不可用(available 为 false),然后进入临界区。
- 如果锁已经被别人拿走了,想要进入的线程就会被阻塞,直到锁被释放。
- 释放锁 (release()):线程执行完任务离开临界区时,必须调用这个函数把钥匙还回去(available 重新设为 true)。
核心特性:
- 原子操作 :
acquire()和release()通过硬件指令(如 Test-and-Set)实现不可中断性 - 等待策略 :
- 自旋锁 :线程会像陀螺一样在原地不停地循环检查锁是否可用。
- 优点:不需要进行复杂的"上下文切换",如果锁很快就会被释放,这种方式效率极高。
- 缺点:它会持续占用 CPU 资源,浪费计算周期,这种现象被称为"忙等" (Busy Waiting)。
- 睡眠锁 :如果锁不可用,操作系统会让该线程进入"睡眠"状态(挂起),并把它放入锁的等待队列中。适合长临界区
- 优点:释放了 CPU 资源,让别的任务去运行。
- 缺点:当锁可用时,唤醒线程和切换上下文需要消耗较多的时间。
- 自旋锁 :线程会像陀螺一样在原地不停地循环检查锁是否可用。
应用示例(POSIX):
c
pthread_mutex_t lock;
pthread_mutex_lock(&lock); // 进入临界区
/* 操作共享数据 */
pthread_mutex_unlock(&lock); // 离开临界区
二、信号量(Semaphores)
信号量是功能更强的同步工具,可视为资源计数器 S,支持两种原子操作:
python
def P(S): # wait()
S -= 1
if S < 0:
block() # 资源不足则阻塞
def V(S): # signal()
S += 1
if S <= 0:
wakeup() # 唤醒等待进程
类型与用途:
- 二元信号量 :S \in {0,1}
- 功能等价于互斥锁,实现互斥访问
- 计数信号量 :S \geq 0
- 管理多实例资源(如 N 个数据库连接)
等待机制优化:
- 阻塞替代忙等:进程进入等待队列,释放 CPU
- 唤醒策略:V 操作自动唤醒等待进程
风险防范:
- 操作顺序:P 必须在 V 前执行,否则破坏互斥
- 资源泄漏:忘记 V 操作将导致死锁
信号量(详细介绍)
一个"共享自习室的管理系统"。
信号量本质上是一个整数变量 S,代表可用资源的数量。
- 计数值的含义:它告诉系统还有多少个"位置"可以使用。
- 例子:如果自习室有 5 个空位,信号量 S 的初始值就是 5。
P 和 V
为了保证数据安全,不能直接修改这个数字,只能通过两个"原子操作"(即不可分割、不会被中途干扰的操作)来访问它:
- P 操作(也叫 wait())------"申请资源"逻辑:想进自习室。会先检查有没有位子。如果有(S>0),就把 S 减 1 然后坐下开始学习;如果没位子了(S≤0),就必须在门口排队等候。
- 口诀:有位子就占,没位子就等。
- V 操作(也叫 signal())------"释放资源"逻辑:学完了要离开。把 S 加 1。如果此时门口有人在排队(在某些实现中,S 加 1 后若仍 ≤0),系统会立刻从等待队列里叫醒一个人,让他进去坐下。
- 口诀:学完腾位子,叫醒后来人。
信号量的两种"变身"
- 二元信号量 (Binary Semaphore) :计数值只能是 0 或 1。它就像一把唯一的钥匙,作用和互斥锁 (Mutex) 几乎一样,保证同一时间只有一个进程能操作共享数据。
- 计数信号量 (Counting Semaphore) :计数值可以是任意非负整数。它专门用来管理有多个实例的资源,比如 3 台打印机或 10 个网络连接。
常见的使用风险
- 顺序写反:先执行了 V 再执行 P,可能会导致多个进程同时闯入临界区,造成数据损坏。
- 忘记释放 :占着资源(执行了 P)却忘了归还(执行 V),排队的人将永远等待下去,引发死锁 (Deadlock)。
简单总结: 信号量就是一个带排队机制的智能计数器,它能确保有限的资源被有序、安全地分配给多个竞争者。
总结
| 机制 | 适用场景 | 核心优势 | 潜在缺陷 |
|---|---|---|---|
| 互斥锁 | 单一资源互斥访问 | 实现简单、低开销 | 不支持资源池管理 |
| 信号量 | 多资源池或复杂同步逻辑 | 灵活控制资源数量 | 操作错误易引发死锁 |
通过合理选用互斥锁或信号量,可有效消除竞态条件,确保并发程序的正确性与稳定性。