CSAPP并发编程
逻辑流:pc值的序列叫逻辑控制流,简称逻辑流
并发:多个逻辑流执行时相互重叠,称为并发流,这种现象叫做并发(concurrency)
并行:并行流是并发流的真子集,两个流并发运行在不用的核或计算机上称为并行流
应用程序实现并发的三种方法
进程
每个逻辑控制流都是一个进程,有其独立的虚拟地址空间
- 服务器在收到客户端请求后生成子进程单独处理,处理完后要回收僵尸进程
- 不同进程间不共享数据,所以需要用IPC进行进程间通讯
- 父进程和子进程都有
listenfd和connfd,所以在父进程中需要关闭connfd,在子进程中需要关闭listenfd
优点:独立的地址空间使一个进程不会覆盖别的进程的虚拟存储器
缺点:1. 共享信息更困难;2. 依靠IPC开销较高,会比较慢
I/O多路复用
逻辑流被模型化为状态机,数据到达文件描述符后,主程序显示地从一个状态转到另一个状态,程序是一个单独的进程,所有的流都共享一个地址空间
类似于进程池,服务器维护一个connection 数组,包含若干个connfd,每个输入请求被当做一个事件,每次从已有事件选择一个进行处理
优点:只使用一个逻辑控制流和地址空间,可以利用调试器进行单步调试(其他的方法因为并行的缘故基本没办法调试),也不会有进程/线程控制的开销。
缺点:代码的逻辑复杂度会比较高,很难进行精细度比较高的并行,也无法发挥多核处理器的全部性能
线程
是运行在一个单一进程上下文中的逻辑流,由内核调度,像是前两种的混合体
每个线程有自己的线程上下文,包括一个唯一的线程ID,栈,栈指针,程序计数器,通用目的寄存器和条件码
每个进程开始生命周期时都是单一线程,这个线程称为主线程,它可以创建对等线程,然后它们便可以并发地运行了。主线程和其他线程唯一的区别是它总是进程中第一个运行的线程
线程上下文切换比进程快得多,而且不是按照严格的父子层次来组织的,与一个进程相关的线程组成一个对等池,独立于其他(进程的)线程创建的线程。对等池的概念:一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止,同时对等线程能读写相同的共享数据
优点:容易共享资源,但也容易出现问题,开销比进程小
缺点:对具体的调度可控性较低,难以调试,因为事件发生的顺序不一致
并发中的同步问题
什么是共享变量
线程存储器模型
- 独立的线程上下文(线程 ID,栈,栈指针,PC,条件码,GP 寄存器)
- 共享的进程上下文部分,包括整个用户虚拟地址空间(只读文本,读/写数据,堆,共享库代码和数据区域)
- 共享同样的打开文件集合
在实际的模型中,寄存器的值虽然是隔离且被保护的,但是在栈中的值并不是这样的(其他线程也可以访问)
将变量映射到存储器
- 全局变量:VM的读/写区域只包含每个全局变量的一个实例,任何线程都可以引用
- 本地自动变量:在运行时,每个线程的栈都包含它自己的所有本地自动变量的实例
- 本地静态变量:VM读/写区域只包含在程序中声明的每个本地静态变量的一个实例,每个对等线程能读写该实例
共享变量
当一个变量的一个实例被一个以上的线程引用时,称为共享的
信号量
共享变量可能引起同步错误,因为我们没有办法预测操作系统所执行的指令的顺序,但可以使用信号量来限制程序的执行顺序来解决
利用进度图可以看到线程的执行顺序,临界区交集的区域称为不安全区,绕开不安全区的轨迹叫做安全轨迹线,使用信号量可以保证得到一条安全轨迹线

计数信号量具备两种操作动作,称为 V(又称signal())与 P(wait())。 V 操作会增加信号量 S 的数值,P 操作会减少。基本思想就是将每个共享变量与一个信号量s(初始化为1)联系起来,然后通过P和V操作将相应的临界区包围起来。以这种方式来保护共享变量的信号量叫做二元信号量,因为它的值总是0或者1。以提供互斥为目的的二元信号量常常也成为互斥锁。在一个互斥锁上执行P操作称为加锁。V操作称为解锁。

除了提供互斥之外,信号量还可以调度对共享资源的访问,一个线程用信号量操作来通知另一个线程,程序中的某个条件已经为真了。经典的使用例子有生产者-消费者问题,读者-写者问题
线程安全
一个程序被多个并发线程调用时并一直产生正确的结果,那么该程序就是线程安全的
在并发中,被线程调度的函数必须具有一种称为线程安全的属性
四类线程不安全的函数
- 不保护共享变量的函数
- 解决办法:使用 P 和 V semaphore 操作
- 问题:同步操作会影响性能
- 在多次调用间保存状态的函数
- 解决办法:把状态当做传入参数
- 返回指向静态变量的指针的函数
- 解决办法1:重写函数,传地址用以保存
- 解决办法2:上锁,并且进行复制
- 调用线程不安全函数的函数
- 解决办法:只调用线程安全的函数
可重入函数:被多个线程调用时不使用共享变量。它是线程安全函数非常重要的子集,不需要同步操作。上面第二类函数的解决办法就是把他们修改为可重入函数
竞争
就是轨迹进入了不安全区,为了消除竞争,可以动态地为每个线程ID分配一个独立的块,但要注意释放这些块以避免存储器泄露