并发不仅是操作系统内核用来运行多个应用程序的机制,还可以在应用程序中扮演重要角色。

现代操作系统提供三种基本的构造并发程序的方法:

  • 进程
  • I/O 多路复用
  • 线程

1. 基于进程的并发编程

每个逻辑控制流都是一个进程,由内核来调度和维护。进程有独立的虚拟地址空间,想要和其他流通信,必须使用某种显式的进程间通信(IPC)机制。我们可以使用熟悉的函数,像 fork、exec 和 waitpid 来构造进程。

对于在父、子进程间共享状态信息,进程有个非常清晰的模型:共享文件表,但是不共享用户地址空间。独立的地址空间确保一个进程不会覆盖另一个进程的虚拟内存,消除了许多令人迷惑的错误。同时也使得进程共享状态信息更加困难,为了共享信息,必须使用显示的 IPC 机制,但是进程控制和 IPC 的开销很高。

2. 基于 I/O 多路复用的并发编程

I/O 多路复用(multiplexing)技术,基本思路就是使用 select 函数,要求内核挂起进程,只有在一个或者多个
I/O 事件发生后,才将控制返回给应用程序。select 是个复杂的函数,有许多不同的使用场景。

在事件驱动程序中,某些事件会驱动、导致流的推进。而I/O 多路复用可以作为事件驱动程序的基础。在事件驱动程序中,逻辑流被模型化为状态机,一个状态机就是一组状态、输入事件、转移。
当一个输入事件发生,状态机发生转移,变化为另一个状态。

服务器使用 I/O 多路复用技术,借助 select 函数检测输入事件的发生。当每个已连接描述符准备好可读时,服务器就为相应的状态机执行转移。

事件驱动设计的优点是,它比基于进程的设计具有更多对程序行为的控制。另一个优点是,每个逻辑流
都能访问该进程的全部地址空间,这使得在流之间共享数据变得容易。

事件驱动设计的缺点是,编码复杂,不能充分利用多核处理器。

3. 基于线程的并发编程

线程就是运行在进程上下文中的逻辑流。每个线程都有自己的线程上下文,包括一个唯一的整数线程 ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有运行在一个进程内的线程共享该进程的整个虚拟地址空间。

同进程一样,线程由内核自动调度,并且内核通过一个整数 ID 来识别线程。同基于 I/O 多路复用的流一样,多个线程运行在单一进程的上下文中,因此共享这个进程虚拟地址空间的所有内容,包括它的代码、数据、堆、共享库和打开的文件。

并发线程执行

每个进程开始生命周期都是单线程,这个线程称为主线程(main thread)。在某一时刻,主线程创建一个对等线程(peer thread),从这个时间点开始,两个线程并发地运行。最后主线程执行一个慢速系统调用,比如 read 或 sleep,或者被系统的间隔计时器中断,控制就会通过上下文切换传递到对等线程。对等线程会执行一段时间,然后控制传递回主线程,依次类推。

线程和进程的不同之处有:

  • 线程的上下文比进程的上下文小得多,所以线程的上下文切换要比进程的上下文切换快得多。
  • 线程不像进程那样,不是按照严格的父子层次组织的,和一个进程相关的线程组成一个对等线程(池)。

对等线程(池)的主要影响是,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。每个对等线程都能读写相同的共享数据。