第十三课:线程(Thread)

一、先回顾:什么是进程?

我们已经学过:

进程 = 正在运行的程序。

例如:

你打开:

微信

操作系统会:

创建PCB

↓

分配内存

↓

分配资源

↓

进入就绪队列

↓

开始运行

所以:

微信:

就是:

一个进程。


二、一个问题

假设:

现在:

微信正在运行。

它同时:

聊天

↓

下载图片

↓

播放语音

↓

接收消息

请问:

这些事情:

是不是:

严格一个接一个做?

如果:

下载图片的时候。

聊天必须暂停。

用户会觉得:

微信:

非常卡。

所以:

需要:

同时做很多事情。


三、是不是开很多个进程?

一种办法:

聊天:

一个进程

下载:

一个进程

语音:

一个进程

可以吗?

可以。

但是:

问题来了。

每个进程:

都有:

PCB

内存

页表

文件描述符

资源……

是不是:

太浪费了?

例如:

三个微信进程。

都需要:

加载:

微信程序。

重复占用很多资源。


四、于是:线程诞生了

操作系统设计者想到:

既然这些功能都属于同一个微信,为什么不共享资源?

于是:

把一个进程:

拆成:

很多:

线程(Thread)。

所以:

现在:

微信:

变成:

微信进程
│
├── 聊天线程
├── 下载线程
├── 网络线程
├── 语音线程
└── 界面线程

这些:

都是:

线程。


五、什么是线程?

教材定义:

线程是CPU调度和分派的基本单位。

这一句话:

很多同学第一次看。

完全不知道什么意思。

我们换成人话。

以前:

CPU:

调度:

QQ进程

↓

浏览器进程

现在:

CPU:

调度:

聊天线程

↓

下载线程

↓

播放线程

所以:

以前:

CPU调度:

进程。

现在:

CPU真正调度的是:

线程。

这句话非常重要。


六、为什么线程叫"轻量级进程"?

很多教材会说:

线程是轻量级进程(Lightweight Process)。

为什么?

来看对比。


一个新进程:

需要:

创建PCB

↓

申请内存

↓

建立页表

↓

建立文件表

↓

建立资源

是不是:

很麻烦?

而:

线程:

只需要:

创建TCB(线程控制块)

↓

加入调度

资源:

直接:

共享。

所以:

线程:

创建速度:

非常快。

切换:

也快。

因此:

叫:

轻量级进程。


七、线程共享什么?

这是考试最喜欢问的问题。

假设:

微信:

一个进程。

里面:

五个线程。

它们:

共享:

代码(程序)

数据(全局变量)

堆(Heap)

打开的文件

内存空间

例如:

聊天线程:

下载了一张图片。

下载线程:

放到:

内存。

聊天线程:

立刻:

就能看到。

为什么?

因为:

共享内存。


八、线程独有的是什么?

虽然共享很多东西。

但是:

每个线程:

也必须有自己的内容。

例如:

聊天线程:

运行到了:

第100行代码。

下载线程:

运行到了:

第500行。

是不是:

不能:

共用:

程序计数器?

否则:

CPU不知道:

恢复到哪里。

所以:

每个线程:

都有:

程序计数器(PC)

寄存器

栈(Stack)

线程状态

这些:

不能共享。


九、一张图彻底理解

微信进程
        ┌────────────────────┐
        │ 代码区             │
        │ 数据区             │
        │ 堆(Heap)           │
        │ 打开的文件         │
        └────────────────────┘
             ▲      ▲      ▲
             │      │      │
      ┌──────┘      │      └──────┐
      │             │             │
┌──────────┐ ┌──────────┐ ┌──────────┐
│聊天线程  │ │下载线程  │ │界面线程  │
│PC        │ │PC        │ │PC        │
│寄存器    │ │寄存器    │ │寄存器    │
│栈        │ │栈        │ │栈        │
└──────────┘ └──────────┘ └──────────┘

