Linux高性能服务器笔记--4
1. 高性能服务器程序框架
1.1服务器模型
1.1.1 C/S模型
服务器启动后,首先创建一个或多个socket,并调用bind函数将其绑定到服务器感兴趣的端口上,然后调用listen函数等待客户端连接。服务器稳定运行后,客户端可以调用connect函数向服务器发起连接。
服务器使用的I/O复用技术是select系统调用。当监听到连接请求后,服务器调用accept函数接受它,并分配一个逻辑单元为新的连接服务。逻辑单元可以是创建新的子进程、子线程或者其他。逻辑单元读取客户请求,读取该请求,并将处理结果返回给客户端。 C/S模型是和资源相对集中的场合,但服务器是通信的中心,当访问量过大时,客户可能都会得到较慢的响应。
1.1.2 P2P模型
1.2服务器编程框架
1.3I/O模型
2.俩种高效的事件处理模式
服务器程序通常需要处理三类事件:IO事件、信号及定时事件。我们将在后续章节依次讨论这三种类型的事件,这一节先从整体上介绍一下两种高效的事件处理模式: Reactor 和Proactor.
2.1 Reactor模式(适合同步I/O模型)
Reactor是这样一种模式,它要求主线程(I/O处理单元)只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。
除此之外,主线程不做任何其他实质性的工作。
读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步IO模型(以epoll_wait为例)实现的 Reactor模式的工作流程是:
-
主线程往epoll 内核事件表中注册socket上的读就绪事件。 2)主线程调用epoll_wait 等待socket上有数据可读。 3)当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列。 4)睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。(应该有关线程池) 5)主线程调用epoll_wait等待socket可写。 6)当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
(如何通知可写呢? 可能需要反应堆? 后面再看)
7)睡眠在请求队列上的某个工作线程被唤醒,它往 socket上写入服务器处理客户请求的结果。
2.2 Proactor模式(适合异步I/O模型)
Proactor模式将所有的I/O操作都交给主线程和内核,工作线程仅负责业务逻辑。
1)主线程调用aio_read 函数向内核注册socket 上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例,详情请参考sigevent的 man手册)。 2)主线程继续处理其他逻辑。 3)当socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。 4)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。.工作线程处理完客户请求之后,调用aio_write 函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序(仍然以信号为例)。 5)主线程继续处理其他逻辑。 6)当用户缓冲区的数据被写人socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。 7〉应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket。
2.3比较
使用Proactor框架和Reactor框架都可以极大的简化网络应用的开发,但它们的重点却不同。
Reactor框架中用户定义的操作是在实际操作之前调用的。比如你定义了操作是要向一个SOCKET写数据,那么当该SOCKET可以接收数据的时候,你的操作就会被调用;
而Proactor框架中用户定义的操作是在实际操作之后调用的。比如你定义了一个操作要显示从SOCKET中读入的数据,那么当读操作完成以后,你的操作才会被调用。
Proactor和Reactor都是并发编程中的设计模式。在我看来,他们都是用于派发/分离IO操作事件的。这里所谓的IO事件也就是诸如read/write的IO操作。"派发/分离"就是将单独的IO事件通知到上层模块。两个模式不同的地方在于,Proactor用于异步IO,而Reactor用于同步IO。
主动和被动
以主动写为例: Reactor将handle放到select(),等待可写就绪,然后调用write()写入数据;写完处理后续逻辑; Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;
可以看出,Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时再进行操作; Proactor直接调用异步读写操作,调用完后立刻返回;
实现
Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应;
Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响)
优点
Reactor实现相对简单,对于耗时短的处理场景处理高效; 操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性; 事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁; 事务分离:将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来,
Proactor性能更高,能够处理耗时长的并发场景;
缺点
Reactor处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理;
Proactor实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现;
适用场景
Reactor:同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序; Proactor:异步接收和同时处理多个服务请求的事件驱动程序;
2.3 模拟Proactor
主线程执行数据读写操作,读写完成后,主线程向工作线程通知这一完成事件,从工作线程的角度,它们就直接获得了数据读写的结果,只需要对读写结果进行逻辑处理。
-
主线程epoll内核事件表中注册socket读就绪事件;
-
主线程调用epoll_wait等待socket上有数据可读;
-
当socket上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列;
-
睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件;
-
主线程调用epoll_wait等待socket可写;
-
当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。
3.1俩种高效的并发模式
从实现上来说,并发编程主要有多进程和多线程两种方式,
而并发模式是指IO处理单元和多个逻辑单元之间协调完成任务的方法。服务器主要有两种并发编程模式:
半同步/半异步(〈half-synclhalf-async)模式和领导者/追随者(Leader/Followers)模式。
3.1.1 半同步/半异步模式
在并发模式中,同步是指程序完全按照代码的顺序执行,异步指的是程序的执行需要有系统事件来驱动。
常见的系统事件有中断,信号等
异步线程的执行效率高,实时性强,但编写相对复杂,难于调试和扩展,不适合大量的并发。 半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程处理I/O事件。当异步线程监听到客户请求后,就将其封装成请求对象并插入队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。
在服务器程序中,如果结合考虑两种事件处理模式和几种I/O模型,则半同步/半异步模式就存在多种变体。
3.1.2半同步/半反应堆
上图中异步线程只有一个,由主线程来充当。其负责监听所有socket上的事件。如果监听socket上有可读事件发生,即有新的连接请求的到来,主线程就接收之以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件。如果连接socket上有读写事件发生,即有新的客户请求来到或有数据要发送至客户端,主线程就将该连接socket插入请求队列中。所有的工作线程都睡眠在请求队列上,当有任务到来时,他们通过竞争获得任务的接管权。
3.1.3 半同步/半反应堆的缺点
图8-10中,主线程插人请求队列中的任务是就绪的连接socket。这说明该图所示的半同 步/半反应堆模式采用的事件处理模式是Reactor模式:
它要求工作线程自己从socket上读取客户请求和往 socket'写入服务器应答。
这就是该模式的名称中“half-reactive”的含义。
实际上,半同步/半反应堆模式也可以使用模拟的Proactor事件处理模式,即由主线程来完成数据的读写。
在这种情况下,主线程一般会将应用程序数据、任务类型等信息封装为一个任务对象,然后将其(或者指向该任务对象的一个指针〉插人请求队列。工作线程从请求队列中取得任务对象之后,即可直接处理之,而无须执行读写操作了。
半同步/半反应堆模式存在如下缺点: 主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费CPU时间。 每个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量CPU时间。
上图中,主线程只管理监听socket,连接socket由工作线程来管理。当有新的连接到来时,主线程就接受并将新返回的连接socket派发给某个工作线程,此后该新socket上的任何I/O操作都由被选中的工作线程来处理,直至客户关闭连接。主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据。其每个线程都维持自己的事件循环,各自独立地监听不同的事件。
可见,图8-11中,每个线程(主线程和工作线程)都维持自己的事件循环,它们各自独立地监听不同的事件。因此,在这种高效的半同步/半异步模式中,每个线程都工作在异步模式,所以它并非严格意义上的半同步/半异步模式。我们将在第15章给出一个用这种高效的半同步/半异步模式实现的简单CGI服务器的代码。
3.1.4 领导者/跟随者模式
领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任何时间点,程序都仅有一个领导者线程,它负责监听I/O事件。而其他线程都是追随者,他们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到I/O事件,就要从线程池中推选出新的领导者线程,然后处理I/O事件。此时新的领导者等待新的I/O事件,而原来的领导者则处理I/O事件,二者实现了并发。
领导者l追随者模式包含如下几个组件:句柄集(HandleSet)、线程集(ThreadSet),事件处理器(EventHandler)和具体的事件处理器(ConcreteEventHandler)。
-
句柄集(Handle):句柄用于表示I/o资源,在Linux下通常就是一个文件描述符。句柄集管理众多句柄,使用wait_for_event方法来监听这些句柄上的I/O事件,并将其中就绪事件通知给领导者线程。领导者则调用绑定到Handle上的时间处理器来处理时间。领导者将Handle和事件处理器绑定是通过调用句柄集中的register_handle方法实现。
-
线程集(ThreadSet):这一组件是所有工作线程的管理者,负责各线程之间的同步,以及新的领导者的推选。线程集中的线程有三种状态:
-
Leader(领导者,等待I/O事件);
-
Processing(正在处理事件);
-
Follower(追随者身份,通过线程集join方法等待成为新的领导者)
-
-
事件处理器和具体的事件处理器:包含一个或多个回调函数handle_event。这些回调函数用于处理时间对应的业务逻辑。事件处理器在使用前需要被绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中的回调函数。
由于领导者线程自己监听IO事件并处理客户请求,因而领导者l追随者模式不需要在线程之间传递任何额外的数据,也无须像半同步/半反应堆模式那样在线程之间同步对请求队列的访问。但领导者/追随者的一个明显缺点是仅支持一-个事件源集合,因此也无法像图8-11所示的那样,让每个工作线程独立地管理多个客户连接。
4.有限状态机
介绍逻辑单元内部的一种高效编程方法: 有限状态机
有的应用层协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一个执行状态,服务器可以根据它来编写相应的处理逻辑,其代码如下:
STATE_MACHINE(Package _pack){ PackageType _type = _pack.GetType(); switch(_type){ case type_A: process_package_A(_pack); break; case type_B: process_package_B(_pack); break; } }
在上述有限状态机中,每个状态都是相互对立的,即状态之间没有转移。转台之间的转移需要状态机内部驱动,其代码如下:
STATE_MACHINE(){ State cur_State = type_A; while(cur_State != type_C){ Package _pack = getNewPackage(); switch(cur_State){ case type_A: process_package_state_A(_pack); cur_State = type_B; break; case type_B: process_package_state_B(_pack); cur_State = type_C; break; } } }
该状态机包含三种状态: type_A、type_B和 type_C,其中 type_A是状态机的开始状态,type_C是状态机的结束状态。状态机的当前状态记录在cur_State变量中。在一趟循环过程中,状态机先通过getNewPackage方法获得一个新的数据包,然后根据cur_State变量的值判断如何处理该数据包。数据包处理完之后,状态机通过给cur_State变量传递目标状态值来实现状态转移。那么当状态机进入下一趟循环时,它将执行新的状态对应的逻辑。
实例:HTTP请求的读取和分析
/************************************************************************* 通过第八章学习的有限状态机 实现http请求的读取和分析 > File Name: state_http.cpp > Author: mgpdian > Mail: 1780484247@qq.com > Created Time: 2022年03月04日 星期五 15时04分57秒 ************************************************************************/ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <fcntl.h> //缓冲区大小 #define BUFFER_SIZE 4096 //主状态机的俩种可能状态 enum CHECK_STATE{ CHECK_STATE_REQUESTLINE = 0, //当前正在分析请求行 CHECK_STATE_HEADER //当前正在分析头部字段 }; //从在状态机的三种可能状态 即行的读取状态 //分别表示: 读取到一个完整的行 行出错 行数据尚且不完整 enum LINE_STATUS{ LINE_OK = 0, //读取到一个完整的行 LINE_BAD, //行出错 LINE_OPEN //行不完整 }; // 服务器处理HTTP请求的结果 enum HTTP_CODE { NO_REQUEST, // 请求不完整,需要继续获取客户数据 GET_REQUEST, // 获得了一个完整的客户请求 BAD_REQUEST, // 客户请求有语法错误 FORBIDDEN_REQUEST, // 客户对资源没有足够的访问权限 INTERNAL_ERROR, // 服务器内部错误 CLOSE_CONNECTION // 客户端已经关闭连接 }; //HTTP应当报文 static const char * szret[] = { "I get a correct result\n", "Someting wrong\n" }; //从状态机 用于解析出一行内容 LINE_STATUS parse_line(char * buffer, int &checked_index, int & read_index) { char temp; // // checked_index指向buffer当前分析的字节 // read_index指向buffer中客户数据的尾部下一个字节 // 第checked_index~(read_index-1)字节右下面进行分析 for (; checked_index < read_index; ++checked_index) { //获取当前要分析的字节 temp = buffer[checked_index]; // 如果当前字节是"\r",即回车符,说明可能读取了一个完整的行 if(temp == 'r') { //如果"\r"碰巧是buffer最后一个客户数据,则分析没有读取到一个完整的行 if((checked_index + 1) == read_index) { return LINE_OPEN; } //如果下一个字符是'\n' 说明成功读取到一个完整行 else if(buffer[checked_index + 1] == '\n') { buffer[checked_index++] = '\0'; buffer[checked_index++] = '\0'; return LINE_OK; } //否则说明HTTP请求存在语法问题 return LINE_BAD; } //如果当前字节是'\n' 检查前一个是否为\r else if(temp=='\n'){ if((checked_index>1) && buffer[checked_index-1] == '\r'){ buffer[checked_index-1] = '\0'; buffer[checked_index++] = '\0'; return LINE_OK; } return LINE_BAD; } } /*如果所有内容都分析完毕也没遇到“\r”字符,则返回LINE_OPEN,表示还需要继续读取客户数 据才能进一步分析*/ return LINE_OPEN ; } //分析请求行 HTTP_CODE parse_requestline(char *temp, CHECK_STATE& checkstate) { char *url = strpbrk(temp, "\t"); // 如果请求行中没有空白字符或者"\t",则请求有问题 if(!url) { return BAD_REQUEST; } *url += '\0'; char * method = temp; if(strcasecmp(method, "GET") == 0) { printf("The request method is GET\n"); } else { return BAD_REQUEST; } url += strspn(url, "\t"); char * version = strpbrk(url, "\t"); if(! version) { return BAD_REQUEST; } *version++ = '\0'; version += strspn(version, "\t"); //仅支持HTTP1.1 if((strcasecmp(version, "HTTP/1.1") != 0)) { return BAD_REQUEST; } //检查URL是否合法 if(strncasecmp(url, "http://", 7) == 0) { url += 7; url = strchr(url, '/'); } if(!url || url[0]!='/'){ return BAD_REQUEST; } printf("The request URL is: %s\n", url); // HTTP请求处理完毕,状态转移到头部字段分析 // checkstate = CHECK_STATE_HEADER; return NO_REQUEST; //因为parse_content对于返回值只会判断BAD_REQUEST 所以后面返回什么不重要 } //分析头部字段 HTTP_CODE parse_headers(char *temp) { // 遇到一个空行,说明得到一个正确的HTTP请求 if(temp[0] == '\0'){ return GET_REQUEST; } // 处理Host头部字段 else if(strncasecmp(temp, "Host:", 5)==0){ temp += 5; temp += strspn(temp, "\t"); printf("the request host is: %s\n" ,temp); } else{ // 其他头部不处理 printf("I can not handle this hander\n"); } return NO_REQUEST; } // 分析HTTP请求的入口函数 HTTP_CODE parse_content(char* buffer, int& checked_index, CHECK_STATE& checkstate, int& read_index, int& start_line){ //记录当前行的读取状态 LINE_STATUS linestatus = LINE_OK; //记录当前HTTP请求的处理结果 HTTP_CODE retcode = NO_REQUEST; //主状态机 用于buffer中取出完整行 while((linestatus = parse_line(buffer, checked_index, read_index)) == LINE_OK) { char * temp = buffer + start_line; //state_line 是行在buffer中的起始位置 start_line = checked_index; //记录下一行的起始位置 //checkstate 记录状态机当前状态 switch(checkstate){ case CHECK_STATE_REQUESTLINE:{ //状态一 分析请求行 retcode = parse_requestline(temp, checkstate); if(retcode == BAD_REQUEST) { return BAD_REQUEST; } break; } case CHECK_STATE_HEADER:{ // 状态二:分析头部字段 retcode = parse_headers(temp); if(retcode == BAD_REQUEST){ return BAD_REQUEST; } else if(retcode == GET_REQUEST){ return GET_REQUEST; } break; } default:{ return INTERNAL_ERROR; } } } // 若没有读取到一个完整的行,需要继续读取客户数据 if(linestatus == LINE_OPEN) { return NO_REQUEST; } else { return BAD_REQUEST; } } int main(int argc, char* argv[]){ if(argc<=2){ printf("usage: %s ip_address port_number \n", basename(argv[0])); return 1; } const char* ip = argv[1]; int port = atoi(argv[2]); struct sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port); int listenfd = socket(PF_INET, SOCK_STREAM, 0); assert(listenfd>0); int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address)); assert(ret!=-1); ret = listen(listenfd, 5); assert(ret!=-1); struct sockaddr_in client_address; socklen_t client_addrlength = sizeof(client_address); int fd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength); if(fd<0){ printf("errno is: %d\n", errno); } else{ char buffer[BUFFER_SIZE]; //读取缓冲区 memset(buffer, '\0', BUFFER_SIZE); int data_read = 0; int read_index = 0; // 当前已读取多少字节的客户数据 int checked_index = 0;// 当前已分析完了多少字节的客户数据 int start_line = 0;// 行在buffer中的起始位置 //设置主状态机初设状态 CHECK_STATE checkstate = CHECK_STATE_REQUESTLINE; while(1)//循环读客户数据 { data_read = recv(fd, buffer + read_index, BUFFER_SIZE - read_index, 0); if(data_read == -1){ printf("reading failed\n"); break; } else if(data_read==0){ printf("remote client has closed the connection\n"); break; } read_index += data_read; // 分析目前已经获得的所有客户数据 HTTP_CODE result = parse_content(buffer, checked_index,checkstate,read_index,start_line); // 尚未得到一个完整的HTTP请求 if(result == NO_REQUEST){ continue; } //得到一个正确 完整的HTTP请求 else if(result == GET_REQUEST) { send(fd, szret[0], strlen(szret[0]), 0 ); break; } //其他情况表示发送错误 else{ send(fd, szret[1], strlen(szret[1]) , 1); break; } } close(fd); } close(listenfd); return 0; }
这个状态机初设状态是LINE_OK, 其原始驱动力来着于buffer中新到达的客户数据, 在main中 我们循环调用recv函数往buffer中读入客户数据 每次成功读取数据后 调用parse_content分析数据
主状态机使用checkstate变量来记录当前的状态。
如果当前的状态是CHECK_STATE_REQUESTLINE,则表示parse_line函数解析出的行是请求行,于是主状态机调用parse.requestline来分析请求行﹔
如果当前的状态是CHECK_STATE_HEADER,则表示parse_line函数解析出的是头部字段,于是主状态机调用parse_headers 来分析头部字段。
checkstate变量的初始值是CHECK_STATE_REQUESTLINE,parse_requestline 函数在成功地分析完请求行之后将其设置为CHECK_STATE_HEADER,从而实现状态转移。
5.提高服务器性能的其他建议
5.1 池
池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源分配。当服务器开始处理客户请求的时候,其需要相关的资源可直接从池中获取,无需动态分配。因此池的速度更快,且避免了服务器对内核的频繁访问。
5.2 数据复制
高性能服务器应该避免不必要的数据复制,尤其是当数据复制发生在用户代码和内核之间的时候。如果内核可以直接处理从socket或者文件读入的数据,则应用程序就没必要将这些数据从内核缓冲区复制到应用程序缓冲区中。这里说的“直接处理”指的是应用程序不关心这些数据的内容,不需要对它们做任何分析。比如ftp服务器,当客户请求--个文件时,服务器只需要检测目标文件是否存在,以及客户是否有读取它的权限,而绝对不会关心文件的具体内容。这样的话,ftp 服务器就无须把目标文件的内容完整地读入到应用程序缓冲区中并调用send 函数来发送,而是可以使用“零拷贝”函数sendfile来直接将其发送给客户端。
此外,用户代码内部〈不访问内核)的数据复制也是应该避免的。举例来说,当两个工作进程之间要传递大量的数据时,我们就应该考虑使用共享内存来在它们之间直接共享这些数据,而不是使用管道或者消息队列来传递。又比如代码清单8-3所示的解析HTTP请求的实例中,我们用指针( start_line)来指出每个行在 buffer中的起始位置,以便随后对行内容进行访问,而不是把行的内容复制到另外一个缓冲区中来使用,因为这样既浪费空间,又效率低下。
5.3 上下文切换和锁
并发程序必须考虑上下文切换问题,即进程切换或线程切换导致的系统开销。同时应考虑共享资源的加锁保护,此时可以减小锁的粒度,比如使用读写锁。
并发程序必须考虑上下文切换(context switch)的问题,即进程切换或线程切换导致的的系统开销。即使是IO密集型的服务器,也不应该使用过多的工作线程(或工作进程,下同),否则线程间的切换将占用大量的CPU时间,服务器真正用于处理业务逻辑的CPU时间的比重就显得不足了。因此,为每个客户连接都创建一个工作线程的服务器模型是不可取的。图8-11所描述的半同步/半异步模式是-种比较合理的解决方案,它允许一个线程同时处理多个客户连接。此外,多线程服务器的一个优点是不同的线程可以同时运行在不同的CPU上。当线程的数量不大于CPU的数目时,上下文的切换就不是问题了。
并发程序需要考虑的另外一个问题是共享资源的加锁保护。锁通常被认为是导致服务器效率低下的一个因素,因为由它引入的代码不仅不处理任何业务逻辑,而且需要访问内核资源。因此,服务器如果有更好的解决方案,就应该避免使用锁。显然,图8-11所描述的半同步/半异步模式就比图8-10所描述的半同步/半反应堆模式的效率高。如果服务器必须使用“锁”,则可以考虑减小锁的粒度,比如使用读写锁。当所有工作线程都只读取一块共享内存的内容时,读写锁并不会增加系统的额外开销。只有当其中某一个工.作线程需要写这块内存时,系统才必须去锁住这块区域。