hello !大家好呀! 欢迎大家来到我的Linux高性能服务器编程系列之两种高性能并发模式介绍,在这篇文章中,你将会学习到高效的创建自己的高性能服务器,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!!
希望这篇文章能对你有所帮助,大家要是觉得我写的不错的话,那就点点免费的小爱心吧!(注:这章对于高性能服务器的架构非常重要哟!!!)
目录
一. 领导者/追随者模式
1.1 什么是领导者和追随者
2.2 模式组件构成
2 3 事件处理器和具体事件处理器
2.4 实例代码分析
一. 领导者/追随者模式
1.1 什么是领导者和追随者
领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任意时间点,程序都仅有一个领导者线程,它负责监听I/O事件。而其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到IIO事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件。此时,新的领导者等待新的I/O事件,而原来的领导者则处理I/O 事件,二者实现了并发。
2.2 模式组件构成
领导者/追随者模式包括以下几个组件:句柄集 ,线程集 , 事件处理集和具体的事件处理器,关系如下图:
1.句柄集 : 句柄(Handle) 用于表示I/O资源,在Linux下通常就是一个文件描述符。句柄集管理众多句柄,它使用wait _ for _ event方法来监听这些句柄上的I/O事件, 并将其中的就绪事件通知给领导者线程。领导者则调用绑定到Handle上的事件处理器来处理事件。领导者将Handle 和事件处理器绑定是通过调用句柄集中的register _ handle 方法实现的。
2.线程集 这个组件是所有工作线程(包括领导者线程和追随者线程)的管理者。它负责各线程之间的同步,以及新领导者线程的推选。线程集中的线程在任一时间必处于如下三种状态之一:
Leader:线程当前处于领导者身份,负责等待句柄集上的I/O 事件。
Processing: 线程正在处理事件。领导者检测到I/O 事件之后, 可以转移到 Processing 状态来处理该事件,并调用promote _ new _ leader方法推选新的领导者;也可以指定其他追随者来处理事件(Event Handoff), 此时领导者的地位不变。当处于Processing状态的线程处理完事件之后,如果当前线程集中没有领导者,则它将成为新的领导者,否则它就直接转变为追随者。
Folower:线程当前处于追随者身份,通过调用线程集的join方法等待成为新的领导者,也肯能被当前的领导者指定新的处理任务
这三种关系的转换关系如下:
2 3 事件处理器和具体事件处理器
事件处理器和具体的事件处理器事件处理器通常包含一个或多个回调函数handle _event。这些回调函数用于处理事件对应的业务逻辑。事件处理器在使用前需要被绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中的回调函数。具体的事件处理器是事件处理器的派生类。它们必须重新实现基类的handle _ event方法, 以处理特定的任务。如图:
2.4 实例代码分析
主线程函数代码:
// 创建追随者线程
pthread_t followers[MAX_FOLLOWERS];
for (int i = 0; i < MAX_FOLLOWERS; ++i) {
if (pthread_create(&followers[i], NULL, follower_thread, (void *)(long)server_sock) != 0) {
perror("pthread_create");
close(server_sock);
exit(EXIT_FAILURE);
}
}
// 主线程作为领导者
while (1) {
// 获取领导权
pthread_mutex_lock(&leader_mutex);
// 等待客户端连接
client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_sock == -1) {
perror("accept");
continue;
}
// 通知一个追随者线程处理客户端请求
pthread_cond_signal(&leader_cond);
// 释放领导权
pthread_mutex_unlock(&leader_mutex);
}
首先我们创建一个工作线程数组,然后对每个线程进行初始化(创建),之后进入主线程,首先对主线程即领导者上锁,然后开始等待客户端的连接,如果有连接,则接收客户端套接字后使用pthread_cond_signal()函数通知一个追随者线程处理客户端请求,之后对此线程解锁。
注:pthread_cond_signal()函数:
pthread_cond_signal(&leader_cond);
是一个 POSIX 线程(pthread)函数,用于在多线程编程中进行条件同步。这个函数的作用是唤醒至少一个等待在指定条件变量leader_cond
上的线程。在领导者/追随者模式中,条件变量用于协调领导者线程和追随者线程的工作。当领导者线程接受了一个新的客户端连接后,它需要通知一个追随者线程来处理这个连接。这时,领导者线程会调用
pthread_cond_signal
来唤醒一个正在等待的追随者线程。具体来说,
pthread_cond_signal
做了以下几件事情:
如果有追随者线程正在
leader_cond
条件变量上等待(通过pthread_cond_wait
或pthread_cond_timedwait
),pthread_cond_signal
会唤醒其中一个线程。唤醒的线程将从
pthread_cond_wait
或pthread_cond_timedwait
函数返回,并且该线程在继续执行之前必须重新获取与条件变量相关联的互斥锁(在本例中是leader_mutex
)。如果没有线程在条件变量上等待,
pthread_cond_signal
的调用不会有任何效果。在领导者/追随者模式中,
pthread_cond_signal
是一个关键点,因为它确保了只有一个追随者线程被唤醒来处理客户端连接,从而避免了多个线程同时处理同一个连接的问题。这是通过在领导者线程和追随者线程之间共享一个互斥锁来实现的,只有当领导者线程释放了互斥锁并且发出了信号之后,追随者线程才能够继续执行。
追随者线程处理函数代码:
// 追随者线程函数
void *follower_thread(void *arg) {
int client_sock;
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
while (1) {
// 获取领导权
pthread_mutex_lock(&leader_mutex);
// 等待成为领导者
pthread_cond_wait(&leader_cond, &leader_mutex);
// 接受客户端连接
client_sock = accept((int)arg, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_sock == -1) {
perror("accept");
continue;
}
// 处理客户端请求
char buffer[1024];
int bytes_received = recv(client_sock, buffer, sizeof(buffer), 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("Received data: %s\n", buffer);
send(client_sock, "Message received.\n", 18, 0);
}
// 关闭客户端连接
close(client_sock);
// 释放领导权
pthread_mutex_unlock(&leader_mutex);
}
return NULL;
}
在代码中,主线程接收到新的客户端连接后,会通知一个等待的追随者线程(通过
pthread_cond_signal
函数),并将服务器套接字文件描述符(server_sock
)作为参数传递给追随者线程。然而,这个服务器套接字文件描述符只是用于监听新的连接请求,并不用于与客户端进行数据传输。因此,当追随者线程被唤醒并开始执行时,它需要通过accept函数再次接收客户端连接,以获取一个用于与客户端进行数据传输的新套接字文件描述符(client_sock
)。这个新的套接字文件描述符才是用于与客户端进行数据传输的,而原始的服务器套接字文件描述符(server_sock
)仍然由主线程使用,用于接收新的连接请求。
注意哦:
在这段代码中,
pthread_mutex_lock(&leader_mutex);
的作用是获取互斥锁,确保同一时刻只有一个线程能够执行接下来的关键代码段。这个关键代码段包括等待成为领导者的条件变量pthread_cond_wait(&leader_cond, &leader_mutex);
和处理客户端请求的代码。当主线程调用
pthread_mutex_lock(&leader_mutex);
时,它获取了互斥锁,这意味着其他尝试获取同一互斥锁的线程将会被阻塞,直到主线程释放这个锁。在主线程释放锁之前,其他线程不能进入关键代码段。在主线程中,当一个新的客户端连接被接受后,主线程会通过
pthread_cond_signal(&leader_cond);
唤醒一个等待的追随者线程。这个信号只唤醒一个线程,而不是所有等待的线程。被唤醒的线程将有机会获取互斥锁并成为领导者,然后开始处理客户端请求。一旦一个追随者线程被唤醒并开始处理客户端请求,它会保持互斥锁,直到请求处理完毕。在这个过程中,其他追随者线程仍然处于等待状态,因为它们无法获取互斥锁。当领导者线程处理完请求并释放互斥锁后,其他线程中的一个将有机会被唤醒并成为新的领导者。
这种设计确保了同一时刻只有一个线程能够处理客户端请求,从而避免了多个线程同时处理同一个客户端连接的问题。每个线程在处理请求之前都会尝试获取互斥锁,只有成功获取锁的线程才能成为领导者并处理请求。其他线程则在等待获取锁的过程中阻塞。
好啦!到这里这篇文章就结束啦,关于实例代码中我写了很多注释,如果大家还有不懂得,可以评论区或者私信我都可以哦!! 感谢大家的阅读,我还会持续创造网络编程相关内容的,记得点点小爱心和关注哟!