第十二章 并发编程
如果逻辑控制流在时间上是重叠,那么它们就是并发的(concurrent)
。这种常见的现象称为并发(concurrency)
。
- 硬件异常处理程序,进程和Unix信号处理程序都是大家熟悉的例子。
我们主要将并发
看做是一种操作系统内核用来运行多个应用程序的机制。
但是,
并发
不仅仅局限于内核。它也可以在应用程序中扮演重要的角色。例如
Unix
信号处理程序如何允许应用响应异步事件
- 例如:用户键入
ctrl-c
- 程序访问虚拟存储器的一个未定义的区域
- 例如:用户键入
其他情况
访问慢速
I/O
设备- 当一个应用程序正在等待来自慢速
I/O
设备(例如磁盘)的数据到达时,内核会运行其他进程,使CPU保持繁忙。
- 当一个应用程序正在等待来自慢速
与人交互
- 和计算机交互的人要求计算机有同时执行多个任务的能力。
通过推迟工作以降低延迟
- 有时,应用程序能够通过推迟其他操作和并发执行它们,利用
并发
来降低某些操作的延迟
- 有时,应用程序能够通过推迟其他操作和并发执行它们,利用
- 服务多个网络客户端
- 一个慢速的客户端可能会导致服务器拒绝为所有客户端提供服务。
- 在多核机器上进行并行运算
使用应用级并发的应用程序称为并发程序(concurrent program)
.
操作系统提供三种基本的构造并发程序的方法:
进程
每个逻辑控制流 都是一个
进程
- 由内核来调度和维护。
因为
进程
有独立的虚拟地址空间- 和其他进程
通信
,控制流必须使用某种显式的进程间通信(interprocess communication,IPC)
进制
- 和其他进程
I/O
多路复用(暂时不太懂)
- 应用程序在一个进程的上下文中显示地调度它们自己的
逻辑流
。 逻辑流
被模型化为状态机
,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态。- 因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。
- 应用程序在一个进程的上下文中显示地调度它们自己的
- 线程
线程
是运行在一个单一进程上下文中的逻辑流
,有内核调度。
- 像
进程
一样由内核进行调度。 - 而像
I/O
多路复用一流一样共享一个虚拟地址空间。
- 像
12.1 基于进程的的并发编程
一个构造并发服务器的自然方法就是,在父进程中接收客户端连接请求,然后创建一个新的子进程来为每个新客户端提供服务。
- 服务器正在监听一个监听描述符(
描述符3
)上的连接请求 - 服务器接收
客户端1
的连接请求 - 并返回一个已连接描述符(
描述符4
)。
- 子进程获得服务器描述符表的完整拷贝(
描述符3,4
) - 子进程关闭它的拷贝中的
监听描述符3
- 服务器关闭描述符表中的
描述符4
- 之后新的客户端又类似之前两个步骤。
12.1.1 基于进程的并发服务器
用
Signal(SIGCHLD,sigchld_handler)
回收僵死进程。- 具体细节见
8.5.7
- 具体细节见
28
行,33
行 父子进程各自关闭他们不需要的拷贝。因为文件表项的引用计数,直到父进程关闭它的描述符,才算结束一次
连接
12.1.2 关于进程的优劣
对于在父,子进程间共享状态信息,进程有一个非常清晰的模型
。
- 共享文件表,但是不共享用户地址空间。
进程
拥有独立的虚拟地址空间即是 优点,也是 缺点。优点
:一个进程
不可能不小心覆盖另一个进程的虚拟存储空间。- 消除许多令人迷惑的错误。
缺点
:独立的地址空间使得进程间共享信息也很困难。必须使用显式的
IPC
(进程间通信)机制。往往还比较
慢
- 进程控制和
IPC
的开销都很大。
- 进程控制和
12.2 基于I/O多路复用的并发编程(暂时跳过)
假设要编写一个echo服务器
。
服务器
既能响应客户端
的请求- 也能对用户从标准输入输出的交互命令做出反应(如
exit
).
因此,服务器
必须要响应两个相互独立的I/O
事件
- 网络客户端发起连接
- 用户在键盘键入命令行。
无论先等待那个事件都不是理想的,解决办法之一是就是使用I/O多路复用技术
。
- 基本的思路
- 使用
select
函数,要求内核挂起进程,只有一个或多个I/O
事件发生后,才将控制返回给应用程序。
- 使用
12.3 基于线程的并发编程
线程(thread)
就是运行在进程上下文中的逻辑流。
- 线程由
内核
调度。 每个线程都有它自己的
线程上下文(thread context)
.- 包括一个唯一的整数
线程ID(Thread ID,TID)
. - 栈和栈指针
- 程序计数器
- 通用目的寄存器和条件码
- 包括一个唯一的整数
所有运行在该进程里的
线程
共享该进程的整个虚拟地址空间。- 共享 包括代码,数据,堆,共享库和打开的文件。
12.3.1 线程执行模型
每个进程开始生命周期时都是单一线程,这个线程称为
主线程(main thread)
。- 某时刻,主线程创建一个
对等线程(peer thread)
。
- 当主线程执行一个慢速系统调用,例如
read
或sleep
- 或者被系统的
间隔计时器
中断。 - 控制就会通过上下文切换传递到
对等线程
。 对等线程
执行一段时间,将控制传递回主线程。
- 当主线程执行一个慢速系统调用,例如
- 某时刻,主线程创建一个
在某些方面,
线程
执行是不等同于进程的。线程
的上下文切换的开销比进程
的小得多,快得多线程
不是按照严格的父子层次来组织。
- 和一个进程相关的线程组成一个
线程池(pool)
。
线程池
概念的主要影响是- 一个线程可以杀死它的任何对等线程,或等待任意
对等线程
终止。 - 每个对等线程都能读写相同的共享数据。
- 和一个进程相关的线程组成一个