详解100行c11线程池 ThreadPool.h

介绍

这个大神的100行实现c11线程池,真的是相当简洁给力,偶尔会在项目里面使用,但是老实说一直是迷迷糊糊,并不清楚具体实现细节,现在有空学习了一波,记录一下,方便以后查阅。

ThreadPool.h

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

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;
};
 
// the constructor just launches some amount of workers
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();
                }
            }
        );
}

// add new work item to the pool
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;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers)
        worker.join();
}

#endif

解释

现在将我觉得可能不太好理解的c11语法做一下解释。

1
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>;

第一行是模板和可变长参数,有几个点需要注意:

  • template<class F> 和 template<typename F>
    对普通操作来说,没有什么区别,可能产生区别的用法请参考此文

  • auto
    此处auto是后面的lamda表达式声明的返回值的占位符

  • enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>
    这里是用lamda表达式声明了函数enqueue,标准格式为[capture](parameters)->return-type{body},这里只是声明,因此只保留了参数和返回类型。

  • std::future<typename std::result_of<F(Args...)>::type>
    既然前面说到了->符号后面是函数的返回类型,那么究竟是什么类型呢?第一层std::future说明这个返回值是future类。我们知道future类声明需要指定其返回值类型,因此<>中就是future的返回类型,这里用了result_of< >::type,说明这个返回类型就是第一个参数F调用第二个参数Args的返回类型。

到此类声明就讲完了。

2

再来说说构造函数。

  • workers.emplace_back([]{});
    这句话很有意思,有两点需要注意,workers类型是vector,如果用vector::push_back(),应该要传入push_back(std::Thread([]{})),先调用Thread类的构造函数,然后在真正进入vector的时候调用拷贝构造函数,也就是说需要一次构造一次拷贝构造。c11建议使用emplace_back,它的好处是,不需要两次构造,直接传入构造函数的参数,只需要在真正进入vector的时候进行一次构造,所以这里传入的是一个lamda表达式形式的函数,这个函数是std::Thread类构造的参数,在这里直接通过emplace_back构造并同时存入vector中。

  • for(;;)引导的无限循环
    这里面有两点需要注意,一是condition.wait(lock, pred),这里wait函数被调用的时候,先判断pred,如果是false,则锁住当前线程;在该线程被唤醒的时候,也要判断pred,如果是true才解锁并继续往下进行。 二是std::move(this->tasks.front()),这里用move的函数也是节省了拷贝构造的时间。for循环开始后,当task执行完,会继续循环从任务队列的头部取出task继续执行,如果所有task都已经执行完,则退出循环线程结束。

3

然后是任务入列函数enqueue,关于这个函数的参数和返回类型前面已经说过。

  • auto task = std::make_shared< std::packaged_task<return_type()> >(
    std::bind(std::forward(f), std::forward(args)…));
    这应该是全文最难理解的一句。从最外层往里看,task是一个共享指针,指向一个packged_task,这个packged_task的返回类型是typename std::result_of<F(Args…)>::type,简单说也就是F(Args…)的返回类型,然后这个packged_task的主体是std::bind(std::forward(f), std::forward(args)…),bind是函数适配器,将函数f和参数args…进行绑定形成新的函数,forward是完美转发。

  • tasks.emplace([task](){ (* task)(); })
    和刚才的emplace_back一样,这里调用queue的emplace方法,通用只需要传入一个函数,避免两次构造。

  • condition.notify_one();
    通知一个线程可以获取锁,并从queue中启动任务。

总结

  1. 碰到难懂的代码我们需要拆分,逐个理解=
  2. enqueue的返回值是一个future,没有看出这个future有什么用途,唯一可行的解释是可以直接通过这些future的get或wait方法直接运行任务。
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值