惊群问题
惊群问题是由于系统中有多个进程在等待同一个资源,当资源可用的时候,系统会唤醒所有或部分处于休眠状态的进程去争抢资源,但是最终只会有一个进程能够成功的响应请求并获得资源,但在这个过程中由于系统要对全部的进程唤醒,导致了需要对这些进程进行不必要的切换,从而会产生系统资源的浪费。
这种情况一般是accept或epoll_create在子进程中处于监听状态,也就是先创建子进程或者子线程然后调用accept或epoll_create阻塞监听的时候发生…,然而在先调用accept阻塞和先创建epoll_create,,然后在创建子进程或子线程也同样会产生惊群,这两种情况是有区别的。前者的epoll_create的惊群问题还没有解决,后者的惊群问题linux内核已经在2.6之后的版本周解决掉了。
1、在Linux系统中,多进程accept在比较旧的版本中存在惊群现象,在linux 2.6 版本之后就不存在这个问题了。在较老的unix系统版本中,每个阻塞在accept调用上的进程,当遇到连接的时候都会唤醒,从而产生惊群。
2、在linux系统中,如果fork在epoll_create()之后调用,也就是epoll_create在fork之前,会出现惊群现象,和accpet惊群现象类似,等待监听描述符的所有进程和线程都会被唤醒,Epoll后面的版本解决了这个问题,这种情况下所有的进程和线程共享同一个epoll_create之后的红黑树结构。
3、 当fork之后再调用epoll_create,也就是由于fork执行之后,每个子进程中复制了当前的这个监听sockfd,从而也会出现惊群现象,这个时候并不共享同一个红黑树,因为每个进程都自己维护一个红黑树。这种情况的惊群问题目前linux还没有解决,没解决的原因主要是linux系统不能确定是否在接受到响应信息的时候只需要唤醒一个进程,有可能需要唤醒多个进程,因而没有对这种情况进行处理。
4、Nginx服务器中处理这个问题的方式是使用互斥锁,在进行epoll_create之前,先让进程争抢这个互斥锁,抢到的进程就进行epoll_create没有抢到的就不会做任何反映,这样就解决了这个惊群问题。(具体细节还需要继续分析)Nginx中的epoll_create 应该在fork之后再创建的epoll_create,所以这种情况的惊群问题需要Nginx自己来处理。
Linux系统内核解决accept惊群问题的方法:
在Linux系统版本在2.6之前,所有监听同一个sockfd的进程都在一个等待队列上,当响应到来的时候会唤醒所有的等待进程。而其当时的解决方法是在调用accept之前先加一把锁,只有获得锁的进程才能accept成功,在Linux系统的2.6版本之后,引入了一个标记位,具体是什么还不清楚,但是引入这个标记位之后系统惊群就解决了。
补充:
线程池的惊群问题:(还不明白)
一个基本的线程池框架是基于生产者和消费者模型的。生产者往队列里面添加任务,而消费者从队列中取任务并进行执行。一般来说,消费时间比较长,一般有许多个消费者。当许多个消费者同时在等待任务队列的时候,也就发生了“惊群效应”。
线程池惊群问题,其实质是由于当生产者线程向消费者线程发送消息来唤醒消费者线程的时候,由于有可能有多个消费者线程在监听,则会有可能唤醒多个线程,但是这种情况比较好解决:
在解决问题之前,需要先明白线程池的数据结构,以及生产者,消费者的处理逻辑:
线程池的数据结构是:
Struct pool
{
Int max; //线程池中的最大线程个数
Int current; //线程池中的当前线程个数
Int free; //线程池中过的空闲线程个数
Pthread_mutex_tpt; //生产者,消费者互斥信号量
Prhread_cond_tcond; //生产者,消费者条件变量
Struct job*first; //任务链表头
Struct job*last; //任务链表尾
…
};
//消费者一般是处在等待唤醒去执行任务的状态中
Pthread_mutex_lock(&pool->pt)
While(pool->first == NULL)
{
Pthread_cond_wait(&pool->cond,&pool->pt);
}
Pthread_mutex_unlock(&pool->pt);
//任务处理
Job->func(job->args);
//生产者一般是处于生产任务,然后去通知消费者进行消费
Pthread_mutex_lock(&pool->pt);
Pthread_cond_signal(&pool->cond);
Pthread_mutex_unlock(&pool->pt);
注意上面的用法,不是pthread_cond_broadcast,这个是广播给所有等待任务的消费者,会产生惊群效应。
一般情况下,使用pthread_cond_signal 不会产生惊群问题。
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
但使用pthread_cond_signal不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。
有了上面这段话,我们就有理由相信,线程池并不会产生“惊群效应”。同时,这种方式使用多进程共享资源,等待管道或者其他资源等,提供cpu利用率。
如果有些系统设计比较简单,pthread_cond_signal还是会广播信号,那如何避免惊群呢?
简单理解就是给每个线程都设置一个条件变量,而不是只使用以上代码中的那一个条件变量。
在线程池中,增加一组线程条件变量,对应于每一个线程。增加任务的时候,如果有空闲线程,那么只通知某一个空闲线程,并且将其置忙。忙与闲,可以通过条件变量来表征,用一个链表表示(类似连接池)。