-
新的抽象:线程
-
线程是什么
- 在经典观念中,一个程序只有一个执行点(一个程序计数器,用来存放要执行的指令),但多线程(Multi-threaded)程序则有多个执行点(多个程序计数器)
-
线程与进程的异同
- 多线程类似于多进程,不过不同在于:多个线程共享同一块地址空间
- 对于线程调度,CPU也需要进行上下文切换
- 对于进程,我们需要将进程状态保存至进程控制块(PCB, Process control block)
- 而对于线程,我们则需要线程控制块(TCB, Thread control block)
- 对于地址空间中的内容,单线程程序只会有一个栈,而多线程程序会有多个栈
- 每个线程会有一个自己的栈,用于存放线程自己的东西,这些栈也被称为线程本地(Thread-local)存储
-
-
并发的问题:不可控的调度
-
多线程程序中的问题
#include <iostream> #include <thread> // 全局变量cnt,两个线程都会对它进行修改 int cnt = 0; // 线程函数,每个线程都会执行这个函数来增加cnt的值 void increaseThreadFunction() { // 每个线程都执行1000000次“给cnt+1”的操作 for (int i = 0; i < 1000000; i++) { cnt++; } } // 主函数,创建两个线程来执行增加cnt的操作,并等待它们完成 int main() { // 创建两个线程,都执行increaseThreadFunction函数 std::thread thread_1(increaseThreadFunction); std::thread thread_2(increaseThreadFunction); // 等待两个线程执行完毕 thread_1.join(); thread_2.join(); // 当两个线程都执行完毕后,输出cnt std::cout << "cnt = " << cnt << std::endl; // 最终输出的cnt值可能小于2000000 // 程序结束,返回值为0 return 0; }- 实际输出:
cnt = 1123662 - 这是因为某个操作在执行时发生了上下文切换,导致结果被错误的计算,例如
- 线程A和线程B被创建,假设CPU让线程A先开始运行
- A在内存中拿到了cnt的值,此时为0,然后在CPU中进行了运算“0+1”,得到结果为1
- 正当线程A要用1覆盖掉内存中cnt的值时,操作系统给线程A叫停了,要去执行线程B
- 线程B就开始运行:取指令、取cnt的值、0+1=1,得到结果为1
- 线程B将结果“1”在内存中覆盖掉原先的cnt
- 线程B结束,接下来线程A运行:线程A计算的结果是1,于是现在,线程A就用1覆盖cnt的值
- 结果,cnt为1,而不是2
- 实际输出:
-
-
原子性愿望
- 为了防止并发的不可控问题,我们需要一条更加强大的指令,这个指令是以原子方式(Atomically) 执行的
- 一个以原子方式(Atomically) 执行的指令是在被中断时,要么已经执行完毕,要么根本没有运行,也就是“全部或没有”
- 这些指令的粒度需要比较小,它们的集合是同步原语(Synchronization primitive)
-
并发术语
-
临界区
- 临界区(Critical section) 是访问共享资源的一段代码,资源通常是一个变量或一个数据结构
-
竞态条件
- 竞态条件(Race condition) 出现在当多个执行线程大致同时进入临界区时。此时它们都试图更新共享的资源,导致了意料之外的结果
-
不确定性
- 一个程序由一个或多个竞态条件组成,运行的结果是不确定的,这就是一个不确定性(Indeterminate) 程序
-
互斥执行
- 程序应当使用一些互斥(Mutual exclusion)原语来保证一时间只有一个线程进入临界区,避免产生竞态,从而产生确定的结果输出
-
操作系统 - Operating System
/
Chapter III 并发
/
Section 1 线程
/
1-1 并发与线程