第十五课:临界区问题(Critical Section Problem)


一、什么是临界区问题?

上一课我们学了:

例如:

balance = balance - 50;

这段代码:

就是:

临界区。

现在:

两个线程:

都想:

进入。

怎么办?

操作系统:

必须:

决定:

谁先进去?

谁在外面等?

这就是:

临界区问题(Critical Section Problem)

一句话总结:

如何让多个进程(或线程)安全地访问临界资源。


二、一个错误的方法——关闭中断

有同学会想到:

CPU 不就是因为切换线程才出错的吗?

那我:

关闭中断

↓

CPU就不会切换

是不是:

就不会:

出错?

例如:

进入临界区

↓

关闭中断

↓

修改余额

↓

打开中断

听起来很好。

但是:

实际上:

几乎不用。


为什么?

第一:

用户程序:

不能:

随便关闭中断。

否则:

任何程序:

都可以:

让电脑:

失去响应。

这是非常危险的。


第二:

多核CPU。

假设:

有:

CPU1

CPU2

CPU1:

关闭了:

中断。

CPU2:

还能:

继续:

修改余额。

所以:

还是:

出错。

因此:

关闭中断:

不是通用方案。


三、第二种方法——忙等待(Busy Waiting)

假设:

门口:

挂着:

一个牌子。

busy = false

意思:

没人。

于是:

线程A:

看到:

没人。

进去。

然后:

改成:

busy = true

线程B:

来到。

发现:

busy = true

就在门口:

一直:

检查。

while(busy){

    一直等
}

这种方式:

叫:

忙等待(Busy Waiting)


什么叫忙等待?

不是:

睡觉。

而是:

CPU:

一直:

执行:

while(true)

不停:

问:

好了没?

好了没?

好了没?

CPU:

一直:

空转。


忙等待有什么缺点?

例如:

线程A:

进入:

临界区。

需要:

10秒。

线程B:

这10秒:

一直:

循环。

CPU:

是不是:

白白:

浪费了?

所以:

忙等待:

效率:

很低。


四、为什么"检查 busy"也会出错?

很多同学第一次会说:

“busy 不就一个变量吗?”

问题来了。

假设:

开始:

busy = false

线程A:

执行:

if(!busy)

CPU:

刚判断:

没人。

准备:

进入。

突然:

切换。

线程B:

也判断:

busy == false

于是:

两个线程:

一起:

进去。

是不是:

又失败了?

所以:

判断和修改不能分开。


五、原子操作(Atomic Operation)

于是:

提出一个概念:

原子操作(Atomic Operation)

什么叫原子?

来自:

物理学。

意思:

不能再分。

在操作系统中:

表示:

一个操作执行过程中,不会被中断。

例如:

判断busy

+

修改busy

必须:

一次:

完成。

不能:

执行一半。


六、锁(Lock)的思想

于是:

操作系统:

发明:

锁。

生活中:

像这样:

房门

↓

钥匙

只有:

拿到钥匙。

才能:

进去。

别人:

只能:

等。

程序:

也是:

一样。

进入:

临界区。

第一步:

加锁

出来:

解锁

于是:

流程:

加锁

↓

进入临界区

↓

修改数据

↓

解锁

是不是:

很像:

厕所:

锁门?


七、进入区、临界区、退出区、剩余区

教材中经常会出现四个区域。

一定要会区分。

┌──────────────┐
│ 进入区       │
├──────────────┤
│ 临界区       │
├──────────────┤
│ 退出区       │
├──────────────┤
│ 剩余区       │
└──────────────┘

① 进入区(Entry Section)

作用:

申请进入。

例如:

加锁

检查busy

都是:

进入区。


② 临界区(Critical Section)

真正:

修改:

共享数据。

例如:

余额--

库存--

打印

③ 退出区(Exit Section)

作用:

释放资源。

例如:

busy=false

解锁

让:

别人:

进去。


④ 剩余区(Remainder Section)

剩下:

普通代码。

例如:

打印欢迎信息

计算数学题

播放音乐

和:

共享资源:

没关系。


八、临界区问题的三个要求(★★★★★)

这是考试高频考点。


① 互斥(Mutual Exclusion)

任何时候:

最多:

一个线程:

进入:

临界区。

这是:

最基本要求。


② 空闲让进(Progress)

如果:

没人:

进入。

别人:

应该:

立刻:

进去。

不能:

一直:

卡住。


③ 有限等待(Bounded Waiting)

不能:

永远:

等。

例如:

线程A:

进去:

1000次。

线程B:

一次:

都没进去。

这就是:

饥饿。

必须:

保证:

最终:

能进入。


九、教材里的经典图

线程A

进入区

↓

临界区

↓

退出区

↓

剩余区

────────────

线程B

进入区

↓

临界区

↓

退出区

↓

剩余区

只有:

临界区

不能:

同时:

进入。

其它区域:

可以:

并发执行。


十、为什么还要继续学习?

到目前为止。

我们知道:

需要:

互斥

↓

加锁

↓

解锁

但是:

真正的问题来了。

这个:

锁。

到底:

怎么实现?

程序:

怎么知道:

什么时候:

等待?

什么时候:

唤醒?

这就是:

下一课:

信号量(Semaphore)


十一、本课重点总结

必须掌握

✅ 临界区问题:如何让多个线程安全访问临界资源。

✅ 原子操作:执行过程中不可中断。

✅ 锁(Lock):保证同一时刻只有一个线程进入临界区。

✅ 四个区域:

进入区

↓

临界区

↓

退出区

↓

剩余区

三大要求(★★★★★)

牢记:

  1. 互斥 :同一时刻只能有一个线程进入临界区。
  2. 空闲让进(Progress) :临界区空闲时,应允许一个等待线程进入。
  3. 有限等待(Bounded Waiting) :等待时间必须有上界,避免饥饿。

注意:上一课讲的"让权等待"是 同步机制设计的一般原则 ;而教材在"临界区问题"这里通常强调的是 互斥、空闲让进、有限等待 这三个要求。不要混淆。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