记住:

上面共享。

下面独立。


十、为什么线程切换更快?

我们以前学过:

进程切换:

需要:

保存PCB

↓

切换页表

↓

切换地址空间

↓

恢复PCB

很慢。

而:

线程切换。

因为:

共享:

代码

内存

页表

所以:

不用:

重新建立。

只需要:

保存:

线程现场

↓

恢复:

另一个线程现场

因此:

线程切换:

快得多。


十一、进程和线程对比(★★★★★)

这是考试一定会考的表格。

比较内容 进程 线程
定义 资源分配的基本单位 CPU调度的基本单位
是否拥有独立地址空间 没有(共享进程地址空间)
创建开销
切换开销
通信 较复杂(IPC) 简单(共享内存)
崩溃影响 一般不影响其他进程 一个线程出错可能导致整个进程崩溃

十二、一个非常经典的例子

为什么:

Chrome:

一个浏览器。

打开一个网页。

任务管理器里面:

却看到:

很多线程?

因为:

浏览器:

通常会:

主线程

↓

渲染线程

↓

网络线程

↓

JavaScript线程

↓

GPU线程

大家:

一起工作。

所以:

浏览器:

很流畅。


十三、为什么多线程会带来新问题?

如果:

两个线程:

同时:

修改:

余额

例如:

账户:

100元。

线程A:

取:

50

线程B:

同时:

取:

80

如果:

没有协调。

最后:

余额:

可能:

变成:

50

20

甚至:

-30

都有可能。

为什么?

因为:

两个线程:

同时:

访问:

共享数据。

于是:

新的问题:

出现了。

这就是:

线程同步(Synchronization)

也是操作系统最经典的一章。

“为什么两个线程同时执行,会算错结果?”

如果这一课理解了,后面的:

都会变得非常容易。


这一课我们先不讲任何公式。

只讲:

为什么需要同步?


第一部分:一个银行案例

假设:

你的银行卡里面有:

100 元

现在:

你同时:

微信支付

+

支付宝支付

都要扣:

50 元。

正常结果:

100

↓

50

↓

0

余额:

应该:

0 元

没有问题。


第二部分:真正发生了什么?

其实。

CPU执行:

不是:

一句话。

而是:

很多条指令。

例如:

微信:

实际上:

可能这样。

① 读取余额

② 计算余额

③ 写回余额

支付宝:

也是:

① 读取余额

② 计算余额

③ 写回余额

注意!

这里:

问题来了。


第三部分:最经典的错误

假设:

CPU:

先执行:

微信。

第一步:

余额:

100

微信:

读取:

100。

但是:

还没有:

写回。

CPU:

突然:

切换。

去执行:

支付宝。

支付宝:

读取:

余额。

注意。

此时:

余额:

还是:

100

因为:

微信:

还没:

写回。

于是:

支付宝:

计算:

100-50

=

50

写回。

余额:

变成:

50

然后。

CPU:

又回来。

继续:

微信。

微信:

认为:

余额:

还是:

100。

因为:

它:

之前:

已经:

读出来了。

于是:

微信:

也写回:

50

最后:

余额:

是多少?

50

但是:

明明:

扣了:

两次:

50。

正确答案:

应该:

0

是不是:

出错了?


第四部分:为什么出错?

因为:

两个线程:

同时:

修改:

同一块数据。

这种情况:

叫:

竞争条件(Race Condition)

教材定义:

多个线程(或进程)同时访问共享数据,结果依赖执行顺序。

记住:

关键词:

同时。


第五部分:共享数据叫什么?

例如:

银行卡余额。

多个线程:

都能访问。

这种资源:

叫:

临界资源(Critical Resource)

例如:

银行卡余额

打印机

共享变量

共享文件

共享内存

都是:

临界资源。

为什么?

因为:

一次:

只能:

一个线程:

修改。


第六部分:访问共享资源的代码

来看代码。

