并发的好处
- 共享资源
- 一台电脑,多个用户
- 一个银行取款余额,多台ATM机
- 嵌入式系统(机器人控制:手臂和手的协调)
- 加速
- I/O操作和计算可以重叠
- 多处理器 - 将程序分成多个部分并行执行
- 模块化
- 将大程序分解成小程序
- 使系统容易扩展
存在的问题
-
竞态条件(race condition)
-
并发程序共享资源时具有不确定性和不可重现性
-
示例:
举例
基本概念
- 临界区(Critical Section):进程中一段需要访问共享资源,且当另一进程处于相应代码区域时便不会被执行的代码区域
- 互斥(Mutual Exclusion):当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并访问任何相同的共享资源
- 死锁(Dead Lock):两个或以上的进程,相互等待对方完成特定任务,而最终没法将自身任务进行下去
- 饥饿(Starvation):一个可执行进程,被调度器持续忽略,以至于虽处于可执行状态却不被执行
临界区的属性
- 互斥
- progress(前进):如果一个线程想进入临界区,那么它最终会成功
- 优先等待:如果一个线程 i 处于入口区,那么在 i 的请求被接收之前,其他线程进入临界区的时间是有限制的(线程 i 不会无限等待)
- 无忙等待(可选):如果一个进程在等待进入临界区,那么它可以在进入前被挂起(忙等会白白占用系统资源)
保护临界区的方法(锁机制)
禁用硬件中断
- 过程
- 进入临界区时,禁用中断,便没有上下文切换
- 离开临界区时,重新启动中断
- 硬件将中断处理延迟到中断启用之后
- 不足
- 一旦中断被禁用,线程无法被停止
- 整个系统都为你停下来
- 其他线程可能处于饥饿状态
- 如果临界区可以任意长怎么办?
- 无法限制响应中断的时间长短
- 可能对存在硬件影响
- 不适用于多CPU的情况。多CPU情况下,禁用一个CPU的中断无法阻止另一个CPU处理中断
- 一旦中断被禁用,线程无法被停止
基于软件的解决方案
-
Peterson算法(双进程)
-
思考
-
内容
int turn; //指示该由谁进入临界区 boolean flag[]; //指示进程是否准备好进入临界区 // 进程Pi的算法: do{ flag[i] = TRUE; turn = j; while(flag[j] && turn == j); // Critical Section flag[i] = FALSE; // Remainder Section }while(TRUE);
-
-
Dekker算法(双进程,较为复杂)
-
Eisenberg and McGuire算法(多进程)
-
Bakery算法(多进程)
-
不足
- 复杂:需要多个进程间的共享数据项
- 忙等浪费CPU时间
- 需要硬件保证:Peterson算法需要LOAD(内存加载到寄存器)和STORE(寄存器存储到内存)是原子操作
基于原子操作指令
-
现代体系结构大多支持特殊的原子操作指令
- 通过特殊的内存访问电路
- 针对单处理器和多处理器
-
两个基本的原子操作
-
Test-and-Set 测试和置位
- 从内存中读取值
- 测试该值是否为1(然后返回True/False)
- 将内存值置1
// 语义 boolean TestAndSet(boolean *target){ boolean rv = *target; *target = TRUE; return rv; }
-
Exchange 交换
- 交换内存中的两个值
// 语义 void Exchange(boolean *a, boolean *b){ boolean temp = *a; *a = *b; *b = temp; }
-
-
锁的实现
-
TestAndSet
/* 忙等待:当临界区较长时,占用较多CPU资源 */ class Lock{ int value = 0; } Lock::Acquire(){ while(test-and-set(value)) ; //忙等 } Lock::Release(){ value = 0; } /* 无忙等待:当临界区较短时,上下文切换代价比忙等更大 */ class Lock{ int value = 0; WaitQueue q; } Lock::Acquire(){ while(test-and-set(value)){ add this TCB to q; schedule(); // CPU调度 } } Lock::Release(){ value = 0; remove one thread t from q; wakeup(t); }
-
Exchange
int lock = 0; //共享数据 // 线程Ti: int key; do{ key = 1; while(key == 1) exchange(lock, key); critical section //临界区代码 lock = 0; remainder section //其他代码 }
-
-
优点
- 适用于单处理器或共享内存的多处理器中的任意数量的进程
- 简单且容易证明
- 支持多临界区
-
缺点
- 忙等消耗CPU资源
- 由于锁分配的随机性,当进程离开临界区,且多个进程等待临界区时,可能导致饥饿
- 死锁:如果一个低优先级进程进入临界区,而一个高优先级进程也有需求,高优先级进程获得处理器并等待临界区 ⇒ 可通过优先级反转解决