这章主要介绍了线程和并发的概念
并发
并发执行下,我们需要考虑2种情况。
数据竞争
为什么并发下会有数据竞争问题?
比如最朴素的场景,2个线程同时执行i++,最终的结果总是不如预期,实际上,是因为i++本身在执行时是3条汇编指令:
mov 0x8049a1c, %eax
add $0x1, %eax
mov %eax, 0x8049a1c
比如现在要执行到50+1,此时当线程T1执行该指令到一半,add执行完后,被操作系统调度机制挂起,开始执行T2,T2读到的数据依旧是旧的也就是50,然后T1执行,将内存修改为51,T2再执行,也依然是将内存修改为51,我们就可以看做一次操作“丢失了”。
我们可以看到,并发问题总在多线程更新共享变量时产生
这种场景,书中总结也就是我们之前常说或者听到过的,竞态条件race condition(或者更具体地说,是数据竞态data race)。
包含静态条件的代码,称为:critical section (临界代码)。
处理这种问题的方式就是,我们需要使这段代码mutual exclusion (互斥),也就是提供Atomicity(原子性)
条件变量
并发还有一种场景,即互相等待的场景,T1线程需要等待T2执行完后再执行,为了实现这种效果,我们还需要提供condition variables(条件变量)。
线程
以前上课学的一句话比较经典:
线程是操作系统(或者说CPU)调度(执行)的最小单位,进程是操作系统分配资源的最小单位
线程和进程的主要区别就是,线程之间一定程度上共享进程的页表(也就是进程数据),是更轻量级的执行单位。
线程的几个好处:
- 共享进程页表(即共享内存)。
- 面对进程中的那些I/O任务,使用线程去进行I/O任务会有更好的表现(线程阻塞等待I/O执行,进程中其他线程依旧可以使用CPU计算资源)
线程的缺点:
- 在进程内存中,线程就必须有自己独享的栈以及一些私有数据了thread-local storage。
- 线程切换的上下文依旧很重,进程有PCB,线程也有自己的TCB,并且更多了。
总结
综上,从进程实际要执行的事情以及日常的任务(I/O)来看,线程这一轻量级执行单元的诞生和大规模使用基本是必然的。尽管进程也可以达到如此效果,但是又要做到进程之间的内存共享会是一个更麻烦的事,并且也会破坏进程自己的定义和内存完整性。
同时,结合之前来看,cow(写时复制)对于线程的创建起到了多大的便利。
以及,在大量使用线程并行执行的情况下,解决并发问题是多么的重要。