线程池的理解
线程池代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <queue>
using namespace std;
std::mutex print_mutex;
class ThreadPool
{
public:
ThreadPool(int numThreads):stop_(false){
for (int i = 0; i < numThreads; i++)
{
threads_.emplace_back([this]
{
while (true)
{
unique_lock<mutex> lock(mtx_);
condition_.wait(lock, [this]
{
return !tasks_.empty() || stop_;
});
if (stop_ && tasks_.empty())
{
return;
}
//当前任务队列中不为空,就从中取出一个任务开始执行
std::function<void()> task(std::move(tasks_.front()));
tasks_.pop();
lock.unlock();
task();
}
});
}
}
~ThreadPool() {
{
unique_lock<mutex> lock(mtx_);
stop_ = true;
}
condition_.notify_all();//如果要析构了,就通知所有等待的线程起来干活
for (thread& thread : threads_)
{
thread.join();//等待所有线程执行完成
}
}
template<typename F,typename...Args>
void addtasks(F&& f,Args&& ...args)//这里使用的是万能引用。Args表示可以带多个参数
{
//forward称为完美转发类型,保持实参task的左引用或者右引用类型(根据传入判断)
function<void()> task = bind(forward<F>(f), forward<Args>(args)...);
{
unique_lock<mutex> lock(mtx_);
//move返回传入的实参task的右值引用类型
tasks_.emplace(move(task));
}
condition_.notify_one();
}
private:
vector<thread> threads_;//线程池
queue<function<void()>> tasks_;//任务队列
mutex mtx_; //互斥变量
condition_variable condition_; //条件变量
bool stop_;
};
int main()
{
ThreadPool threads(5);
for (int i = 0; i < 10; i++)
{
threads.addtasks([i] {
{
//这里的print_mutex是为了保证打印不会乱序
//跟这里的线程池没有关系
std::lock_guard<std::mutex> lock(print_mutex);
cout << "task: " << i << "is running" << endl;
}
this_thread::sleep_for(chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(print_mutex);
cout << "task: " << i << "is done" << endl;
}
});
}
return 0;
}
实验结果如下:
这里实现了一个简单的线程池,在main函数中循环添加了10个任务,然后依次从线程池里面取出线程来执行任务。这里创建了五个线程,用以执行分发的任务,属于单生成者-多消费者模型。
接下来我们就尝试画图分析,线程池实现的原理
初始化线程池
ThreadPool threads(5)
当执行这句话的时候,创建了5个线程池,此时任务队列为空,如图所示
这里的addtask作为生产者,线程池中的线程作为消费者,不过此时生产者还没有生产任务,消费者没有可消费的任务。即线程池中的线程一直处于wait()等待状态,工作线程为空。
生产者生产一个任务
当生产者每生产一个任务后,并且成功放入到任务队列之后,就通过notify_one通知消费者(线程池),我已经生产了一个任务,你们谁来处理呢?
此时notify_one通知了线程池中等待的线程,通过操作系统的调度,假设唤醒了线程3,线程3拿到了资源锁,线程3就从wait状态中退出,开始工作,首先从任务队列中取出任务,并且释放资源锁(线程3从任务队列取出任务后就不需要锁了),开始执行取出的任务,如下图所示。
生产者又生产了一个任务
假设有另一个任务到来,我们分析一下线程3是否能够执行这个新的任务,以下是可能出现的情况。
新任务到来时,线程3还没有执行完成
- 此时线程3还在执行 task()任务;并且任务队列mtx_已解锁;
- 主线程成功获取锁,添加任务到任务队列中去,随后调用 notify_one();
- 唤醒的是线程池中处于 wait 状态的线程,比如说线程5;
- 线程5从任务队列中取出任务,并执行任务
线程3此时仍然在工作,不处于等待状态,所以不会被唤醒,自然也不产生竞争。
新任务到来时,线程3恰好执行完成
线程3恰好执行任务队列后,任务队列为空,此时来了一个新任务
task();//线程3恰好执行完成这个函数
unique_lock<mutex> lock(mtx_); //进入下一个循环,抢占锁
CaseA
- 线程3与生产者竞争,并且线程3成功抢到锁
- 此时线程3发现任务队列为空,执行cv.wait(),进入等待状态
- 生产者随后获取了资源锁,添加任务,并且调用notify_one()唤醒等待线程
- 有可能线程3被再次唤醒,执行这个任务(此时线程池中的所有线程都有可能被唤醒)
CaseB
线程3与生产者竞争,生产者抢到锁了;生产者往任务队列中添加任务,并且调用notify_one唤醒线程池中等待线程,假设准备唤醒线程5。这样存在两种情况
CaseB1
- 线程3(此时还是活跃线程),并且在线程(如线程5)被唤醒前就已经获取了资源锁
- 线程3从任务队列中取出任务,并执行任务
- 线程5被唤醒后发现任务队列为空,重新陷入等待状态
CaseB2
- 等待线程(如线程5)被成功唤醒
- 线程3和线程5竞争抢占资源锁,抢占成功的一方负责执行任务,抢占失败的线程进入等待状态
- 成功执行完任务的线程也进入等待状态
tips
:这个恰好执行完成的度不好把握,如果是线程3刚刚执行完task()
任务并退出,实际上是不可能等到主线程添加了任务,唤醒线程池中等待线程,等待线程与活跃线程3竞争锁的;更可能的方式是,将要执行完task()
,会存在上述的一个情况,所以笼统的说恰好执行完task这个任务也是不准确的。
新任务到来时,线程3已经执行完成
此时所有线程都是处于等待状态的,由操作系统调度唤醒某个线程。
生产者生产快于消费者消费
下图是生产者生产的过快,消费者来不及处理任务队列中的数据。
上述是我分析了线程池中可能存在的一些情况,如果有误,敬请斧正。
知识点剖析
生产者-消费者模型
特点
• 如果队列里的生产产品已满,生产者就不能继续生产;
• 如果队列里的产品从无到有,生产者需要通知一下消费者,告诉它可以来消费了;
• 如果队列里已经没有产品了,消费者也无法继续消费;
• 如果队列里的产品不满,消费者也得去通知下生产者,说你可以来继续生产了。
优势
解耦合:将生产者类和消费者类进行解耦,消除代码之间的依赖性,简化工作负载的管理。
复用:通过将生产者类和消费者类独立开来,那么可以对生产者类和消费者类进行独立的复用与扩展。
调整并发数:由于生产者和消费者的处理速度是不一样的,可以调整并发数,给予慢的一方多的并发数,来提高任务的处理速度。
异步:对于生产者和消费者来说能够各司其职,生产者只需要关心缓冲区是否还有数据,不需要等待消费者处理完;同样的对于消费者来说,也只需要关注缓冲区的内容,不需要关注生产者,通过异步的方式支持高并发,将一个耗时的流程拆成生产和消费两个阶段,这样生产者因为执行 put() 的时间比较短,而支持高并发。
支持分布式:生产者和消费者通过队列进行通讯,所以不需要运行在同一台机器上,在分布式环境中可以通过 redis 的 list 作为队列,而消费者只需要轮询队列中是否有数据。同时还能支持集群的伸缩性,当某台机器宕掉的时候,不会导致整个集群宕掉。
生产者-消费者模型:理论讲解及实现(C++)
多线程必考的「生产者 - 消费者」模型
condition_varible
对于condition_variable::wait函数的理解
condition_variable::wait提供了两个重载函数
void wait(unique_lock<mutex>& _Lck) { // wait for signal
// Nothing to do to comply with LWG-2135 because std::mutex lock/unlock are nothrow
_Cnd_wait(_Mycnd(), _Lck.mutex()->_Mymtx());
}
template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate
while (!_Pred()) {
wait(_Lck);
}
}
(1)void wait( std::unique_lock<std::mutex>& _Lck)
调用底层函数_Cnd_wait
函数实现了原子操作(解锁unlock(mutex对应的资源)、阻塞当前的执行线程)。把当前线程添加到等待线程列表中,该线程会持续阻塞直到被 notify_all() 或 notify_one() 唤醒。被唤醒后,该线程会重新获取mutex(竞争拿到该mutex资源),获取资源后执行下一步操作。
(2)void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)
这个重载设置了第二个参数 _Pred, 只有当_Pred为false时,wait才会阻塞当前线程。
【C++】多线程condition_variable的wait
对于condition_variable::notify_one函数的理解
void notify_one() noexcept;
void notify_all() noexcept;
如果有线程在等待 *this,则调用 notify_one 将解除阻塞其中一个等待线程。
notify_all是解除所有等待线程。
条件变量操作的原子性保证
wait()系列操作(wait() / wait_for() / wait_until()) 和 notify_one()/notify_all()操作,包括三个原子步骤:解锁+等待、唤醒与重新加锁,都在一个全局顺序中执行。
这个顺序由条件变量内部维护,类似于原子变量的修改顺序。
实现效果为:notify_one() 不可能被延迟到"在通知后开始等待"的线程之后执行
如:如果线程A调用 notify_one() 后,线程B才开始调用 wait(),那么B不会被这次通知唤醒
通知时是否应该持有锁?
// 情况1:不持有锁通知(推荐)
{
std::lock_guard lock(mutex);
// 更新共享状态
} // 自动解锁
cv.notify_one(); // 通知时未持有锁
// 情况2:持有锁通知(特定场景需要)
{
std::lock_guard lock(mutex);
// 更新共享状态
cv.notify_one(); // 通知时仍持有锁
}
通知时一般不持有锁,原因是被唤醒的线程会立即尝试获取锁,而如果通知线程仍持有锁,会导致"唤醒即阻塞"的低效场景。
lambda的理解
lambda表达式是一种匿名函数,主要用于表示简单的行为或者代码块,并在需要时传递到其他函数中。Lambda表达式的基本语法如下:
[capture list] (parameter list) -> return type { function body }
- capture list 是捕获列表,用于指定 Lambda表达式可以访问的外部变量。捕获列表可以为空,表示不访问任何外部变量,也可以使用默认捕获模式 & 或 = 来表示按引用或按值捕获所有外部变量等。
- parameter list 是参数列表,用于表示 Lambda表达式的参数,可以为空,表示没有参数,也可以和普通函数一样指定参数的类型和名称。
- return type 是返回值类型,用于指定 Lambda表达式的返回值类型,可以省略,表示由编译器根据函数体自动推导,也可以使用 -> 符号显式指定。
- function body 是函数体,用于表示 Lambda表达式的具体逻辑。