同步互斥
独立程序
1、不和其他程序共享资源
2、输入状态决定结果,具有确定性
3、可重现起始条件
4、调度顺序不重要
并发进程
1、多个进程间有资源共享,可能会因为不同顺序出现相互的干扰
2、不确定性
3、不可重现
4、不确定性、不可重现导致 bug 是间歇性发生的
进程 / 线程合作优点
1、共享资源
2、加速:IO 操作、计算可以重叠,多处理器将程序分成多个部分并行执行
3、模块化:将大程序分解成小程序,使系统易于扩展
竞态条件
1、系统缺陷:结果依赖于并发执行或者事件的顺序 / 时间
2、避免竞态:原子操作(Atomic Operation):指一次不存在任何中断或者失败的操作,要么执行完成,要么没有执行,不会中途停止、部分执行
临界区
相互感知的程度 | 交互关系 | 进程间的影响 |
相互不感知 | 独立 | 一个进程的操作对于其他进程的结果没有影响 |
间接感知(双方都与第三方方交互,如共享资源 ) | 通过共享进行协作 | 一个进程的结果依赖于共享资源的状态 |
直接感知(双方直接交互,如通信) | 通过通信进行协作 | 一个进程的结果依赖于从其他进程获得的信息 |
1、同步问题的解决方案主要基于间接感知的部分,其中主要会有如下三种状态
(1)互斥(mutual exclusion):一个进程占用资源,其他进程无法使用
(2)死锁(deadlock):多个进程各占用部分资源,形成循环等待
(3)饥饿(starvation):其他进程轮流占用资源,一个进程一直得不到资源
2、最基本的状态称为互斥,是一个良好的原子操作的思路
3、临界区:互斥资源存放的位置,其上资源同时只允许一个进程的访问,临界区附近的代码结构如下:
(1)进入区:检查是否可以进入临界区的一段代码,如果可以进入,则设定“正在进入临界区”标志
(2)临界区:进程中访问临界资源的一段需要互斥执行的代码
(3)退出区:清除“正在访问临界区”标志
(4)剩余区:跟同步互斥无关的代码
4、临界区的访问规则,必须满足前三条规则
(1)空闲则入:没有进程在临界区时,任何进程可以进入
(2)忙则等待:有进程在临界区时,其他进程均不能进入临界区
(3)有限等待:等待访问临界区的进程不能无限等待
(4)让权等待 / 无忙等待(可选):等待访问临界区的进程,访问之前会被挂起
实现方案
1、禁用中断
2、软件方法
3、高级抽象
禁用中断
1、禁止硬件中断响应,不会发生上下文切换,因此没有并发
(1)硬件将中断处理延迟到中断启用之后
(2)现代计算机体系结构都提供指令来实现禁用中断
2、进入临界区,禁用中断;离开临界区,开启中断
3、缺点
(1)禁用中断之后,进程无法停止,整个系统都会为此停下来,可能导致其他进程处于饥饿状态
(2)临界区可能很长,无法确定中断响应时间,可能存在硬件影响
(3)多个 CPU 情况下,CPU 只能禁用自身的中断,无法禁用其他 CPU 中断,无法解决同步互斥,即只能用于单个 CPU
软件方法
1、设置一些全局的共享变量,来标识两个线程对互斥资源的占用情况,从而实现同步
2、Peterson 算法
(1)复杂,需要两个进程间的共享数据项
(2)需要忙等待,浪费 CPU 时间
3、Dekker 算法
(1)针对双线程
(2)绝大多数的线程都会卡在里层循环,只有当前线程执行完之后释放资源,才将下一个 turn 轮到的线程唤醒
4、n 线程的 Eisenberg 和 McGuire 算法
(1)可以实现有效的轮流
(2)但多线程、多临界区的 debug 会变得更加困难
5、Bakery 算法
(1)n 个进程的临界区
(2)进入临界区之前,进程接收一个数字
(3)得到的教字最小的进入临界区
(4)如果进程 Pi 和 Pj 收到相同的数字,如果 i < j,Pi 先进入临界区,否则 Pj 先进入临界区
(5)编号方案总是按照枚举的增加顺序生成数字
高级抽象
1、锁:数据结构
(1)二进制状态:锁定、解锁
(2)主要 API
Lock::Acquire():锁被释放前一直等待,然后得到锁
Lock::Release():释放锁,唤醒任何等待的进程
(3)使用锁封装相关的互斥操作,控制临界区访问
2、原子操作指令
(1)通过特殊的内存访问电路
(2)针对单处理器和多处理器
(3)Test-and-Set(测试和置位):从内存中读取值,测试该值是否为 1,然后返回 true 或 false,内存值设置为1
(4)Exchange(交换):交换内存中的两个值
4、Test-and-Set 实现 Lock::Acquire()
(1)如果锁被释放,即 test-and-set 读取 0,并将值设置为 1,锁被设置为忙并且需要等待完成
(2)如果锁处于忙状态,即 test-and-set 读取 1,并将值设置为 1,不改变锁的状态并且继续循环(自旋锁)
class Lock {
int value = 0;
}
Lock::Acquire() {
while (test-and-set());//spin
}
Lock::Release() {
value = 0;
}
(3)增加实现无忙等待
class Lock {
int value = 0;
WaitQueue q;
}
Lock::Acquire() {
while (test-and-set()) {
add this TCB to wait queue q;
schedule();//执行调度算法
}
}
Lock::Release() {
value = 0;
remove one thread t from q;
wakeup(t);
}
5、Exchange 实现
int key;
do {
key = 1;
while (key == 1) {
exchange(lock,key);
}
//critical section
lock=0;
//remainder section
}
6、优点
(1)适用于单处理器,或共享主存的多处理器中任意数量的进程
(2)简单并且容易证明
(3)可以用于支持多临界区
7、缺点
(1)忙等待消耗处理器时间
(2)当进程离开临界区,并且多个进程在等待时,可能会导致饥饿
(3)可能会导致死锁:一个低优先级进程拥有临界区(拥有独占资源),一个高优先级进程拥有 CPU,低优先级无法释放锁,高优先级等待临界区
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战