并发
l 迭代 与 并发
迭代服务器 在为随后的客户请求提供服务前,首先处理已经接受的全部请求。在处理一个请求时,迭代服务器或者将其他的请求发放入队列或者抛弃。迭代服务器特别适合下列情形之一
Ø 短时长服务,而且服务时长变化很小。比如标准的Internet的ECHO和DAYTIME服务。
Ø 服务运行不频繁的。比如远程文件备份服务,仅仅在系统负载较轻的晚上运行。
迭代服务器的设计和实现相对要简单,因为他总是在内部的一个单进程地址空间执行他的服务,如图所示。
服务实现伪代码如下:
void iterative_server (void)
{
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
}
}
这种迭代结构使得每个请求的处理在一个相对比较粗糙的级别上串行进行,有点类似应用程序在进行Select操作。这种粗糙层次的并发使得应用程序无法充分利用确定的系统处理资源(比如多CPU)和操作系统特性(比如对并行DMA的支持)。迭代服务结构在处理请求时会阻塞客户端使得客户端无法进行其他动作。客户端的阻塞还使计算超时重发机制的实现变得困难,同时大大增加了网络的额外流量。
并发服务器同时并发处理来自客户的多个请求,如上图。依赖于所用操作系统和硬件平台,并发服务器或者在不同的CPU上分别处理请求或者在单个CPU上以时间片(time-slices)来分别处理不同的请求。如果这个服务器是个单服务服务器,相同服务的多个拷贝可以同时运行。如果这个服务器是个多服务服务器,多个服务的多个拷贝也同样可以运行。并发服务器非常适合于IO边界操作和处理时间可变的长时长服务。与迭代服务器相比,多线程并发服务器允许更加精细的同步技术,比如数据库加锁等。这种服务设计需要并发控制机制,比如semaphores or mutex locks,以此确保稳定可靠的操作和各个活动进程/线程间数据的共享。并发服务的构造有多种方法,比如多线程/多进程。一个常用的并发设计是每请求线程模式,在这里一个服务分发线程将不同的请求分发到不同线程的服务伺服器上。下面是它的工作伪代码:
void concurrent_service_dispatcher (void)
{
initialize listener endpoint(s)
for (each new client request) {
receive request
spawn new worker thread and pass request to this thread
}
}
void worker_thread (void)
{
perform requested service
if (response required) send response to client
terminate thread
}
这种模式很容易被修改以支持其它的并发服务器模式,比如每连接线程模式。
void concurrent_service_dispatcher (void)
{
initialize listener endpoint(s)
for (each new client connection) {
accept connection
spawn new worker thread and pass connection to this thread
}
}
void worker_thread (void)
{
for (each request on the connection) {
perform requested service
if (response required) send response to client
}
}
每连接线程为对客户请求进行优先级排队服务提供了很好的支持。举例说,更高优先级客户可以与具有更高优先级的线程绑定,这样,来自较高优先级的客户请求将优先获得服务。
l 进程 与 线程
并发服务器能够采用多进程和多线程方式实现。
多进程:一个进程是一个操作系统的实体,它为程序指令的执行提供了一个上下文。每个进程都包含和管理确定的一些资源比如虚拟内存和IO操纵等等,同时与其他进程相隔离而得到保护。多个进程间通讯采用共享内存。缺点是无法实现精细的控制,某些应用无法实现。
多线程:目前大多数的OS平台在一个进程中支持多线程。一个线程是在一个进程中被保护区域上下文里的一个简单指令序列。一个线程包含和管理一些确定的资源比如函数运行栈(Run-time stack)、优先级和特定线程数据等等。如果有多个CPU,在多线程上运行的服务能够并行的处理。
采用多线程较多进程能够减小如下并发开销:
1. 线程的创建和上下文的切换: 线程保留的状态信息要比进程少,因此创建线程和在线程中的上下文切换的额外开销要比进程小。比如在同一进程的线程间切换时,进程范围的资源比如虚拟地址映射和缓存,就不需要改变。
2. 同步: 在调度和执行应用线程时,没有必要在核心模式和用户模式之间切换。同样,线程间同步比进程间同步的代价要小得多。这是因为在线程里的被同步对象通常都是本地的(无需核心层转换)。较之进程间的全局共享内存(涉及系统核心层)好。
3. 数据拷贝 线程能够通过本地进程内存区间共享信息,这通常要比采用IPC消息传送机制的进程间通讯要快得多。这个原因在于同一进程不同线程间数据的拷贝无需通过系统内核。通常,使用一个进程的共享地址空间来实现线程间通讯比通过共享内存或者本地的IPC机制的进程间通讯要有效得多。
尽管许多的操作系统都支持多线程,但是并不意味着所有的应用程序必须都是多线程的。实际上,使用多线程也有不少的有限制的地方:
1. 健壮性(Robustness):为了减小上下文切换和同步的开销,线程之间的保护很少。因为在相同进程空间的各个线程间保护不是很好,因此,在进程中一个有缺陷的服务实现可能通过全局共享数据而影响到该进程其它线程中运行的服务。这样有可能导致不可预料的后果,从而使整个进程崩溃甚至导致网络服务器挂起。另外某些操作系统的一些调用在线程中使用时可能会对整个进程产生负面影响。比如UNIX系统中的EXIT调用,将关闭调用进程中的所有线程。
2. 访问权限 多线程的另外一个问题是一个进程里的所有线程共享使用相同的用户ID和对包括受保护系统资源的访问权限。要防止意外的或者有意的对未授权资源的访问,很多的网络服务比如TELNET运行在不同的分离进程里。
3. 性能 一个经常混淆概念是一个应用采用多线程将提高性能。在很多情况下,多线程并没有提供性能上的提高。举例来说,运行于单CPU(uni-processor)的基于计算的应用就不能从多线程中获得好处。原因在于,计算任务不需要交互间通讯。同样,非常精细的加锁会产生很高程度的同步开销,这些都会导致应用无法充分利用并行处理带来的好处。在有些情况下,多线程将显著的提高系统性能。举例来说,单CPU的IO操作应用(这里的好处来自多线程服务能够在通讯服务和磁盘操作服务间交迭运行)
l 积极分发 与 需时响应 于 延迟进程/线程分发策略
在分发进程和线程时存在多种不同策略。不同的策略在不同的环境下应用能够优化并发服务器的性能。下面将讨论这些策略,他们能够让开发人员依据客户需求和可用的OS处理资源来调整服务端的并发层次。
积极分发:这种策略在服务创建时就启动一个或多个系统进程或线程。这种热启动(warm-started)执行方式在请求能够被响应前就启动多个服务,能够提高系统的响应时间。启动的个数取决于一系列的因素:CPU的个数,当前机器负载和客户请求队列时间等等。下图是他的一个实现示意图
需时分发 这种策略在客户新的连接建立或客户请求到达时将分发一个新的从属进程或线程。它的例子包括每请求线程和每连接线程。在分发线程/进程和启动服务代价特别高昂时,该策略能够将资源开销最小化。下图为该策略的示意图。这种策略的缺陷是:在实时系统和大负载情况下,这种策略将降低系统的性能。
延迟分发 采用延迟分发策略的应用通常采用积极分发或需时分发策略来分配初始的线程或进程集。当然这个集通常是相当小的,比如单个线程或进程。当请求到达时,已经存在的线程和进程将处理这些请求。如果这个请求处理超过一个特定的时长,另外一个线程/进程将被启动来响应下面到达的请求。
通常,上面所列策略的选择在减小的启动负载增加的资源消耗间折衷考虑。
l CPU分配和线程模型
规划调度是操作系统的主要运行机制,用来确保系统资源的合适使用。因为线程是多线程进程里调度和执行的一个单元,因此在这里最关心的系统资源是CPU。今天,大多数平台都有一系列调度机制来管理应用创建的线程。不是所有的平台都允许你改变线程的系统资源分配,但是无论如何,你都应该知道在你的应用平台下那些工作时你可以做的以及系统怎样调度资源。这样可以最大限度优化你的应用。
N:1用户线程模型: 早期的线程实现是位于操作系统进程控制机只得上面,完全由用户空间的库来管理操纵。因此,OS内核几乎不知道线程。内核只是调度进程,该进程中的库管理着这N个线程。这种设计被称为“N:1”模型,有时候又叫做用户线程模型。这种线程模型的好处是系统核心没有卷入相同进程中的任何现成生命周期和上下文切换进程中。因此线程的建立,删除和上下文切换效率很高。但是这种模式有两个问题,他们的原因是系统内核对线程的不理会。
1. 无论系统中有多少个CPU,每一个进程都只会在一个CPU上。该进程的所有线程将为这个CPU而竞争,在该进程的调度中得进行时间片共享分配。
2. 如果某个线程发生了一个阻塞操作,比如读写文件,该进程的所有线程都将阻塞直到这个操作的完成。
1:1核心线程模型 为解决N:1模型的问题,一种更加高级的线程模型被采用。他就是1:1模型,这里OS内核直接支持线程。每一个线程的建立都直接被系统内核所操纵,内核负责调度这些线程到系统的各个(如果有多个地话)CPU上。这种模型也称之为核心线程模型。Linux和Windows NT/2000都是采用这种模型。
这种模型解决了上面N:1模型的问题,这是因为系统内核参与到线程的整个生命周期包括建立和调度等。
N:M混合线程模型:现在的一些操作系统比如Solaris提供了N:1和1:1的混合模型。称之为N:M模型。这种设计支持用户线程和核心线程的混合。但应用程序建立一个线程,线程库将建立一个用户线程,但是只有在应用程序需要时或者显示的指出时才会建立核心线程。
无论多么强大的工具,滥用和错误的使用都会伤害自己。线程也是如此,那么在这些线程模型中你该怎么选择合适的线程模型?下面这些考虑是必须得,当你建立一个新的线程。
Ø 分离强CPU(CPU-intensive task)任务 如果你新创建线程的主要动机来自让一个计算繁重的任务拥有自己的空间,使他在自己的范围里调度并且要最小同应用中的其他线程发生竞争,这个时候你应该考虑核心线程模式,也就是1:1模式。这种策略可以避免线程间调度开销,同时使得OS充分利用多个CPU(当然是要有,呵呵)。
Ø 简化系统设计如果你的主要目的是为了在逻辑上将你的应用分为好几个分离的任务,你无需采用核心线程模式。如果你采用用户线程模式,在到达逻辑上分解业务的同时,你也可以避免线程创建和调度时的系统核心开销。
l 基于任务的 与 基于消息的并发体系结构
并发体系结构包括应用服务处理的各个单元(如层、功能函数、连接和消息)和各种逻辑/物理CPU的配置。网络应用的并发结构是影响应用性能一系列因素(包括协议、总线、内存和网络接口特征。)中的一个。并发体系的三个基础部分包括:
1. CPU,这是应用代码的底层执行代理
2. 数据和控制消息,用来在一个或多个应用间或网络设备间发送和接收。
3. 服务处理,基于消息完成任务的执行
基于这种分类,有两种类型的并发体系结构基于任务的和基于消息的。
基于任务的并发体系结构这种设计根据应用中服务功能的单元分配组织CPU。在这里,任务是主动的,被任务处理的消息是被动的。如下示意图。并发的获得是通过在各自CPU上执行服务任务和在任务/CPU间传递数据消息和控制消息来得到的。
基于消息的并发结构:这种设计能够根据来自网络应用和接口的消息分配组织CPU。在这种体系结构中,消息时主动的,而任务是被动的。如下示意图所示。通过在各个不同CPU上确保一系列的数据和控制消息在一个服务任务栈上同时进行处理实现并发性。每请求线程、每连接线程和线程池都是基于这种基于消息的并发结构。
并发结构的选择将影响应用性能的关键要素比如上下文切换、同步、和数据移动开销等。基于消息的并发结构通常要比基于任务的并发结构有效得多。这也是他得到广泛使用的原因。但是好像基于任务的并发结构在实现上要更加简单一些。