CMU CSAPP笔记 第十二章
并发的基本知识
- 并发的应用
- 访问慢速I/O设备
- 与人交互(键盘输入等)
- 通过推迟部分工作(例如free动态内存)来降低延迟
- 网络服务器同时服务多个客户端
- 并发的经典问题
- 竞争
- 死锁
- A需要B执行才能执行,同时B需要A执行才能执行
- 活锁
- 外部事件或者系统操作一直在子进程之前执行,导致要执行的子进程迟迟不能执行(类似于一直有人插队)
构造并发程序的基本方法
- 基于进程
- 内核自动管理多个逻辑流
- 每个进程有其私有的地址空间(也就是说进程切换的时候需要保存和载入数据)
- 每个客户端由独立子进程处理
- 必须回收僵尸子进程
- 子进程会复制父进程的 listenfd 和 connfd,所以在父进程中需要关闭 connfd,在子进程中需要关闭 listenfd
- 因为服务器只要创建一次 listenfd 即可
- 优点在于各个进程之间独立不会相互干扰
- 缺点在于进程间共享信息困难
-
基于事件
- 由程序员手动控制多个逻辑流
- 所有的逻辑流共享同一个地址空间
- 这个技术称为 I/O multiplexing
-
基于线程
- 内核自动管理多个逻辑流
- 每个线程共享地址空间
- 属于基于进程和基于事件的混合
- 几个相关函数
- pthread_create
- pthread_join
- 阻塞并等待新线程运行结束,回收内存
- 与waitpid不同,该函数只能等待指定的线程,而不能任意线程
- pthread_detach
- 使该线程不能被别的线程回收或杀死
- 基于线程的服务器在新线程中不用像子进程一样关闭一个已连接描述符,因为线程都在同一个进程中
多线程中的共享变量
- 我们说一个变量是共享的,当且仅当它的一个实例被一个以上的线程引用
- 哪怕这个变量不是全局变量,而是在某个线程的栈上的
- 因为虽然每个线程的栈是独立的,但是线程之间不设防,允许被其他线程访问
- 哪怕这个变量不是全局变量,而是在某个线程的栈上的
- 线程同步错误
- 同进程一样,线程的执行顺序也是不固定的,也会发生竞争错误
- 例如一段累加代码,从汇编角度来看,由开头,尾部各一个周期,以及中间三个周期构成。而中间的三个周期的执行如果被打断,就会出现错误
- 因此需要在执行中间周期时保证不被打断
- 进度图
- K个线程的并发可以用K维的进度图来表示,这里我们以最简单的二维为例
- K个线程的并发可以用K维的进度图来表示,这里我们以最简单的二维为例
信号量
- 信号量S是一个非负整数的全局变量,只有P,V两种操作
- 对于P(S),如果S是非零,P将S减一,并且立即返回;如果S为零,挂起线程,直到S非零,通常通过V重启,然后P将S减一并返回
- 对于V(S),V将S加一,但是当有多个线程等待重启时,V不可预测重启哪个
- P和V都是不可被中断的
- 通过P,V可以将共享变量加锁,避免线程竞争错误
- 利用信号量来调度共享资源
- 消费者和生产者问题
- 模型
- 生产者等待空的 slot,把 item 存储到 buffer,并通知消费者
- 消费整等待 item,从 buffer 中移除 item,并通知生产者
- 应用
- 多媒体处理
- 生产者生成 MPEG 视频帧,消费者进行渲染
- 事件驱动的图形用户界面
- 生产者检测到鼠标点击、移动和键盘输入,并把对应的事件插入到 buffer 中
- 消费者从 buffer 中获取事件,并绘制到到屏幕上
- 多媒体处理
- 模型
- 读者和写者问题
- 模型
- 读者线程只读取对象
- 写者线程修改对象
- 多个读者可以同时读取对象
- 应用
- 在线订票系统
- WEB页面缓存
- 实现策略
- 读者优先或写者优先
- 都会出现starvation的现象,即活锁
- 模型
- 消费者和生产者问题
线程安全
- 4 类线程不安全的函数
- 不保护共享变量的函数
- 解决办法:使用 P 和 V 来保护
- 在多次调用间保存状态的函数
- 例如rand函数,通常在传入固定随机数种子时,多次运行的结果是一样的,但是rand函数的下一次输出依赖于上一次输出,这导致多线程时可能无法返回固定结果
- 解决办法:把状态当做传入参数
- 返回指向静态变量的指针的函数
- 被一个线程使用的结果可能被另一个线程悄悄覆盖
- 解决办法1:重写函数,传地址用以保存
- 解决办法2:上锁,并且进行复制
- 调用线程不安全函数的函数
- 解决办法:只调用线程安全的函数
- 不保护共享变量的函数
- 可重入性
- 多线程时不会引用任何共享数据
- 是线程安全函数重要的子集,不需要同步操作,更高效
- 标准 C 库中的函数都是线程安全的(如 malloc, free, printf, scanf),大多数 Unix 的系统调用也都是线程安全的
- 大多数不安全的函数都有可重入版本,名字为原函数名+‘_r’
死锁
- 避免死锁的方法,对于每个线程调用P的顺序一致
- 例如都是P(S0),P(S1)的顺序
- 释放顺序随意
超线程
- CPU在执行单线程任务时,并不是核心内每一个单元都在工作。而超线程技术就是让闲着的那些执行单元去做另一个线程的工作。
- 即共享functional units
- 但是假设有两个线程在某一时刻都要使用CPU中的一个特定执行单元,那么他们俩就没法同时执行了,只能一个一个来。