本章提要
在开发具有“可伸缩性”和“健壮性”的网络应用程序,特别是网络服务器时,并发性(concurrency)显得十分重要。本章提供了“并发设计空间(concurrency design dimension)”的领域分析(domain analysis),“并发设计空间”涉及正确使用进程、线程和同步装置(synchronizer)的策略(policy)和机制。本章讨论以下设计空间:
l 循环、并发及反应式服务器;
l 进程与线程;
l 进程/线程创建策略;
l 用户、核心及混合线程模型;
l “分时”及“实时”调度级别;
l “基于任务”与“基于消息”的体系。
5.1 循环、并发及反应式服务器
服务器可以分为循环式(iterative)、并发式(concurrent)和反应式(reactive)等几大类。在这一设计空间中,需要权衡的地方主要在于:是要简化编程;还是要提高程序的可伸缩性,使之日后能够增强“提供服务(service-offering)”的能力和“主机承担负荷”的能力。
循环式服务器在处理后续请求之前,会完整地处理每一个客户请求。因此,在处理一个请求时,循环式服务器要么将其他请求排成队列,要么忽略它们。循环式服务器最适合以下两种服务:
l 短期服务,例如,执行时间短、标准的Internet ECHO和DAYTIME服务;
l 不经常运行的服务,例如,在平台负载较轻的夜间才运行的“远程文件系统备份服务”。
开发“循环式服务器”相对来说较直接。图5.1(1)告诉我们:在内部,循环式服务器常常在“单进程”地址空间中执行服务请求,如以下伪代码所示:
void iterative_server ()
{
initialize listener endpoint(s)
for (each new client request) {
retrieve next request from an input source
perform requested service
if (response required) send response to client
}
}

