一、背景知识
1. 独立的线程
- 不和其他线程共享资源或状态;
- 确定性:输入状态决定结果;
- 可重现性:能够重新起始条件,I/O
- 调度的顺序不重要
2. 合作的线程
- 在多个线程中共享状态;
- 不确定性;
- 不可重现性;
- 不确定性和不可重现性意味着 bug 可能是间歇性发生的
3. 合作的优点
- 共享资源
- 加速,I/O 和计算可以重叠
- 模块化,使系统易于扩展
4. 合作的要求
- 无论多个线程的指令序列怎样交替进行,程序都必须正常工作;
- 不确定性要求并行程序的正确性。
二、概念
1. 竞态条件
竞态条件(race condition):程序结果依赖于并发执行或者事件的顺序/时间。
2. 原子操作
原子操作(atomic operation)是指一次不存在中断或失败的执行。
操作系统需要利用同步机制在并发执行的同时,保证一些操作是原子操作。
3. 临界区
临界区(Critical Section):进程中的一段需要访问共享资源并且当另一个进程处于相应代码区域时便不会被执行的代码区域。
4. 互斥
互斥(Mutual exclusion):当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并且访问任何相同的共享资源。
5. 死锁
死锁(Dead lock):两个或以上的进程,在相互等待完成特定任务,而最终没法将自身任务进行下去。
6. 饥饿
饥饿(Starvation):一个可执行的进程,被调度器持续忽略,以至于虽然处于可执行状态却不被执行。
三、临界区
1. 临界区的属性
- 互斥
- Progress:如果一个线程想要进入临界区,那么它最终会成功;
- 有限等待:如果一个线程 i 处于入口区,那么在 i 的请求被接受之前,其他线程进入临界区的时间是有限制的;
- 无忙等待(可选):如果一个进程在等待进入临界区,那么在它可以进入之前会被挂起。
2. 禁用硬件中断
- 进入临界区,禁用中断;离开临界区,开启中断;
- 一旦中断被禁用,线程就无法被停止;
- 要是临界区可以任意长,就无法限制响应中断所需的时间。
3. 基于软件的解决方案
- Peterson 算法
- Dekkers 算法
- N 线程的软件方法
- 复杂,需要忙等待
4. 更高级的抽象
原子操作
- Test-and-Set:从内存中读取值,测试该值是否为1(然后返回真或假),内存值设置为1;
- 交换:交换内存中的两个值。
利用Test-and-Set来实现锁
class Lock {
int value = 0;
}
Lock::Acquire() {
while (test-and-set(value))
; //spin
}
Lock::Release() {
value = 0;
}
利用交换实现锁
int key;
do {
key = 1;
while (key == 1) exchange(lock, key);
critical section;
lock = 0;
remainder section
}
优点
- 适用于单处理器或者共享主存的多处理器中任意数量的进程;
- 简单并且容易证明;
- 可以用于支持多临界区。
缺点
- 忙等待消耗处理器;
- 当进程离开临界区并且多个进程在等待的时候可能导致饥饿;
- 死锁:如果一个低优先级的进程拥有临界区并且一个高优先级进程也有需求,那么高优先级进程会获得处理器并等待临界区。