第十二章 并发编程
如果逻辑控制流在时间上重叠,那么它们就是并发的。
使用应用级并发的应用程序称为并发程序,三种基本的构造并发程序的方法:进程、I/O多路复用、线程
12.1基于进程的并发编程
第一步:服务器接受客户端的连接请求
第二步:服务器派生一个子程序为这个客户端服务
第三步:服务器接受另一个连接请求
第四步:服务器派生另一个子程序为新的客户端服务
12.1.1 基于进程的并发服务器
必须要包括一个SIGCHLD处理程序,来回收僵死子程序的资源
为避免存储器泄露,必须关闭各自的connfd拷贝
直到父子进程的connfd都关闭了,到客户端的的连接才会终止
12.1.2 关于进程的优劣
父子进程间的共享状态信息模型:共享文件表,但是不共享用户地址信息
有独立的地址空间:优点:一个进程不可能不小心覆盖另一个进程的虚拟存储器
缺点:共享状态信息变得困难,必须使用显式的IPC机制;比较慢
12.2 基于I/O多路复用的并发编程
服务器必须响应两个相互独立的I/O事件
- 网络客户端发起连接请求
- 用户在键盘上键入命令行
基本思路:使用select函数,要求内核挂起进程
Select函数有两个输入:1.读集合的描述符集合 2.基数n
当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符k就表示准备好可以读了
12.2.1 基于I/多路复用的并发事件驱动服务器
I/O多路复用可以用做并发事件驱动程序的基础
一个状态机就是一组状态、输入事件和转移
自循环是同一输入和输出状态之间的转移
活动客户端的集合维护在一个pool结构里
在通过调用init_pool初始化池之后,服务器进入一个无限循环
在循环的每次迭代中,服务器调用select函数来检测两种不同类型的输入事件:
- 来自一个新客户端的连接请求到达
- 一个已存在的客户端的已连接描述符准备好可以读了
Init_pool函数初始化客户端池
Add_client函数添加一个新的客户端到活动客户端池中
Check_clients函数回送来自每个准备好的已连接描述符的一个文本行
- I/O多路复用技术的优劣
优点:1.比基于进程的设计给了程序员更多对程序行为的控制
2.每个逻辑流都能访问该进程的全部地址空间
缺点:1.编码复杂 2.不能充分利用多核处理器
12.3基于线程的并发编程
线程就是运行在进程上下文中的逻辑流
每个线程都有它自己的线程上下文,包括一个唯一的整数线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码
所有运行在一个进程里的线程共享该进程的整个虚拟地址空间
12.3.1线程执行模型
每个进程开始生命周期时都是单一线程,这个线程称为主线程
在某一时刻,线程创建一个对等线程,从这个时间点开始,两个线程就并发地运行
最后,因为主线程执行一个慢速系统调用,控制就会通过上下文切换传递到对等线程
对等线程执行一段时间,然后控制传递回主线程
12.3.2Posix线程
Posix线程是在C程序中处理线程的一个标准接口
Pthreads运行程序创建、杀死和回收线程,与对等线程安全地共享数据
12.3.3创建线程
Pthread_create函数
创建一个新的线程,并带着一个输入变量arg
在新线程的上下文中运行线程例程f
当其返回时,参数tid包含新创建线程的ID
12.3.4终止线程
方法:1.顶端线程例程返回(隐式)
2.调用pthread_exit函数(显式)
3.某个对等线程调用Unix的exit函数
4.另一个对等线程通过以当前线程ID作为参数调用pthread_cancle函数
12.3.5 回收已终止线程的资源
调用pthread_join函数等待其他线程终止
会阻塞,直到线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源
12.3.6分离线程
Pthread_detach分离可结合线程tid
线程能够通过以pthread_self()为参数的pthread_detach调用来分离它们
12.3.7 初始化进程
Pthread_once
12.4多线程程序中的共享变量
12.4.1 线程存储器模型
一组并发进程运行在一个进程的上下文中
每个线程都有它自己独立的线程上下文
12.4.2 将变量映射到存储器
全局变量是定义在函数之外的变量
本地自动变量就是定义在函数内部但是没有static属性的变量
本地静态变量是定义在函数内部并有static属性的变量
12.4.3 共享变量
一个变量v是共享的,当且仅当它的一个实例被一个以上的变量引用
12.5 用信号量同步线程
一般而言,没有办法预测操作系统是否将为线程选择一个正确的顺序,可以借助进度图来阐明
12.5.1 进度图
进度图将n个并发线程的执行模型化为一条n维笛卡儿空间中的轨迹线
每条轴k对应于进程k的进度
每个点I代表进程已经完成了指令I这一状态
图的原点对应于没有任何线程完成一条指令的初始状态
进度图将指令执行模型化为一种状态到另一种状态的转换
合法的转换是向右或者向上的
操作共享变量cnt内容的指令(L,U,S)构成了一个临界区
两个临界区的交集称为不安全区
绕开不安全区的轨迹线叫做安全轨迹线,反之,叫做不安全轨迹线
12.5.2 信号量
信号量s是具有非负整数的全局变量,只能由两种操作处理。
P(s):如果s是非零的,那么P将s减1返回,如果s为零,就挂起
V(s):将s加1,如果有任何线程阻塞在P操作等待s变成非零,那么V操作会重启线程中的一个,然后s减1
12.5.3 使用信号量来实现互斥
基本思想:将每个共享变量与一个信号量s联系起来,用P和V操作将临界区包围起来
二元信号量的值总是0或1
以提供互斥为目的的信号量也称为互斥锁
P:加锁 V:解锁
一个被用作一组可用资源的计数器的信号量称为计数信号量
关键思想:创建禁止区
12.7 其他并发问题
12.7.1 线程安全
四个线程不安全函数类:
- 不保护共享变量的函数
- 保存跨越多个调用的状态的函数
- 返回指向静态变量的指针的函数
- 调用线程不安全函数的函数
12.7.2 可重入性
可重入函数:当它们被多个线程调用时,不会引用任何共享数据
12.7.3 在线程化的程序中使用已存在的库函数
见p693图12-39
除了rand和strtok,所以这些线程不安全函数都是第3类的
12.7.4 竞争
当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点,就会发生竞争
12.7.5 死锁
信号量引入运行时的错误,叫做死锁
一组线程被阻塞,等待一个永远也不会为真的条件
互斥锁加锁顺序规则:如果对于程序中每对互斥锁(s,t),给所有的锁分配一个全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放,那么这个程序就是无死锁的。
参考资料:《深入理解计算机系统》