什么是惊群,这篇文章写的很好:
举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结 果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:
参考
1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。
2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
对于惊群效应的场景描述,最常见的就是对于socket操作符的accept操作的描述。当多个用户进程/线程同时监听同一个端口时,由于实际上一个请求过来,只有一个进程/线程accept成功,所以就会产生惊群效应。
实际上这是一个很古老的问题。linux操作系统在内核层面很早就解决了这个问题,一个请求过来,内核只会唤醒一个进程来accept,这样就没有惊群现象了。但是在很多场景下,只要有竞争,就可能会出现惊群效应。比如常见的生产者-消费者模型,一般来说消费可能会比较耗时,所以消费者会有多个。当突然有生产者往队列里面投了一个job时,这些消费者就会一哄而上去队列中抢这个job,这就发生了惊群效应。
一个基本的线程池框架也是基于生产者-消费者模型的。也就是说只要用到了进程池或者线程池,你可能就避免不了要处理惊群效应带来的问题。所以你能感觉到惊群效应的无处不在了吗。。。
那么对于线程池这个模型,我们怎么解决它可能出现的惊群问题呢?
// 线程池类型定义
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; // 线程任务链表的表尾
...
}
一个线程池的定义一般如上所示:有最大线程数限制max_threads,当前线程数curr_threads和空闲线程数idle_threads,然后还有线程互斥锁,还有线程条件变量,每个线程条件变量对应一个唯一线程,然后还有一个线程任务队列(链表)。为什么需要线程条件变量和线程任务队列这些??
消费者一般在等待任务和处理任务的过程中,处理逻辑可以简化为:
pthread_mutex_lock(&pool->mutex);
while(pool->first==NULL)//如果没有任务,一直等待
pthread_cond_wait(&pool->cond, &pool->mutex);
pthread_mutex_unlock(&pool->mutex);
//回调处理
job->func(job->arg);
如果生产者有任务了,就会通过内核通知消费者。生产者的通知过程可简化为:
pthread_mutex_lock(&pool->mutex);
pthread_cond_signal(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
注意上面用的是pthread_cond_signal,这个是根据进程条件变量来通知某一个等待消费的线程,而不是用pthread_cond_broadcast函数来广播给所有等待任务的消费者,这样就不会产生惊群效应。
pthread_cond_signal函数的作用上面也说了,是根据条件变量发送信号给一个正在休眠等待消费的线程,这个休眠消费线程获取到这个信号后马上激活,进行消费。
如果某一时刻,所有的消费线程都在进行各自的消费,此时又有一个生产者发送了一个任务,现在没有空闲消费者来接怎么办??
在这种情况下,生产者调用pthread_cond_signal函数也会立刻返回,只不过此时不是根据条件变量通知一个消费线程来accept,而是将此任务放到线程池队列中去,等待有空闲的消费线程过来取。
上面说了一个条件变量对应一个消费线程(意思就是每次都能根据当时的条件找到唯一的一个消费进程),那条件变量到底是什么呢??
条件变量的作用就是筛选出一个最合适的消费线程。那生产者生产了一个任务后,是怎么根据条件变量筛选出那个最合适的消费线程的呢??
假设有多个消费线程正在休眠,首先根据各消费线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。
在上面这种机制下,我们能保证生产者生产了一个任务后只会通知唯一的一个消费者线程,所以不会出现惊群效应。等等,到目前为止,咱们定义的线程池中,pthread_mutex好像并没有用到呢??
刚刚我们只是假设有pthread_cond_signal这种函数来保证每个任务只通知一个唯一的消费进程,那如果有的系统没有这样的单一通知机制,只有pthread_cond_broadcast函数这种广播机制怎么办??
此时pthread_mutex这个线程池互斥锁便派上用场了。在只有pthread_cond_broadcast函数这种广播机制的情况下,通过一个线程互斥锁,给每一个消费者线程创建一个条件变量......(好像原文后面说的不是很清楚)
【大意】:在线程池中,增加一组线程条件变量,对应于每一个线程。增加任务的时候,如果有空闲线程,那么只通知某一个空闲线程,并且将其置忙。忙与闲,可以通过条件变量来表征,用一个链表表示(类似连接池)。
如果所有线程都忙,那么就将任务加入全局队列,并且通知所有消费者(这时惊群是很小的,除非所有线程都刚好同一时刻完成任务,同一时刻争夺资源,否则只有极少数线程会发生惊群)。
上面也说道了linux在操作系统层已经避免了惊群效应的出现,但是nginx作为一个移植性非常高的web服务器,它自己也实现了一套避免出现惊群效应的机制。