什么是惊群
举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。
对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结 果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:
1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。
2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
- fork--accpet:其实在Linux2.6版本以后,内核内核已经解决了accept()函数的“惊群”问题,大概的处理方式就是,当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程或线程。所以,如果服务器采用accept阻塞调用方式,在最新的Linux系统上,已经没有“惊群”的问题了。
for(i = 0; i < PROCESS_NUM; ++i){
pid = fork();
if(pid == 0){
while(1){
connfd = accept(fd, (struct sockaddr *)NULL, NULL);
printf("process %d accept success\n", getpid());
//处理连接
close(connfd);
}
}
}
- accept--fork:由主进程监控socket,到来一个连接,建立一个子进程处理该连接,当发生大量连接时,主进程压力大,可能会出现不能正常完成某些client的连接。
while(1){
connfd = accept(fd, (struct sockaddr *)NULL, NULL);
pid = fork();
f(pid == 0){
printf("process %d accept success\n", getpid());
//处理连接
close(connfd);
}
}
- epoll_wait:内核对于阻塞在epoll_wait的进程,也是采用全部唤醒的机制,所以存在和accept相似的“惊群”问题。新版本的的解决方案也是只会唤醒等待队列上的第一个进程或线程,所以,新版本Linux部分的解决了epoll的“惊群”问题。(但是从结果看来并没有很好的解决惊群现象)与文章3的不一致的地方,原文用epoll+fork()模式(将该socket加入到epoll中,然后fork出多个子进程,每个进程都阻塞在epoll_wait上,如果有事件到来,则判断该事件是否是该socket上的事件如果是,说明有新的连接到来了,则进行接受操作.如不加sleep,不会出现惊群),我执行程序后,出现惊群现象:说明每个woker都被唤醒,产生accept,但只有一个可以真正的处理事件成功。
- 线程池中的”惊群”:一个基本的线程池框架是基于生产者和消费者模型的。生产者往队列里面添加任务,而消费者从队列中取任务并进行执行。一般来说,消费时间比较长,一般有许多个消费者。当许多个消费者同时在等待任务队列的时候,也就发生了“惊群效应”
// 线程池类型定义
struct thread_pool {
int max_threads; // 线程池中最大线程数限制
int curr_threads; // 当前线程池中总的线程数
int idle_threads; // 当前线程池中空闲的线程数
pthread_mutex_t mutex; // 线程互斥锁
pthread_cond_t cond; // 线程条件变量
thread_job *first; // 线程任务链表的表头
thread_job *last; // 线程任务链表的表尾
pthread_cond_broadcast,这个是广播给所有等待任务的消费者,会产生惊群效应。
使用pthread_cond_signal不会有“惊群现象”产生,它最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。