什么是惊群,举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结 果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:
1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。
2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
也就是影响性能
在很多场景下,只要有竞争,就可能会出现惊群效应。比如常见的生产者-消费者模型,一般来说消费可能会比较耗时,所以消费者会有多个。当突然有生产者往队列里面投了一个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_signal也会成功返回。(也就是说,线程池那些空闲的线程其实是处于阻塞状态)
如果某一时刻,所有的消费线程都在进行各自的消费,此时又有一个生产者发送了一个任务,现在没有空闲消费者来接怎么办??
在这种情况下,生产者调用pthread_cond_signal函数也会立刻返回,只不过此时不是根据条件变量通知一个消费线程来accept,而是将此任务放到线程池队列中去,等待有空闲的消费线程过来取。
上面说了一个条件变量对应一个消费线程(意思就是每次都能根据当时的条件找到唯一的一个消费进程),那条件变量到底是什么呢??
条件变量的作用就是筛选出一个最合适的消费线程。
那生产者生产了一个任务后,是根据什么条件来筛选出那个最合适的消费线程的呢??
假设有多个消费线程正在休眠,首先根据各消费线程优先级的高低确定哪个线程接收到信号开始继续执行。
如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。
上述两句是确定多个空闲线程的执行顺序,也就是排好序(我个人认为是排好序后就将可以首先执行的那个线程对应的条件变量赋给 “线程条件变量”)
线程条件变量的具体实现
在线程池中,增加一组线程条件变量,对应于每一个线程。增加任务的时候,如果有空闲线程,那么只通知某一个空闲线程,并且将其置忙。忙与闲,可以通过boolean来表示,用一个链表表示
如果所有线程都忙,那么就将任务加入全局队列,并且通知所有消费者(这时惊群是很小的,除非所有线程都刚好同一时刻完成任务,同一时刻争夺资源,否则只有极少数线程会发生惊群)。
这种机制下,我们能保证生产者生产了一个任务后只会通知唯一的一个消费者线程,所以不会出现惊群效应。
nginx也会有惊群效应,它的解决方案是,不让多个进程在同一时间监听接受连接的socket,而是让每个进程轮流监听