什么是惊群 举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结 果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果: 1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。 2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
最常见的例子就是对于socket描述符的accept操作,当多个用户进程/线程 监听在同一个端口上时,由于实际只可能accept一次,因此就会产生惊群现象,当然前面已经说过了,这个问题是一个古老的问题,但目前的内核版本已经修复了这个问题,一个链接过来,内核只会唤醒一个子进程出来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_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_broadcast,这个是广播给所有等待任务的消费者,会产生惊群效应。
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。 但使用pthread_cond_signal不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是 根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则 根据等待时间的长短来确定哪个线程获得信号。但 无论如何一个pthread_cond_signal调用最多发信一次。 有了上面这段话,我们就有理由相信,线程池并不会产生“惊群效应”。同时,这种方式使用多进程共享资源,等待管道或者其他资源等,提供cpu利用率。
如果有些系统设计比较简单,pthread_cond_signal还是会广播信号,那如何避免惊群呢?
方法一:要想避免如上线程池设计中的惊群问题,在 仍然共用一个线程互斥锁的条件下, 给每一个消费者线程创建一个线程条件变量,生产者线程在添加任务时, 找到空闲的消费者线程, 将任务置入该消费者的任务队列中,同时 只通知 (pthread_cond_signal) 该消费者的线程条件变量,消费者线程与生产者线程虽然共用相同的线程互斥锁(因为有全局资源及调用 pthread_cond_wait 所需),但 线程条件变量的通知过程却是定向通知的,未被通知的消费者线程不会被唤醒,这样惊群现象也就不会产生了。当然,还有一些设计上的细节需要注意,比如: 当没有空闲消费者线程时,需要将任务添加进线程池的全局任务队列中,消费者线程处理完自己的任务后需要查看一下线程池中的全局任务队列中是否还有未处理的任务。 【大意】:在线程池中,增加一组线程条件变量,对应于每一个线程。增加任务的时候,如果有空闲线程,那么只通知某一个空闲线程,并且将其置忙。忙与闲,可以通过条件变量来表征,用一个链表表示(类似连接池)。 如果所有线程都忙,那么就将任务加入全局队列,并且通知所有消费者(这时惊群是很小的,除非所有线程都刚好同一时刻完成任务,同一时刻争夺资源,否则只有极少数线程会发生惊群)。
推荐阅读有详细的介绍 《pthread_cond_signal与pthread_cond_wait》 【参考资料】 http://www.tuicool.com/articles/2aumqe http://blog.163.com/[email protected]/blog/static/16223010220122611523786/ http://baike.baidu.com/link?url=6x0zTazmBxTYE9ngPt_boKjS8ivdQnRlfhHj-STCnqG9tjKwfCluPsKlq-ASUkdQTPW3XrD8FtyilBaI75GJCK