图5.1 循环式/反应式服务器与并发式服务器
由于这样一种循环式结构,每一个请求处理都会在一个相对粗糙的层次上被串行化例如,在应用程序和OS同步事件多路分离程序(如select()或WaitForMultipleObjects())之间的接口上。但是,这种粗糙层次上的并发性不会充分利用主机平台上的某些处理资源(例如,多CPU)和OS特性(例如,并行DMA和I/O设备之间的传输)。
循环式服务器的另一个特点是:当客户因为等待服务器处理请求而被阻塞时,会阻止客户程序继续往下执行。如果服务器端延迟过久,则应用程序和中间件层中用于“重新传输”的“超时”计算会变得很复杂,从而引发严重的网络阻塞。根据客户和服务器交换请求时使用的协议类型,服务器还有可能接收到重复请求。
并发式服务器同时处理多个客户请求,如图5.1(2)所示。根据OS和硬件平台不同,并发式服务器在执行它的服务时,要么使用多线程,要么使用多进程。如果是“单服务(single-service)”服务器,则同一服务的多个副本可以同时运行。如果是“多服务(multiservice)”服务器,则不同服务的多个副本也可以同时运行。
并发式服务器非常适合“I/O操作频繁(I/O-bound)”的服务和(或)“执行时间会变化”的长周期服务。和循环式服务器不同,并发式服务器可以使用更精细的同步技术,能够在“应用程序定义的层次”将“请求”串行化。这种设计需要使用同步机制,如信号量(semaphore)或互斥(mutex)锁(lock)[EKB+92],以保证同时运行的进程和线程之间的稳固合作和数据共享。我们将在第6章探讨这些机制,并在第10章演示它们的应用示例。
我们将在第5.2节看到,并发式服务器可以通过各种方式构成,例如,通过使用多进程或多线程。并发式服务器的一个常见设计是“一个请求一个线程(thread-per-request)”:为了并发处理客户请求,针对每一个客户请求,主线程都会单独创建一个工作者线程。
void master_thread ()
{
initialize listener endpoint(s)
for (each new client request) {
receive the request
spawn new worker thread and pass request to this thread
}
}
主线程继续侦听新的请求,工作者线程则处理客户请求,如下所示:
void worker_thread ()
{
perform requested service
if (response required) send response to client
terminate thread
}
可以直接修改这个“thread-per-request”模型,使之支持其他“并发式”服务器模型,如“thread-per-connection(一个连接一个线程)”:
void master_thread ()
{
initialize listener endpoint(s)
for (each new client connection) {
accept connection
spawn new worker thread and pass connection to this thread
}
}
在这个设计中,主线程继续侦听新的连接,工作者线程则处理来自这个连接的客户请求,如下所示:
void worker_thread ()
{
for (each request on the connection) {
receive the request
perform requested service
if (response required) send response to client
}
}
“thread-per-connection”出色地支持“客户请求”的优先级控制。例如,来自高优先级客户的连接会被关联到高优先级线程上。因而,来自高优先级客户的请求会在低优先级客户请求之前被处理,因为OS可以抢占低优先级线程。
在第5.3节,我们还介绍了另外几个并发服务器模型,例如线程池(thread pool)和进程池(process pool)。
反应式服务器几乎是同时处理多个请求——尽管所有处理实际上在一个线程中完成。多线程尚未在OS平台上广泛普及之前,并发处理通常是通过“同步事件多路分离”策略来实现的:多个服务请求由一个“单线程(single-threaded)进程”依次循环处理。例如,标准的X Windows服务器就是以这种方式运作的。
通过“同步事件多路分离”机制,如第6章介绍的select()和WaitForMultipleObjects(),可以显式执行分时处理,将服务器的注意力分散到每一个请求上,以此方式实现反应式服务器。以下伪代码演示的是,在基于select()的反应式服务器中,我们使用的一种典型的编程风格:
void reactive_server ()
{
initialize listener endpoint(s)
// Event loop.
for (;;) {
select() on multiple endpoints for client requests
for (each active client request) {
receive the request
perform requested service
if (response is necessary) send response to client
}
}
}
这个服务器虽然能够在一段时间内服务多个客户,但从服务器的角度来看,它本质上是循环式的。因此,较之利用“OS提供的完善的多线程支持”,利用这种技术开发的应用程序存在以下局限性。
l 编程的复杂性增加。某些类型的网络应用程序(例如,“I/O操作频繁”的服务器)难以用“反应式服务器”模型来编程。例如,开发者必须显式创建事件循环线程,还要负责手工保存和恢复环境信息。所以,为了让客户认为它们的请求是在被“并发”处理,而不是“循环”处理,每一个请求就都必须在一段相对较短的时间内执行。同样,长时间的操作(例如,下载大型文件)得被显式编写为一个“有限状态机”,在对其他对象的事件做出反应的过程中,这个状态机会记录这个对象的处理过程。随着状态数量的增加,设计会变得很笨重。
l 可靠性和性能降低。如果一个操作失败(例如,一个服务进入死循环,或陷入死锁(deadlock)而被无限期挂起),则整个服务器进程将会挂起(hang)。而且,即使整个进程没有失败,但只要有一个服务调用了系统函数,或产生了分页错误,OS就会阻塞整个进程,从而降低整个服务器进程的性能。另一方面,如果只使用“非阻塞”方法,那将很难运用到DMA之类的高级技术,从而无法利用数据和指令缓存(data and instruction cache)的“引用局部性(locality of reference)”来提高性能。如第6章所述,OS“多线程”机制可以克服这些性能上的局限性,因为对那些运行在单独线程上的独立服务来说,多线程的出现能使它
们的“抢占式”和“并行”执行自动化。若想不通过完整的“并发式服务器”方案来解决这些问题,一个方法是使用“异步I/O”,如副栏11所示。
|
副栏11:异步I/O和前摄式服务器 |
|
另一个在单线程服务器中处理多I/O流的机制是异步I/O。这一机制允许服务器通过一个或多个I/O句柄来启动I/O请求,不用因为要等待请求完成而被阻塞。而当请求结束时,OS会通知调用者,服务器则可以在已经结束的I/O句柄上继续处理。以下OS平台提供了异步I/O机制: l 通过“重叠(overlapped)I/O”和“I/O完成端口(completion port)”支持异步I/O 的Win32 平台 [Ric97, Sol98]。 l 一些实现了aio_*()“异步I/O”函数、符合POSIX标准的平台 [POS95, Gal95]。 但是,和“多线程”及“同步事件多路分离”机制相比,“异步I/O”的实现不像前者那样具有可移植性;所以,在本书中不再对其作进一步讨论。 在 [SH]一书中,我们会讲述“使用了Proactor模式” [SSRB00]的ACE Proactor框架;还会讨论“异步I/O”。在使用了Proactor模式的“事件驱动型(event-driven)”应用程序中,在受到“异步操作完成”事件的触发时,程序可以高效地多路分离(demultiplex)和分发(dispatch)服务请求,从而获得并发性所带来的性能提高,同时无需承担并发处理上的任何工作。在Win32和支持aio_*()同步I/O函数的POSIX系列平台上,都可以使用ACE Proactor 框架(framework)。 |
日志服务程序 => 为简单起见,在第4章的网络日志服务程序的初始版本中,使用的是“循环式服务器”设计。在后续章节中,将对这个日志服务器的功能和伸缩性进行扩充:在第7章,扩充后的服务器展现的是“反应式”服务器风格;在第8章,展现的是使用了“多进程”的“并
发式”服务器风格;在第9章,演示了几个“使用多线程”的“并发式”服务器设计。






