关于线程池的“惊群效应”

什么是惊群
        举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结 果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:
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》
【参考资料】
<think>嗯,用户现在想了解线程池和进程池,这属于并发编程的范畴。我需要先理清楚这两个概念的区别和各自的适用场景。 首先,线程池线程池是管理多个线程的机制,预先创建好一定数量的线程,等待任务到来时分配执行。这样可以避免频繁创建和销毁线程的开销,提高效率。线程共享同一进程的内存空间,所以通信方便,但需要注意线程安全。 然后是进程池。进程池管理的是多个进程,每个进程独立,有自己的内存空间。进程间通信需要IPC机制,比如管道、消息队列等。进程池适用于CPU密集型任务,能利用多核优势,且进程之间更隔离,一个崩溃不会影响其他进程。 接下来,我需要比较两者的区别。资源方面,线程共享内存,进程独立;创建销毁开销线程更小;通信方式上线程用锁、条件变量等,进程用IPC;安全性方面进程更隔离,但线程需要处理同步问题。 应用场景的话,线程池适合I/O密集型或需要快速响应的任务,比如Web服务器处理请求。进程池适合计算密集型任务,如图像处理,科学计算。 然后,我需要给出C++的实现示例。线程池可以使用std::thread、mutex、condition_variable等,任务队列来管理。进程池的话,可能需要用fork创建子进程,用管道或共享内存通信,不过C++标准库没有直接支持,可能需要用系统调用或者第三方库。不过进程池的例子比较复杂,可能用户更想看到大致的框架,比如用fork和任务分发的方式。 在解释问题原因时,要说明为什么需要池化技术:减少资源创建销毁的开销,提高响应速度,控制并发数量防止资源耗尽。 可能用户接下来会问到线程同步机制,进程间通信方式,或者具体如何实现任务队列的线程安全。所以相关问题需要涵盖这些方面。</think><< 线程池和进程池是两种常见的并发编程模型,用于优化资源管理和任务调度效率。 ### 线程池 (Thread Pool) **定义**: 预创建一组线程并维护任务队列,复用线程资源来处理多个任务。 **C++实现示例**: ```cpp #include <vector> #include <queue> #include <thread> #include <mutex> #include <condition_variable> #include <functional> class ThreadPool { public: ThreadPool(size_t threads) : stop(false) { for(size_t i = 0; i < threads; ++i) workers.emplace_back([this] { for(;;) { std::function<void()> task; { std::unique_lock<std::mutex> lock(queue_mutex); condition.wait(lock, [this]{ return stop || !tasks.empty(); }); if(stop && tasks.empty()) return; task = std::move(tasks.front()); tasks.pop(); } task(); } }); } template<class F> void enqueue(F&& f) { { std::unique_lock<std::mutex> lock(queue_mutex); tasks.emplace(std::forward<F>(f)); } condition.notify_one(); } ~ThreadPool() { { std::unique_lock<std::mutex> lock(queue_mutex); stop = true; } condition.notify_all(); for(std::thread &worker : workers) worker.join(); } private: std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; }; ``` **核心组件**: 1. 工作线程集合:持续等待任务 2. 任务队列:存储待执行任务 3. 同步机制:互斥锁+条件变量保证线程安全 **优势**: - 避免线程频繁创建/销毁开销 - 控制并发线程数量 - 提高任务响应速度 ### 进程池 (Process Pool) **定义**: 预创建多个子进程,通过IPC进行通信,由父进程分配任务。 **C++实现特点**: ```cpp // 示意性代码(实际实现涉及进程管理和IPC) #include <unistd.h> #include <sys/wait.h> void worker_process() { // 子进程处理逻辑 while(true) { // 通过管道/共享内存获取任务 // 执行任务... } } class ProcessPool { public: ProcessPool(int size) { for(int i = 0; i < size; ++i) { if(fork() == 0) { // 子进程 worker_process(); exit(0); } } // 父进程任务分配逻辑... } }; ``` **核心组件**: 1. 子进程集合:独立运行环境 2. IPC机制:管道/共享内存/消息队列 3. 负载均衡:任务分配策略 **优势**: - 更好的隔离性和稳定性 - 充分利用多核CPU - 避免进程创建开销 ### 关键区别 | 特性 | 线程池 | 进程池 | |-----------|--------------------------|--------------------------| | 资源隔离性 | 共享内存空间(需同步) | 独立内存空间(天然隔离) | | 创建开销 | 较小(约1MB/线程) | 较大(约10MB/进程) | | 通信方式 | 共享内存+同步原语 | IPC(管道/共享内存/套接字等) | | 容错性 | 单线程崩溃导致整个进程终止 | 单进程崩溃不影响其他进程 | | 适用场景 | I/O密集型、高并发请求 | CPU密集型、计算任务 | ### 选择依据 - 使用线程池当: 需要共享状态 任务量多但轻量 要求低延迟响应 - 使用进程池当: 需要强隔离性 CPU密集型计算 需要利用多核优势 ### 典型问题场景 1. **资源泄漏**:未正确释放线程/进程资源 2. **死锁**:同步机制使用不当 3. **负载不均衡**:任务分配策略不合理 4. **效应**:多个线程/进程同时唤醒竞争资源
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值