2.3.2 基本的实现方法
在并发环境中,同步与互斥的实现需要严谨的逻辑保证。软件方法通过进程间的逻辑约定协调资源访问,硬件方法则利用处理器的原子指令确保操作的不可分割性。两种方法各有其适用场景,深入理解它们的实现细节,有助于掌握更复杂同步机制的设计原理。
一、软件方法:通过逻辑规则协调临界区访问
软件方法不依赖专门的硬件支持,而是通过共享变量和约定的逻辑规则,实现多个进程对临界资源的互斥访问。其核心挑战是在无硬件干预的情况下,保证临界区访问的排他性、安全性和公平性。最经典的软件方法包括Peterson算法和Dekker算法,其中Peterson算法因简洁性和正确性被广泛用作教学案例。
1. Peterson算法:双进程互斥的经典实现
Peterson算法专为两个进程的互斥设计,通过两个共享变量和简单的逻辑判断,完美满足互斥的三个基本要求(空闲让进、忙则等待、有限等待)。
为直观理解其实现,我们用C语言描述算法的核心逻辑。假设系统中有两个进程P0和P1,它们通过以下共享变量协调临界区访问:
-
int turn
:表示当前允许进入临界区的进程编号(0或1),用于冲突时的优先级判断; -
bool flag[2]
:数组元素flag[i]
表示进程i是否准备进入临界区(true
表示准备进入)。
进程Pi(i为0或1,j为另一个进程的编号,即j=1-i)的代码实现如下:
// 共享变量初始化
int turn = 0;
bool flag[2] = {false, false};
// 进程Pi的代码
void process_i() {
flag[i] = true; // 声明当前进程准备进入临界区
turn = j; // 将"优先权"让给对方进程
// 循环等待:直到对方未准备进入,或当前轮到自己进入
while (flag[j] && turn == j) {
// 忙等:通过循环检测条件,不执行任何有效操作
}
// 进入临界区:访问共享资源
critical_section();
// 退出临界区:重置状态,允许其他进程进入
flag[i] = false;
// 非临界区操作
remainder_section();
}
算法逻辑解析:
- 当进程Pi想要进入临界区时,先通过
flag[i] = true
告知对方“自己需要访问资源”; - 再通过
turn = j
主动让出优先权,体现“谦让”原则; - 循环判断条件
flag[j] && turn == j
的含义是:“如果对方也需要访问资源,且当前优先权属于对方,则自己等待”; - 当对方退出临界区(
flag[j]
变为false
),或优先权通过turn
转移给自己(turn == i
)时,Pi进入临界区; - 退出临界区时,
flag[i] = false
告知对方“资源已释放”。
正确性验证:
- 忙则等待:若P0已进入临界区,则
flag[0] = true
且turn
可能为0或1。此时P1的循环条件flag[0] && turn == 0
会成立(若turn=0
),或因flag[0] = true
持续等待,直到P0退出; - 空闲让进:若临界区空闲(
flag[0]
和flag[1]
均为false
),任何进程设置flag[i] = true
后,因flag[j] = false
,循环条件不成立,可直接进入; - 有限等待:当两进程同时申请进入时,
turn
会被最后设置的进程覆盖(如P0先设turn=1
,P1再设turn=0
),最终turn
的值决定哪个进程优先进入,避免无限等待。
2. 软件方法的局限与扩展
Peterson算法仅适用于两个进程,若扩展到N个进程,逻辑会变得极其复杂(如Dekker算法的N进程扩展需要嵌套循环)。此外,软件方法的“忙等”特性会导致CPU资源浪费——等待的进程持续占用处理器却不推进工作,尤其在多处理器系统中,可能导致临界资源长期被某一进程占用,其他进程空转。
因此,软件方法更多用于理论验证,实际系统中很少直接采用。但它揭示的“共享变量协调”思想,为后续高级同步机制提供了基础。
二、硬件方法:利用原子指令保证操作的不可分割性
硬件方法的核心是通过处理器提供的原子指令(Atomic Instruction)实现临界区访问控制。原子指令是指“执行过程不可被中断”的指令,即指令从开始到结束的整个过程中,不会有其他进程或中断插入,从而避免了“读-改-写”操作的中间状态被其他进程干扰。
常见的原子指令包括“测试并设置(Test-and-Set)”和“交换(Swap)”,它们被广泛用于操作系统内核的同步机制实现。
1. 测试并设置(Test-and-Set)指令
测试并设置指令的功能是:读取指定内存单元的值,然后将该单元设置为新值,整个过程不可中断。在x86架构中,该指令通过LOCK BTS
(带锁的位测试并设置)或LOCK CMPXCHG
(带锁的比较交换)实现。以下是基于GCC编译器的C语言封装实现(利用__sync_lock_test_and_set
原子操作函数):
// 定义共享锁变量,0表示空闲,1表示占用
int lock = 0;
// 原子操作函数:测试并设置锁
// 返回值:操作前的锁状态(0表示空闲,1表示占用)
int test_and_set(int *lock, int value) {
return __sync_lock_test_and_set(lock, value);
}
// 进程访问临界区的代码
void access_critical_section() {
// 循环等待,直到获取锁
while (test_and_set(&lock, 1) == 1) {
// 忙等:锁被占用时循环检测
}
// 进入临界区
critical_section();
// 释放锁:将锁重置为0
lock = 0;
}
逻辑解析:
- 共享变量
lock
是临界资源的“访问令牌”:lock=0
表示资源空闲,lock=1
表示资源被占用; - 进程想要进入临界区时,调用
test_and_set(&lock, 1)
:若锁原本为0(空闲),则函数返回0,进程退出循环进入临界区;若锁为1(被占用),则函数返回1,进程持续循环等待; - 进程退出临界区时,将
lock
设为0,释放资源,允许其他进程获取锁。
原子性保证:__sync_lock_test_and_set
函数通过硬件锁总线(LOCK前缀)确保“读取旧值”和“设置新值”两个操作不可分割,避免了多个进程同时读取到“空闲”状态而导致的冲突。
2. 交换(Swap)指令
交换指令的功能是:交换两个内存单元的值,且整个过程不可中断。在x86架构中,可通过LOCK XCHG
指令实现。以下是基于交换指令的互斥实现(同样使用GCC原子操作):
// 共享锁变量,0表示空闲,1表示占用
int lock = 0;
// 原子交换函数:交换a和b的值
void swap(int *a, int *b) {
__sync_swap(a, b); // GCC提供的原子交换函数
}
// 进程访问临界区的代码
void access_critical_section() {
int key = 1; // 局部变量,用于标记当前进程的申请状态
// 循环交换,直到获取锁
do {
swap(&lock, &key); // 交换锁和key的值
} while (key == 1); // 若key为1,说明锁被占用,继续等待
// 进入临界区
critical_section();
// 释放锁
lock = 0;
}
逻辑解析:
- 进程通过局部变量
key=1
表示“自己需要访问资源”; - 每次调用
swap(&lock, &key)
时,若lock
为0(空闲),交换后key=0
,进程退出循环进入临界区;若lock
为1(被占用),交换后key=1
,进程继续循环; - 退出临界区时,
lock=0
释放资源,其他进程的swap
操作会将key
置为0,从而获得访问权。
3. 硬件方法的优势与局限
硬件方法的优势在于:
- 通用性:支持任意数量的进程,无需针对特定场景设计复杂逻辑;
- 高效性:原子指令由硬件直接支持,执行速度快,比软件方法的循环判断更高效;
- 安全性:原子性保证从根本上避免了竞争条件,无需担心中间状态被干扰。
但硬件方法仍存在“忙等”问题:等待锁的进程会持续执行原子指令,占用CPU资源。为解决这一问题,实际系统中常将硬件原子指令与“阻塞-唤醒”机制结合(如后续章节的信号量):当进程获取锁失败时,由操作系统将其阻塞(放入等待队列),而非空转;当锁释放时,再唤醒等待队列中的进程,从而提高CPU利用率。
三、软件方法与硬件方法的对比与应用
维度 | 软件方法(以Peterson为例) | 硬件方法(以Test-and-Set为例) |
依赖条件 | 无专门硬件支持,仅需共享变量 | 依赖处理器的原子指令支持 |
适用进程数 | 仅支持少量进程(如2个) | 支持任意数量进程 |
等待方式 | 忙等(循环检测共享变量) | 忙等(循环执行原子指令) |
实现复杂度 | 逻辑复杂,扩展到N进程难度大 | 逻辑简单,无需复杂判断 |
实际应用 | 多用于教学和理论验证 | 操作系统内核同步的基础(如Linux的futex) |
在实际系统中,软件方法和硬件方法并非孤立使用。例如,Linux内核的futex(快速用户空间互斥体)机制就结合了两者的优势:用户态通过原子指令尝试获取锁,若失败则进入内核态阻塞,既保证了轻量场景下的高效性,又避免了忙等导致的资源浪费。这种“软硬结合”的思路,是现代操作系统同步机制的设计核心。
理解这些基本实现方法,不仅能帮助我们掌握同步与互斥的本质,更能为后续学习锁、信号量等高级机制奠定基础——它们本质上都是对软件或硬件方法的封装与优化,旨在平衡正确性、效率与易用性。