早睡早起,坚持锻炼。
1. IO多路复用
异步编程: 使用select
函数,要求内核挂起进程。只有在一个或多个IO事件发生后,才将控制返回给应用程序。不利于使用多核。
2. 线程
2.1. 线程模型
- 每个进程生命周期开始时都是单一线程。之后某一时刻,主线程创建一个对等线程(peer thread)。之后,两个线程就并发执行。
- 线程不是严格按照父子层次来组织的,和进程相关的每一个线程组成一个对等的线程池。
2.2. Posix线程
创建线程
1 |
|
终止线程
一个线程会在下面的情况下被终止
- 当顶层线程例程返回时,线程会隐式终止
pthread_exit
会显示终止线程- 若主线程调用
pthread_exit
,它会等待其它所有线程终止然后再终止主线程和整个进程
- 若主线程调用
- 某个对等线程调用
exit
会终止整个进程 - 另一个对等线程调用
pthread_cancel
可以终止当前线程
1 |
|
回收已终止线程的资源
1 |
|
注意: pthread_join
只能等待指定线程的终止,无法等待任意线程的终止。
分离线程
在任何时刻,有两种线程
- 可结合的(joinable): 能够被其它线程回收和杀死,在被其它线程回收资源之前,它的内存资源不释放
- 分离的(detached): 不能被其它线程回收和杀死,它的内存资源在它终止时由系统自动释放
pthread_detach
用于分离可结合线程tid
,参数主要通过pthread_self
获取。
1 |
|
初始化线程
pthread_once
主要用于动态初始化多个线程共享的全局变量。
once_control
全局变量总是被初始化为PTHREAD_ONCE_INIT
。当第一次用该参数调用pthread_once
时,它会调用init_routine
函数(无参数也无返回值),之后以once_control
为参数对pthread_once
的调用则什么也不干。
1 |
|
3. 用信号量同步线程
3.1. 生产者-消费者问题
使用三个信号量,一个作为互斥锁保证对缓冲区的互斥访问,另外两个记录空槽位和可用项目的数量。
1 |
|
3.2. 读写者问题
读者优先
使用共享变量readcnt
,通过互斥锁保护,记录读者数量。另外使用writer
互斥锁保证写者唯一。
1 |
|
4. 线程级并行: 性能
强扩展公式
$S_p = \frac{T_1}{T_p}$
- $T_k$: k个核上的运行时间
- 绝对加速比: 当$T_1$表示顺序执行版本的执行时间
- 相对加速比: 当$T_1$表示并行版本在单个核上的执行时间
效率
$E_p = \frac{S_p}{p} = \frac{T_1}{pT_p}$
具有高效率的程序有效工作时间所占百分比更长。可以衡量并行化造成的开销。
弱扩展
增加处理器数量的同时,增加问题的规模。此时的加速比和效率表达为单位时间完成的工作总量。例如: 处理器数量翻倍,同时单位时间处理两倍工作量,则有线性的加速比和100%的效率。
5. 并发问题
5.1. 线程安全
一个函数是线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。
以下是四种线程不安全函数类
- 不保护共享变量的函数
- 保持跨越多个调用的状态的函数
- 例如: 使用函数内部的静态变量或全局变量
- 返回指向静态变量的指针的函数
- 例如:
ctime
和gethostbyname
- 应对: 加锁-复制技术,在每一个调用线程不安全函数位置对互斥锁加锁,将函数返回结果复制到一个私有内存位置,然后对互斥锁解锁
- 例如:
- 调用线程不安全函数的函数
5.2. 可重入性
可重入函数: 当它们被多个线程调用时,不会引用任何共享数据。
注意: 可重入函数集合是线程安全函数的一个真子集。即可重入函数一定线程安全,但是线程安全函数不一定是可重入的。
- 显式可重入函数: 所有数据引用都是本地的自动栈变量,没有引用静态或全局变量。
- 隐式可重入函数: 指针作为参数传递,若调用线程保证指针指向非共享数据,则可重入。
5.3. 竞争
竞争: 一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它控制流中的x点。
例如: 多个线程对同一个共享变量的更新。
5.4. 死锁
死锁: 一组线程被阻塞,等待一个永远也不会为真的条件。