1. 什么是线程
在上一篇进程里已经讲到,可以将一个程序里互不影响又能单独拆分出来的任务放到新进程里执行,但每个进程都有独立的地址空间,资源是互相隔离的(如果不用共享内存的话),那么如果想用多个进程同时处理一个文件怎么办呢?由此引入了Thread线程的概念。线程就是进程中的一段代码片段,该代码片段可以和其他代码片段并发执行,除了一些线程独有的执行信息,线程能够共享进程的所有资源。
2. 为什么需要线程
- 可以将一个进程中IO操作和CPU操作分离开,既能共享地址空间和文件数据又能提升效率
比如一个文件编辑的应用,用一个线程处理用户输入,一个线程定时写入硬盘实现自动保存,一个线程在后台做拼写检查。如果只有一个线程,保存时就不能处理输入或检查,只能顺序处理。 - 创建线程的开销远低于进程
- 充分利用多核CPU或多CPU计算机的计算能力
3. 线程模型
经典线程模型
除了线程执行过程信息是线程独有的,其他都是和process共享的。下表第二列是线程独有的信息,称作Thread Control Block (TCB):program counter是线程要执行的下一条指令的地址;registers保存了当前执行指令的数据或访存地址等;方法调用栈中每一个栈帧就是一个未返回的方法调用,保存了局部变量和返回地址;线程的状态的进程一样也分为running, block, ready和terminated.
Per-process items | Per-thread items |
---|---|
Address space | Program counter |
Global variables | Registers |
Open files | Stack |
Child processes | State |
Pending Alarms | |
Signals and signal handlers | |
Accounting information |
线程的生命周期
- 创建线程:thread_create
- 终止线程:thread_exit
- 等待其他线程终止:thread_join,当前线程进入block状态
- 主动放弃CPU:thread_yield, 当前线程进入ready状态
POSIX 线程
为了能编写出标准化的可移植的多线程程序,IEEE定义了一个标准,Pthreads. 这个标准定义了60多个方法,我们介绍其中几个主要的:
- Pthread_create
创建一个线程并返回线程标识符 - Pthread_exit
终止一个线程并释放它的stack - Pthread_join
当前线程进入block状态,等待其他线程执行结束 - Pthread_yield
主动放弃CPU使用权,进入ready状态 - Pthread_attr_init
创建一个线程相关的属性结构,并初始化为默认值 - Pthread_attr_destroy
删除线程的属性结构信息,释放内存资源,但线程本身仍然存在
4. 线程实现
线程可以在用户空间或内核空间实现,也可以混合实现,接下来我们分别讨论不同的实现及其优缺点。
在用户空间实现线程
线程完全在用户空间实现,OS 内核对线程的存在一无所知,对OS来说它管理的还是一个个进程。进程需要自己管理线程,维护thread table和TCB。
优点
- 可以在不支持线程的OS上执行
- 线程切换不用切换到内核态执行,节省开销,速度更快
- process可以自定义线程调度算法
缺点
- 实现阻塞式系统调用变得复杂:比如一个线程等待键盘输入时,不能调用OS提供的blocking system call,因为这会导致整个process 被block
- page fault会导致process被block: 如果一个线程遇到了缺页错误,OS会将整个process block去做访存操作,即使其他的线程可以处在ready状态。
- 由于时钟中断作用不到process内部,线程之间切换只能依赖于线程主动放弃CPU使用权
在内核空间实现线程
线程在kernel实现,由kernel管理thread table和TCB。线程的创建和终止都通过system call实现。
优点
- 可能导致线程block的调用都由system call实现
- 一个thread block之后,调度器可以选择相同process中的其他线程继续执行,也可以选择不同process的线程执行
缺点
- 每次system call切换到kernel开销巨大:针对这个缺点的解决方案是使用线程池,一个线程执行结束之后不直接销毁而是进入idle状态等待新的任务。
混合实现
为了平衡用户空间和内核空间实现线程的优缺点,可以采用在用户空间和内核空间混合实现的方式,开发人员决定使用多少kernel thread 和 user-level thread。
编写多线程程序要考虑的问题
- 如何处理属于某个线程的全局变量,而让其他线程不可见
比如errno. thread1 要打开一个文件前去检查对文件的permission,OS返回结果到errno, 此时CPU使用权转到thread2, thread2执行操作也向全局变量errno写入覆盖了thread1的结果,thread1恢复执行以后去errno取到错误的结果导致执行失败。
解决办法:每个thread维护自己的私有全局变量 - 如何避免多个线程同时进入一个不可重入的方法,比如malloc
- signal问题:比如一个键盘中断发出signal又没有指定线程时,应该由哪个线程来捕获这个信号
- 栈管理问题:一个进程栈溢出时,OS会自动分配stack空间,但如果一个进程拥有多个线程,每个线程都有自己的栈空间,如果kernel不知道这些stack的存在就无法正确地自动分配更多空间。