130 / 163 · C11 · 约 8 分钟
并发:简介
先记住这句话
本章用全新视角介绍线程:同一进程内部可以同时存在多条独立执行流,它们共享整块地址空间。每条线程自带程序计数器和寄存器,切换时不必更换页表。每个线程还拥有私有栈。引入线程主要是为了利用多核真正并行,以及在部分线程等待I/O时让其余线程继续前进。
线程究竟是什么
可以把线程想象成进程内部的轻量级执行流。它拥有独立的程序计数器和一整套寄存器,因此操作系统调度器能在线程之间来回切换,动作很像进程切换。最大的不同在于:同一进程的所有线程看见的是同一份虚拟内存,全局变量和堆上的数据对它们完全可见,访问起来非常直接。
为什么要使用线程
今天的机器通常有好几个处理器核心。把一个巨大的计算任务切成几块,每块交给一条线程,这些块就能真正同时运行。另一条重要理由是:当某条线程因为磁盘读写或网络收发而暂停时,同一进程里的其他线程仍然可以占用CPU继续干活,整个程序不会完全停下来。
共享地址空间里的多份栈
经典的单线程进程只有一个栈,通常放在地址空间的低端。一旦变成多线程,每条线程都必须有自己的栈,用来保存局部变量、函数参数和返回地址。这些栈被安放在共享虚拟地址空间的不同角落,而堆仍然只有一份,所有线程都可以向它申请内存。
常见误区
- 误以为线程一定会按照被创建的先后顺序去执行
- 在后续章节讲同步之前就对共享变量又读又写
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <pthread.h>
int results[2] = {0, 0};
void *thread_one(void *unused) { (void)unused;
results[0] = 10 + 20;
return NULL;
}
void *thread_two(void *unused) { (void)unused;
results[1] = 30 + 40;
return NULL;
}
int main(void) {
pthread_t t1, t2;
printf("main: begin\n");
pthread_create(&t1, NULL, thread_one, NULL);
pthread_create(&t2, NULL, thread_two, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("result0=%d result1=%d\n", results[0], results[1]);
printf("main: end\n");
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-26-threads.c -o example && ./example预期结果
main: begin
result0=30 result1=70
main: end
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
线程之间的上下文切换为什么不必更换页表?
查看参考答案
因为同一进程的所有线程已经共享同一份地址空间,硬件页表寄存器保持不变即可。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。