balance = balance - 50;

你觉得:

是不是:

一条语句?

实际上:

CPU:

会拆成:

读取 balance

↓

减50

↓

写回 balance

所以:

这一段代码:

必须:

连续执行。

不能:

执行到一半。

被别人:

插进来。

这段代码:

叫:

临界区(Critical Section)

教材定义:

访问临界资源的代码。

记住:

不要混。

很多同学:

分不清。


临界资源 vs 临界区

这是考试最喜欢出的选择题。

名称 是什么
临界资源 被共享的数据或设备
临界区 操作临界资源的那段代码

例如:

余额

属于:

临界资源。

而:

balance = balance - 50;

属于:

临界区。


第七部分:怎么解决?

最简单的方法。

规定:

一次:

只能:

一个线程:

进入。

例如:

微信:

正在:

修改余额。

支付宝:

必须:

外面:

等待。

直到:

微信:

完成。

才能:

进入。

于是:

执行顺序:

变成:

微信

↓

全部执行完成

↓

支付宝

是不是:

就不会:

错?

这就是:

互斥(Mutual Exclusion)


第八部分:什么叫同步?

很多人:

容易混。

其实:

同步:

不是:

一起执行。

而是:

按照规定的顺序执行。

例如:

打印。

必须:

编辑完成

↓

才能打印。

不能:

文件:

还没写好。

打印机:

已经:

开始打印。

这就是:

同步。


第九部分:同步和互斥区别(★★★★★)

这是最容易考的地方。

互斥

两个线程:

抢:

同一个资源。

例如:

微信

↓

余额

↑

支付宝

必须:

一个一个来。

关键词:

竞争。


同步

例如:

生产者:

先生产。

消费者:

后消费。

必须:

有顺序。

关键词:

协作。


一张图理解

互斥:

A ----\
        \
         临界资源
        /
B ----/

同步:

生产

↓

放入缓冲区

↓

消费者取走

一个:

解决:

竞争。

一个:

解决:

先后顺序。


第十部分:进程同步四大原则(★★★★★)

教材:

一定会讲。

必须记住。


① 空闲让进

如果:

没人:

使用。

那么:

别人:

应该:

马上:

进去。

不能:

空着。


② 忙则等待

如果:

已经:

有人:

在里面。

别人:

必须:

等待。

不能:

一起。


③ 有限等待

不能:

一直:

等。

否则:

有人:

永远:

进不去。

就是:

饥饿。

所以:

必须:

保证:

最终:

轮到。


④ 让权等待

如果:

不能:

进入。

不要:

一直:

占CPU。

应该:

睡觉。

等:

别人:

叫醒。

这个原则:

后面:

信号量:

会详细讲。


第十一部分:生活例子

厕所。

只有:

一个坑位。

是不是:

典型:

临界资源?

有人:

进去。

别人:

只能:

外面:

排队。

是不是:

互斥?

等:

里面:

出来。

外面:

进去。

是不是:

同步?

整个过程:

是不是:

满足:

四个原则?

这就是:

操作系统。


第十二部分:为什么现代程序越来越重视同步?

因为:

现在:

CPU:

越来越多核。

例如:

8核

16核

32核

多个线程:

真的:

同时:

运行。

如果:

没有:

同步。

程序:

很容易:

数据错乱。

所以:

现代编程:

几乎:

离不开:

同步。


第十三部分:本课重点(★★★★★)

必须记住

竞争条件(Race Condition)

多个线程同时访问共享数据,结果依赖执行顺序。


临界资源

一次:

只能:

一个线程:

访问。


临界区

访问临界资源:

那段代码。


互斥

解决:

竞争问题。

一次:

一个线程。


同步

解决:

协作顺序。

谁先。

谁后。


第十四部分:一句口诀

一定要背下来:

资源叫临界,

代码叫临界区;

抢资源叫互斥,

按顺序叫同步。


Logo

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

更多推荐