hello !大家好呀! 欢迎大家来到我的Linux高性能服务器编程系列之两种高性能并发模式介绍,在这篇文章中,你将会学习到高效的创建自己的高性能服务器,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!!
希望这篇文章能对你有所帮助,大家要是觉得我写的不错的话,那就点点免费的小爱心吧!(注:这章对于高性能服务器的架构非常重要哟!!!)
导言:
并发编程的目的是让程序“同时”执行多个任务。如果程序是计算密集型的,并发编程并没有优势,反而由于任务的切换使效率降低。但如果程序是I/O 密集型的,比如经常读写文件,访问数据库等,则情况就不同了。由于I/O 操作的速度远没有CPU的计算速度快,所以让程序阻塞于I/O 操作将浪费大量的CPU时间。如果程序有多个执行线程,则当前被I/O操作所阻塞的执行线程可主动放弃CPU(或由操作系统来调度),并将执行权转移到其他线程。这样一来,CPU 就可以用来做更加有意义的事情(除非所有线程都同时被I/O 操作所阻塞),而不是等待I/O操作完成,因此CPU的利用率显著提升。
目录
一.半同步/半异步模式
1.1 什么是同步/异步
1.2 半同步/半异步
1.3 半同步/半反应堆模式(变体)
1.4 更高效的半同步/半异步模式
1.5 实例代码
一.半同步/半异步模式
1.1 什么是同步/异步
首先,对于I/O模型中的同步和异步,意思是区分内核向应用程序通知的是何种I/O事件(是就绪事件还是完成事件),以及让谁来完成任务(是应用程序还是内核)。而在并发编程中,指程序按照顺序还是需要按照程序执行的需要由系统事件来驱动,常见的系统事件有:中断,信号。
如下图:
按照同步方式运行的线程称为同步线程,按照异步方式运行的线程称为异步线程。显然,异步线程的执行效率高,实时性强,这是很多嵌入式程序采用的模型。但编写以异步方式执行的程序相对复杂,难于调试和扩展,而且不适合于大量的并发。而同步线程则相反,它虽然效率相对较低,实时性较差,但逻辑简单。因此,对于像服务器这种既要求较好的实时性,又要求能同时处理多个客户请求的应用程序,我们就应该同时使用同步线程和异步线程来实现,即采用半同步/半异步模式来实现。
1.2 半同步/半异步
在这个模式中,同步线程用于处理客户逻辑,异步线程用于处理I/O事件,异步线程监听到客户请求后,将其封装为请求对象并且插入请求队列中,请求队列将通知某个工作在同步模式的工作线程来读取并且处理该对象,线程选择使用Round Robin算法,也可以通过条件变量和信号量来实现,具体工作流程图如图所示:
1.3 半同步/半反应堆模式(变体)
异步线程只有一个,由主线程来充当。它负责监听所有socket上的事件。如果监听socket 上有可读事件发生,即有新的连接请求到来,主线程就接受之以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件。如果连接socket 上有读写事件发生,即有新的客户请求到来或有数据要发送至客户端,主线程就将该连接 socket插入请求队列中。所有工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争(比如申请互斥锁)获得任务的接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务,这是很合理的。
如图:
其中主线程插入请求队列中的任务是就绪的连接socket , 这说明半同步/半反应堆模式采用的事件处理模式是Reactor模式:它要求工作线程自己从socket 上读取客户请求和往socket写入服务器应答。
这就是该模式的名称中“half-reactive”的含义。实际上,半同步/半反应堆模式也可以使用模拟的Proactor事件处理模式,即由主线程来完成数据的读写。在这种情况下,主线程一般会将应用程序数据、任务类型等信息封装为一个任务对象,然后将其(或者指向该任务对象的一个指针)插入请求队列。工作线程从请求队列中取得任务对象之后,即可直接处理之,而无须执行读写操作。
半同步/半反应堆模式存在如下缺点:
1) 主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费CPU 时间。
2) 每个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量CPU 时间。
1.4 更高效的半同步/半异步模式
主线程只管理监听socket, 连接socket由工作线程来管理,当有新的连接到来时,主线程就接受之并将新返回的连接socket 派发给某个工作线程,此后该新 socket上的任何I/O操作都由被选中的工作线程来处理,直到客户关闭连接。
主线程向工作线程派发socket 的最简单的方式,是往它和工作线程之间的管道里写数据。
工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新 socket上的读写事件注册到自己的epoll内核事件表中。
每个线程(主线程和工作线程)都维持自己的事件循环,它们各自独立地监听不同的事件。因此,在这种高效的半同步/半异步模式中,每个线程都工作在异步模式,所以它并非严格意义上的半同步/半异步模式。
1.5 实例代码
异步线程逻辑处理:
// 事件处理函数
void *handle_connection(void *socket_desc) {
int sock = *(int*)socket_desc;
char *message;
int len;
// 接收客户端数据
while((len = read(sock, message, 1024)) > 0) {
printf("收到数据:%s\n", message);
// 发送响应
write(sock, "Hello, Client!", 14);
memset(message, 0, 1024);
}
// 关闭套接字
close(sock);
return 0;
}
同步线程监听请求和分配任务给异步线程:
// 循环处理事件
while(1) {
activity = select(FD_SETSIZE, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
printf("select error");
return -1;
}
if (FD_ISSET(sock, &readfds)) {
// 有新的客户端连接
newsock = accept(sock, (struct sockaddr *)&cli_addr, (socklen_t*)&clilen);
printf("新的客户端连接:%s\n", inet_ntoa(cli_addr.sin_addr));
client_sockets[i] = newsock;
FD_SET(newsock, &readfds);
}
for (i = 0; i < MAX_CLIENTS; i++) {
if (FD_ISSET(client_sockets[i], &readfds)) {
// 处理客户端数据
new_sock = malloc(1);
*new_sock = client_sockets[i];
if (pthread_create(&thread_id, NULL, handle_connection, (void*)new_sock) < 0)
return -1;
}
pthread_detach(thread_id);
free(new_sock);
FD_CLR(client_sockets[i], &readfds);
}
}
}
好啦!到这里这篇文章就结束啦,关于实例代码中我写了很多注释,如果大家还有不懂得,可以评论区或者私信我都可以哦!! 感谢大家的阅读,我还会持续创造网络编程相关内容的,记得点点小爱心和关注哟!