持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情
为什么需要线程池?
线程池,是一个多线程程序组成部分之一
对于一个程序,我们想尽可能多的利用多核的性能
这个时候我们就需要多线程或者多进程来压榨CPU的性能了
但是多进程通信代价属实更大,所以,多线程有时候是更好的选择!
我们设想这样一个场景 一主多从
我们有一个主线程负责事件分发处理,把事件通过队列拿给工作队列
工作队列负责处理逻辑,主线程则只关心事件分发
stateDiagram-v2
request --> master
master --> request
master --> worker1
worker1 --> master
master --> worker2
worker2 --> master
master --> worker3
worker3 --> master
worker --> task
task --> worker
而任务队列在其中起到的作用就是任务的调度
graph TD
event --> task_queue
task_queue --> worker1
task_queue --> worker2
task_queue --> worker3
这就是线程池的基本逻辑
若是我们对每一个task开一个线程,这样固然是可以达到目的
可是这样的开销我们经受不起,因为创建和消耗线程是需要时间和内存的。
并且若是线程数过多以后,则还可能导致OOM kill 掉你的进程
线程池的实现
为了减少心智负担,我直接采用了许多C++ 标准库中的许多东西,有如下内容:
std::vectorstd::queuestd::functionstd::condition_variablestd::mutexstd::threadstd::future
线程池需要一个容器来存放我们的线程对象,用于回收线程池资源
std::vector<std::thread> worker_;
线程池需要一个队列,用来存放我们的执行任务
using FUNC = std::function<void()>;
std::queue<FUNC> que_;
线程池需要锁,同时,我们希望锁占用时,线程睡眠等待,同时,执行完成后我们需要回收线程资源
bool stop_;
std::mutex mtx_;
std::condition_variable cond_;
首先,在构造函数中将线程先创建好,这是一种池式组件的思想
在这里,采用了RAII的手法,来控制锁
explicit thread_pool(size_t thread_count) : stop_(false) {
for (int i = 0; i < thread_count; ++i) {
worker_.emplace_back([this]() {
while (true) {
FUNC task;
{
std::unique_lock<std::mutex> lock(this->mtx_);
this->cond_.wait(
lock, [this]() { return this->stop_ || !this->que_.empty(); });
if (this->stop_ && this->que_.empty())
return;
task = std::move(que_.front());
que_.pop();
}
task();
}
});
}
}
其次,我们需要将其析构函数释放资源的部分写好
回收资源比较简单,只需要把stop开关开启,同时等待任务队列消费完毕,就可以了
~thread_pool() {
{
std::lock_guard<std::mutex> lock(mtx_);
stop_ = true;
}
cond_.notify_all();
for (auto &v : worker_)
v.join();
}
最后,构造析构都写了,还剩下一个添加任务的接口
void add_task(FUNC &&func) {
std::lock_guard<std::mutex> lock(mtx_);
que_.push(func);
cond_.notify_one();
}
至此,我们的线程池就简单写完了,不过,这个线程池是一个极为简单的,接口简单的线程池
因为我们只能传给它一个 void()的函数,不能够传参数,就很不友好
虽然我们调用时,可以使用lambda来构造好我们传入的参数。
但是,我们应该寻求一个更加自然的方式传入参数
接下来,我们来看一下,如何改进我们的线程池
线程池改进
上文提到,改进点显然在我们的add_task
如何支持多参数呢?
少不了函数重载,那么一个一个写显得我很蠢,我们当然要用到可变长传参了!
需要用到的技术
- 我们需要接受函数对象和参数
可变长传参实现 - 我们需要推导出函数的返回值
decltype实现 - 我们需要拿到返回值 异步拿返回值
future实现
只需要用一点点完美转发和模板编程,就可以轻松实现我们的匹配任意函数的需求
template <typename F, typename... Arg>
auto submit(F &&func, Arg &&...arg) -> std::future<decltype(func(arg...))> {
using ret = decltype(func(arg...));
auto fun = [fn = std::forward<F>(func),
... pack = std::forward<Arg>(arg)]() {
return fn(pack...);
}; // c++ 14 can capture move
auto task = std::make_shared<std::packaged_task<ret()>>(fun);
add_task([task]() { (*task)(); });
return task->get_future();
}
完整代码
class thread_pool {
public:
using FUNC = std::function<void()>;
explicit thread_pool(size_t thread_count) : stop_(false) {
for (int i = 0; i < thread_count; ++i) {
worker_.emplace_back([this]() {
while (true) {
FUNC task;
{
std::unique_lock<std::mutex> lock(this->mtx_);
this->cond_.wait(
lock, [this]() { return this->stop_ || !this->que_.empty(); });
if (this->stop_ && this->que_.empty())
return;
task = std::move(que_.front());
que_.pop();
}
task();
}
});
}
}
template <typename F, typename... Arg>
auto submit(F &&func, Arg &&...arg) -> std::future<decltype(func(arg...))> {
using ret = decltype(func(arg...));
auto fun = [fn = std::forward<F>(func),
... pack = std::forward<Arg>(arg)]() {
return fn(pack...);
}; // c++ 14 can capture move
auto task = std::make_shared<std::packaged_task<ret()>>(fun);
add_task([task]() { (*task)(); });
return task->get_future();
}
~thread_pool() {
{
std::lock_guard<std::mutex> lock(mtx_);
stop_ = true;
}
cond_.notify_all();
for (auto &v : worker_)
v.join();
}
private:
void add_task(FUNC &&func) {
std::lock_guard<std::mutex> lock(mtx_);
que_.push(func);
cond_.notify_one();
}
bool stop_;
std::mutex mtx_;
std::condition_variable cond_;
std::queue<FUNC> que_;
std::vector<std::thread> worker_;
};
总结
其实本质上来说,线程池做的事情就是一个生产者、消费者模型
没错,就是你在操作系统书上所看见的那个,最基本的,最实在的模型
我们这个东西究其本质不过是一个 多生产者、多消费者模式
而其用途呢?
其实还是比较多的,特别是高吞吐的情况下,处理逻辑尽可能的让cpu吃满,这个时候,线程池这个组件就有用武之地了
小技巧:分配线程池的大小和 cpu的核数有关,具体分配多少根据实际情况调优