第十二章 并发编程 学习笔记
第十二章 并发编程
进程是程序级并发,线程是函数级并发。
三种基本的构造并发程序的方法:
- 进程:每个逻辑控制流是个一个进程,由内核进行调度和维护。
- I/O多路复用:应用程序在一个进程的上下文中显式地调度他们自己的逻辑流。
- 线程:运行在单一进程上下文中的逻辑流,由内核进行调度。
12.1 基于进程的并发编程
构造并发程序最简单的方法就是用进程。
使用大家都很熟悉的函数例如:
- fork
- exec
- waitpid
关于在父、子进程间共享状态信息:共享文件表,但不共享用户地址空间。
进程又独立的地址空间既是优点又是缺点:
- 优点:防止虚拟存储器被错误覆盖
- 缺点:开销高,共享状态信息才需要IPC机制
12.2 基于I/O多路复用的并发编程
就是使用select函数要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
select函数处理类型为fd_set的集合,也叫做描述符集合。
select函数有两个输入:一个称为读集合的描述符集合和该妒忌和该读集合的基数(n)(实际上是任何描述符集合的最大基数)。select函数会一直阻塞,直到读集合中至少有一个描述符准备好可以读。当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符K就表示准备好可以读了。
作为一个副作用,select修改了参数fdset指向的fd_set,指明读集合中一个称为准备好集合的子集。函数返回的值指明了准备好的集合的基数。由于这个副作用,我们必须在每次调用select函数时都更新集合。
I/O多路复用可以用作并发事件驱动程序的基础。
服务器使用I/O多路复用,借助select函数检测输入事件的发生。
12.3 基于线程的并发编程
线程就是运行在进程上下文中的逻辑流。
基于线程的逻辑流结合了基于进程和基于I/O多路复用的流的特性。
每个进程开始生命周期时都是单一线程,这个线程称为主线程。在某一时刻,主线程创建一个对等线程,从这个时间点开始,两个线程就并发地运行。
在一些重要的方面,线程执行是不同于进程的,因为一个线程的上下文比一个进程的上下文小很多,线程的上下文切换比进程的上下文快得多。
另一个不同就是线程不像进程那样,不是按照严格的父子层次来组织的。和一个进程相关的线程组成一个对等(线程)池,独立于其他线程创建的线程。
主线程总是进程中第一个运行的线程。对等(线程)池的概念的主要影响是,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。另外,每个对等线程都能读写相同的共享数据。
Posix线程是C程序中处理线程的一个标准接口。基本用法是:
- 线程的代码和本地数据被封装在一个线程例程中
- 每个线程例程都以一个通用指针为输入,并返回一个通用指针。
这里需要提到一个万能函数的概念。
万能函数:
void func(void parameter)
typedef void (uf)(void para)
即,输入的是指针,指向真正想要传到函数里的数据,如果只有一个就直接让指针指向这个数据,如果是很多就将它们放到一个结构体中,让指针指向这个结构体。后面这个方法就是万能函数的使用思想。
线程例程也是这样的。
创建线程:
终止线程:
回收已终止线程的资源:
分离进程:
初始化线程:
12.4 多线程程序中的共享变量
一个变量是共享的,当且仅当多个线程引用这个变量的某个实例。
一组并发线程运行在一个进程的上下文中。
每个线程都有它自己的线程上下文:
- 一个唯一的整数线程ID——TID
- 栈
- 栈指针
- 程序计数器
- 通用目的寄存器
- 条件码
线程化的C程序中变量根据他们的存储类型被映射到虚拟存储器:
- 全局变量
- 本地自动变量
- 本地静态变量
变量v是共享的——当且仅当它的一个实例被一个以上的线程引用。
12.5 用信号量同步线程
共享变量十分方便,但也引入了同步错误的可能性。
这里有个关键点:一般而言,你没有办法预测操作系统是否将为你的线程选择一个正确的顺序。
进度图:
进度图是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态。
当n=2时,状态比较简单,是比较熟悉的二维坐标图,横纵坐标各代表一个线程,而转换被表示为有向边
转换规则:
- 合法的转换是向右或者向上,即某一个线程中的一条指令完成
- 两条指令不能在同一时刻完成,即不允许出现对角线
- 程序不能反向运行,即不能出现向下或向左
而一个程序的执行历史被模型化为状态空间中的一条轨迹线。
信号量:可以解决同步不同执行线程问题,具有非负整数值的全局变量,只能由两种特殊的操作来处理,这两种操作被称为:
12.7 其他并发问题
当用线程编写程序时,我们必须小心地编写那些具有称为线程安全性属性的函数。
一个函数被称为线程安全的当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。
如果一个函数不是线程安全的,我们就称为线程不安全的。
有一类重要的线程安全函数,叫可重入函数,当他们被多个线程调用时,不会引用任何共享数据。
可重入的包括显式可重入函数和隐式可重入函数。