【转载】走进 C++11(三十一) 如何用 60 行实现 C++11 thread pool

381 阅读6分钟

原文地址:走进 C++11(三十一) 如何用 60 行实现 C++11 thread pool

我对文章的格式和错别字进行了调整,并在他的基础上把重点部分进一步解释完善。以下是正文。

正文

开始之前先上一下代码链接

最近看了看我的计划,写道这里也算是到了一半,大部分都是讲的单一的 C++11 的用法,基本都是理论知识,就像我上大学的时候,老师一直讲理论知识,结局就是能去能不去的时候,我选择了后者。所以在这里穿插一下小的综合运用文章,让大家知道为什么要用 C++11 ,C++11 好在哪里,项目中如何运用 C++11.

首先介绍一下背景。在我们的工作中,避免不了多线程之间的配合。在现在处理器动辄 8 核 16 核的背景下,如果我们的程序还停留在单线程的模型,那么我们就没法享受多处理器带来的性能提升。之前看过我司代码中的 threadpool 。写的那叫一个滴水不漏,每个小细节都有大量的代码去实现。不但非常冗长,而且以我的智商基本上读不懂。唯一的有点就是:真稳定。不过 threadpool 的模型已经很难融入现代 C++ 了。

所以有必要通过 C++11 来重新实现一下 threadpool,对比一下 modern C++ 和 C98.

为什么要有 threadpool

如果谈论 threadpool ,你会想到有什么功能呢?

传统的模型大概是这样的,把一个函数指针传给 threadpool 。然后 thread 们会在合适的时候调用这个函数。那么还有一个问题就是函数的返回值怎么传递回调用的线程。这个功能往往有很多种方法,我司的思路就是调用你的 callback 将函数返回值返回给你。当然不是返回给调用函数的线程。

以上的描述中反映的 threadpool 的两个最基本的需求

  • 可以把一个可执行的对象扔给 threadpool 去执行。
  • 可以把执行的返回值带回。

其实这就是 threadpool 存在的合理性 —— 把工作扔给它,我只要知道结果就行。当然任务扔给 threadpool 后,你就可以去干一些别的工作。

有人会说,扔给 threadpool ,无非是让别的线程去干活,干的总活并没有减少。相反,一些 threadpool 的开销反而让工作变的更慢。至于这个问题我想用 redis 来举例子。

众所周知,redis 最新版本支持的多线程。redis 的作者在解释为什么引入多线程的时候说过。在他们维护 redis 的时候,发现 redis 的瓶颈竟然出现在分配内存上(从 socket 上拷贝内存)。所以你会发现 redis 起了多线,只是为了加速内存拷贝,最终的逻辑还是在一个线程执行的。所以可以看出,可以把较慢的代码或者可以流水操作的代码让不同的线程执行。

现代化 threadpool 提出了什么更高的要求

之前我们分享过 std::function 。std::function 是C++11提供的可执行代码的包装器,它可以是一个普通函数,或者是函数指针,或者是lambda... ,所以对于我们来说, threadpool 也要支持 std::function 能支持的类型。

关于返回值,还有如何返回到 calling thread,之前我们也分享过std::future 。

如果大家忘记了这两个概念,可以回去找找之前的文章复习一下。

走进 C++11(二十三) 函数对象包装器之 std::function

走进 C++11(二十七) 处理未来发生的事情 std::future

还有就是线程间的同步,之前我们分享过 std::condition_variable,如果忘记了,看看这个吧:

走进 C++11(三十)标准化条件变量 -- condition_variable

还有就是 thread 的包装器,我们用了 std::thread ,同样,如果记不清了,看看这个:

走进 C++11(二十四)一统江湖之线程 -- std::thread

至此我们凑齐了实现 threadpool 的几大件,下面我们看看如何来实现它

原理

对象定义

要实现一个 threadpool 。我们要有以下的信息:

  1. 我们要有个结构体,记住我们控制的 thread。
  2. 我们要有个结构体,记住我们要做的事情。
  3. 我们要有个 condition_variable 来做线程间同步。
  4. 为了优雅的退出,我们要有个标志位,标志着我现在想退出了,大家都退下吧。

功能上要有:

  1. 构造函数
  2. 析构函数
  3. 最重要的 -- 添加任务的函数

实现起来如下:

class ThreadPool
{
  public:
    ThreadPool(size_t);
    template <class F, class... Args>
    auto enqueue(F &&f, Args &&... args) -> std::future<typename std::result_of<F(Args...)>::type>;
    ~ThreadPool();


  private:
    // need to keep track of threads so we can join them
    std::vector<std::thread> workers;
    // the task queue
    std::queue<std::function<void()>> tasks;
    // synchronization
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

初始化

这里构建了我们需要的 thread 。并把它放在一个 vector 里。

这个 thread 只干一件事,那就是等 condition_variable 的通知,如果有通知,那么从 task queue 里边拿出一个 task ,并执行该 task 。

当然还有一些判断是否退出的逻辑。

inline ThreadPool::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(this->queue_mutex);
                        this->condition.wait(lock,
                                             [this] { return this->stop || !this->tasks.empty(); });
                        if (this->stop && this->tasks.empty())
                            return;
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }
                    task();
                }
            });
}


添加任务 API

说到函数,不可避免的就是函数的实现和函数的参数,为了实现支持不同的类型,我们选择了用模板来适配。

同时为了得到返回值,我们将返回值设置成了 future 。那么问题来了,这该如何实现?是不是想起了 packaged_task ? 如果忘了,回忆一下吧。

走进 C++11(二十九) 将工作打包成任务,丢给执行者 —— std::packaged_task

packaged_task 可以将可执行的工作打包,然后获取它的 future。

至此我们就可以实现我们的功能了。思路就是来了一个可执行的工作,首先封装成 packaged_task 。然后把这个 task 放到 task queue 中。并且通知一个线程说 queue 里边有东西了,赶紧去干活。

在返回之前,得到它的 future 并返回。

实现如下:

template <class F, class... Args>
auto ThreadPool::enqueue(F &&f, Args &&... args)
    -> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;
    auto task = std::make_shared<std::packaged_task<return_type()>>(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...));
    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        // don't allow enqueueing after stopping the pool
        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");
        tasks.emplace([task]() { (*task)(); });
    }
    condition.notify_one();
    return res;
}

至此,所有功能都实现了,有了 C++11,是不是一切都变得美好了起来,用了 60 行就实现了以前无数行才能实现的功能,而且简单易懂,支持现代化的 C++ 调用。