并发编程:进程、IO 多路复用、线程
00 并发(concurrent)
并发:逻辑控制流在时间上有重叠,换言之,两个相对独立的逻辑控制流在时间上不是严格的先后次序,而是有一次或多次的交替。
- 内核级并发
- 操作系统用来运行多个应用程序的机制
- 应用级并发
- 访问慢速 IO 设备
- 与人交互
- 通过推迟工作以降低延迟
- 服务多个网络客户端
- 在多核机器上进行并行计算
应用级并发程序称为并发程序。现代操作系统提供了三种基本构造方法:
- 进程。将并发逻辑流放在不同的进程中,进程由内核来调度和维护。因为进程有独立的虚拟地址空间,不同进程下的并发逻辑流必须使用显式的进程间通讯(IPC)机制。
- IO 多路复用。应用程序在同一个进程的上下文中显式地调度不同并发逻辑流。不同并发逻辑流被模型化为状态机,进程在不同状态之间切换,所有的并发逻辑流共享同一个地址空间。
- 线程。线程是运行在一个单一进程上下文中,同样由内核进行调度。可以把线程看作是以上两种方式的混合体,像进程流一样由内核调度,像 IO 多路复用一样共享同一个虚拟地址空间。
01 基于进程的并发编程
使用进程构造并发是最简单的。
以并发 Web 服务器为例。父进程负责监听一个监听描述符,在有连接请求时,会创建一个子进程,父子进程间拷贝监听描述符和已连接描述符。我们的目标就是让父进程只只负责监听,让子进程只负责处理 已连接 socket 的通讯,所以需要在父进程关闭已连接描述符,在子进程关闭监听描述符。
要注意一点的是,在父进程中关闭已连接描述符至关重要,否则将会很快耗尽系统资源,因为若父进程不主动关闭已连接描述符,子进程即使通信结束,也无法关闭父进程已连接描述符。
另外,还有一点必须考虑并正确妥当地处理好:回收僵死进程。这是因为服务器的目标一般是要运行超长时间,在运行期间会不可避免地出现僵死进程。
总结起来,基于进程的并发 Web 服务器,是通过共享文件表实现,我们只需要处理好描述符的管理即可,具体调度由内核负责。因为这样,基于进程的并发在实现起来比较容易,逻辑也比较简单,但进程控制和共享信息的 IPC 机制的开销都很高,所以往往比较慢。
关键概念是:描述符,文件表,内核调度,IPC。
02 基于 IO 多路复用的并发编程
基本思路是使用 select 函数,要求内核挂起进程,只有在一个或多个 IO 事件发生后,才将控制返回给应用程序。示例如下:
- 当集合 {0, 4} 中任意描述符准备好读时,返回
- 当集合 {3, 6, 8} 中任意描述符准备好写时,返回
- 等待一个 IO 事件发生过了 XX 秒,就超时
select 函数是一个复杂的函数,有很多使用场景。此处只涉及一个场景:等待一组描述符准备好读。
select 函数一直阻塞,直到 read_set 中有描述符变为可读,之后调用相应的处理函数。
IO 多路复用可以作为事件驱动程序的基础,在事件驱动程序中,逻辑流是因为某件事情而前进的。一般是将逻辑流模型化为状态机,粗略而言,状态机由一组 state、input event 和 transition 组成,其中 transition 就是将 state 和 input event 映射到 state。
状态机可以用有向图来表示,其中节点表示状态,有向弧表示转移,弧上的标号代表输入事件。
IO 多路复用下的并发给了程序员对于程序更强的控制力,同时由于在一个进程下,效率提升明显,但是一个明显的缺点是编码复杂,代码量大且不容易控制逻辑流的粒度大小。此外,不能很好地利用多核处理器。
核心:select 检测可读描述符到待读描述符的状态变化,以此为基础划分状态机。状态机的三个组成,状态,输入事件,转移。在转移中运行业务逻辑流的同时将当前状态和输入事件映射到对应状态,触发下一个状态机里的转移。
03 基于线程的并发编程
现代操作系统中,允许同一个进程上下文中存在多个线程。事实上,进入一个进程后,会首先创建一个主线程,不同于进程间严格的父子关系,主线程与其创建的子线程之间是对等关系,共同存在于一个线程池中,线程之间的切换也是由内核负责,但不同于多进程并发,同一个进程下的多个线程虽有自己的 TID(Thread ID)、栈、栈指针、程序计数器、通用目的寄存器和条件码,但会共享地址空间内的所有内容,包括代码、数据、堆、共享库和打开的文件。
一个线程上下文要比一个进程上下文小得多,切换也快得多。对等线程概念下,一个线程可以杀死对等线程,也可以等待对等线程终止。再次说明,每个对等线程都能读写相同的共享数据。
多线程编程中的基础概念:
- 主线程。进程下第一个线程。
- 子对等线程。由主线程创建的一个与自己对等的子线程。
- 概念上对等,但实际使用时,区别对待。
- 每一个子线程都被封装为一个 routine。
- 调用线程创建函数创建并运行子线程。
- 设置子线程为可结合或可分离,默认为可结合。
- 终止线程的几种方式:
- 线程内部 routine 历程返回时,线程会 隐式地终止。
- 调用 thread_exit 线程终止函数,显式终止,当主线程调用它时,会等待所有其他对等线程终止,然后在终止自己和整个进程。
- 任意线程调用 exit 函数时,进程和相关线程将被终止。
- 另一个对等线程调用以当前线程 ID 为参数的 thread_cancle 函数可终止当前线程。
- thread_join 函数会等待某一线程终止,并回收线程资源。
- 可分离线程一旦运行将无法被杀死或回收,将在运行终止时由系统释放所占磁盘资源
不知是该恭喜,还是该怎样,总之阅读到该文的,你是第 人。每一次刷新,都是不同的自己。