多线程的进程与单线程的进程相比主要有两个优点:更高的效率和共享的存储器。效率提高源于上下文交换的额外开销减少。上下文交换是指操作系统将CPU从一个运行线程调度到另一个线程所需执行的指令。在线程间切换时,操作系统必须保存原先线程的状态(如寄存器中的值)并读取新线程的状态。同一个进程中的线程共享的进程状态越多,操作系统需要改变的状态就越少。因此,同一个进程中的两个线程间的切换比不同进程中的两个线程快。尤其是因为同一进程中的线程共享个存储器地址空间,进程内的线程切换就意味着操作系统不必改变虚拟存储器映射。
线程的第二个优点是共享存储器,这对于程序员而言比效率提高更为重要。并发服务器中多个服务器副本需要相互通信或访问共享的数据,因此利用线程更容易构造并发服务器。另外,利用线程也更容易构造监控系统。尤其是因为它们共享存储器,一个服务器中的从线程可将统计数据留在共享存储器中,从而使监控线程可把从线程的活动情况报告给系统管理员。稍后我们将给出一个监控系统的实例。
1. 线程的缺点
虽然多线程提供的优点是单线程的进程所欠缺的,但它们也有一些缺点。其中最重要的一点是,由于线程间共享存储器和进程状态,一个线程的动作可能会对同一个进程内的其他线程产生影响。例如,当两个线程试图在同一时刻访问同一个变量时,它们之间就会产生相互于扰。
我们知道线程API提供了协调线程间动作的函数。但是,许多将指钊返回给一个静态数据项的库函数不是线程安全(thread safe)的。也就是说,如果多个线程试图并发调用该库函数,返回结果是不叮预知的。我们以gethostbyname库函数为例加以说明,该函数可用于解析域名并返回相应的IP地址。如果两个线程并发调用gethostbyname,后一次解析的结果将覆盖前一次结果。因此,如果多个线程调用某个库函数,线程之间必须加以协调,确保某个时刻只有一个线程调用该库函数。
另一个缺点是缺乏健壮性。在单线程的服务器中,如果某一个并发服务器的副本造成服务器出错(例如一个非法的存储器引用),操作系统只会终止引发故障的进程。但是在多线程的服务器中,如果一个线程使服务器出错,操作系统将终止整个进程。
2. 描述符、延迟和退出
线程和进程间的关系容易被混淆,特别是那些具有单线程进程编程经验的程序员更易将两者混淆。除了静态资源(例如全局变量),许多动态分配的资源都是与进程相关的(而非单独的线程)。例如,由于文件描述符资源属进程所有,一旦某个线程打开了一个文件,同一个进程中的其他线程也可以使用同一个描述符访问文件。此外,如果某个线程关闭了一个文件描述符,意味着整个进程内的该描述符已被关闭(即该进程中的其他线程不能再访问此描述符)。
类似地,虽然有些操作系统函数只会影响调用它的线程,但有些函数会影响整个进程。例如,如果一个线程进行I/0调用(例如调用read)时被阻塞,只有一个线程会被阻塞。但如果某个线程调用了exit函数,整个进程将立刻退出。也就是说,exit函数是与整个进程而非单个线程相关的。关于以上讨论的要点如下:
虽然一些系统函数只影响调用线程,但有些函数(例如exit)会影响整个进程。
3. 线程退出
如果一个线程不能调用exit终止整个进程,它该如何终止自己而不会终止进程内的其他线程呢?有两种方法:一种是在线程的顶级过程(即线程一开始调用的过程)返回时终止该线程;另一种是调用pthread_exit终止该线程。总结如下:
线程API中包括一些只对线程起作用的函数。例如,线程可调用pthread_exit(或从其顶级过程中返回时)终止自己,而不会影响进程中的其他线程。
4. 线程协调和同步
由于各个线程按照自己的步调并发运行,线程间的同步是必须的。例如,如果一个线程在进行I/O操作时被阻塞,延迟时间长度取决于操作系统和下层的硬件;无法预知在该线程的延迟期间其他线程的行为或者它们将执行哪些指令。因此,程序员希望能协调线程的执行,从而引入了一些函数调用。Linux提供了三种同步机制:互斥(mutex)、信号量(semaphore)和条件变量(conditionvariable:)
4.1 互斥
线程使用互斥可对共享数据项进行排它性访问。通过调用pthread_muelx_init可动态地初始化一个互斥T;程序员可为每个需要保护的数据项都安排一个独立的互斥。一个互斥初始化之后、线程在使用数据项前必须调用pthread_mutex_lock,使用完后再调用pthread_mutex_unlock。这样可确保某一个时刻只有一个线程访问数据项。在一个互斥中,第一个调用pthread_mutex_lock的线程将不受阻挡地继续执行。但在该线程使用数据项的过程期问,后续调用pthread_mulex_lock的其他线程将被阻塞,直到第一个线程使用完数据项并调用了pthread_mutex_unlock后,另一个线程才得以使用该数据项。此时,操作系统将使其中一个阻塞等候的线程回到运行状态。总结如下:
互斥是使线程同步的机制之一。每个互斥与一个数据项相关;任何时刻只有一个线程访问受互斥保护的数据。
4.2 信号量
信号量(有时被称为计数信号量)是一种同步机制,它用于系统中有N个资源可用的情况,是对互斥机制的一种推广。信号量允许N个线程同时执行,而不是像互斥一样在某个时刻只允许一个线程执行通过临界区。
类似互斥,信号量可以动态启动。函数sem_init初始化一个信号量;它带有一个参数N表示可用的资源数初始化一个信号量后,一个线程在使用一个资源前必须调用sem_wait,并在用完后调用sem_post返还资源。N个线程都可在调用em_wait后不会受到影响——每个线程都可在调用后继续执行。但是,如果再有其他线程调用sem_wait,它们就将被阻塞。这些线程将一直处于阻塞状态,直到某个运行线程调用了此信号量上的sem_post后,其中一个阻塞的线程才得以继续运行。总结如下:
信号量是一种线程同步机制,是互斥机制的一种推广形式,任一时刻,至多有N个线程能够访问受到信号量(初始化计数器为N)保护的资源。
4.3 条件变量
最复杂和难以理解的一种同步机制被称为条什变量。实质上,只有一种情况需要条件变量:
- 一组线程使用互斥对同一个资源提供排它性访问。
- 一旦某个线程获得资源,它需要等待一个特定的条件发生。
如果没有条件变量,面对这种情况,程序必须使用一种忙等待(busywaiting)的形式:每个线程要重复地获得一个互斥,测试条件是否满足,然后释放互斥。条件变量允许线程原子地完成这两个动作,从而令等待更为高效。在被条件变量阻塞前,线程获得了一个互斥。在线程调用pthread_cond_wait以便等待某个条件变量时,线程同时指定了等待的条件变量和所拥有的互斥。操作系统将同时释放线程拥有的互斥并阻塞线程。
线程执行pthread_cond_wait后将处于阻塞状态,直到其他线程给此变量发信号①时才被唤醒。给条件变量发信号有两种形式,差别在于多个等待线程被处理的方式不同。即使有多个线程等待同一条件变量,函数pthread_cond_signal只允许一个线程继续执行;而函数pthread_cond_broadcast却让所有被阻塞的线程都可继续执行。在操作系统允许某个线程继续执行前,它将在线程被解除阻塞的同时再次获得阻塞前曾经有过的互斥。换句话说,等待条件变量意味着暂时放弃互斥,然后在得到发给该变量的信号时自动地重获互斥。因此,在某个线程因某个条件变量阻塞时,其他线程仍然可以获得互斥——仍然可以在临界区内继续执行。
条件变量是一种与互斥配合使用的线程同步机制,在线程等待一个条件时,它将暂时放弃所拥有的互斥;在条件变量得到信号后线程又将重新获得互斥。
5. 监控
此服务器实例中新实现了一个监控机制。虽然本实例中的监控机制并不复杂,但它说明了监控程序如何使用共享程序与从线程交互。监控程序用一个执行prstats过程的独立线程实现。本例中,prstats过程包含一个循环,在每一次循环过程中,监控程序打印连接的相关统计信息,然后睡眠INTERVAL秒。统计输出中列出了通信中的连接数、已完成通信的连接数、总连接时间和每个连接所传输的平均字节数。
监控线程和从线程使用一个共享的全局数据结构—-stats相互通信。每个从线程给将各自连接的有关信息加入到stats结构中,而监控线程每INTERVAL.秒提取一次信息。为确保任一时刻其有一个线程访问共享结构,服务器使用一个互斥,stats,st_mutex。线程在访问共享结构前等待互斥,并在使用完结构后释放互斥。
在一个实际的服务器中,监控程序可以让管理员以更复杂的形式与服务器交互。例如,监控线程可以不只打印统计信息,而是让管理员用键盘键入命令。因此,监控程序可按需提供信息,并可让管理员控制服务器(例如,动态设置或改变最大并发线程数)。
6. 小结
并发服务器可在一个进程内用若干线程实现。线程的主要优点是它具有较少的上下文切换开销和共享存储器的能力。线程的主要缺点是它增加了编程的复杂性。程序员必须使用同步机制协调线程对全局变量和一些库程序的访问,而且必须记住一些系统函数(如exit)会影响整个进程而非单个线程。