进程。用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护,因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信(interprocess communication,IPC)机制。
I/O多路复用。在这种形式的并发编程中,应用程序在一个进程的上下文中显式的调度它们自己的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式的从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。
线程。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。你可以把线程看成是其他两种方式的混合体,像进程流一样由内核进行调度,而像I/O多路复用流一样共享同一个虚拟地址空间。
基于I/O多路复用的并发编程
I/O多路复用(I/O multiplexing)技术,基本的思路就是使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
select是一个复杂的函数,有许多不同的使用场景。我们只讨论第一种场景:等待一组描述符准备好读。
select函数处理类型为fd_set的集合,也叫做描述符集合。逻辑上,我们将描述符集合看成一个大小为n的位向量:bn-1,...,b1,b0。
每个位bk对应于描述符k。当且仅当bk=1,描述符k才表明是描述符集合的一个元素。只允许你对描述符集合做三件事:1)分配它们,2)将一个此种类型的变量赋值给另一个变量,3)用FD_ZERO、FD_SET、FD_CLR和FD_ISSET宏来修改和检查它们。
针对我们的目的,select函数有两个输入:一个称为读集合的描述符集合(fdset)和该读集合的基数(n)(实际上是任何描述符集合的最大基数)。select函数会一直阻塞,直到读集合中至少有一个描述符准备好可以读。当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符k就表示准备好可以读了。select有一个副作用,它修改参数fdset指向的fd_set,指明读集合的一个子集,称为准备好集合(ready set),这个集合是由读集合中准备好可以读了的描述符组成的。该函数返回的值指明了准备好集合的基数。注意,由于这个副作用,我们必须在每次调用select时都更新读集合。
基于线程的并发编程
到目前为止,我们已经看到了两种创建并发逻辑流的方法。在第一种方法中,我们为每个流使用了单独的进程。内核会自动调度每个进程,而每个进程有它自己的私有地址空间,这使得流共享数据很困难。在第二种方法中,我们创建自己的逻辑流,并利用I/O多路复用来显式的调度流。因为只有一个进程,所有的流共享整个地址空间。第三种方法---基于线程,它是这两种方法的混合。
线程(thread)就是运行在进程上下文中的逻辑流。在本书里迄今为止,程序都是由每个进程中一个线程组成的。但是现代系统也允许我们编写一个进程里同时运行多个线程的程序。线程由内核自动调度。每个线程都有它自己的线程上下文(thread context),包括一个唯一的整数线程ID(Thread ID,TID)、栈、栈指针、程序计数器、通用目的寄存器和条件码。所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间。
基于线程的逻辑流结合了基于进程和基于I/O多路复用的流的特性。同进程一样,线程由内核自动调度,并且内核通过一个整数ID来识别线程。同基于I/O多路复用的流一样,多个线程运行在单一进程的上下文中,因此共享这个进程虚拟地址空间的所有内容,包括它的代码、数据、堆、共享库和打开的文件。