Learning c++11 by a thread pool

516 阅读4分钟

ThreadPool

  • 简述

一个线程池的simple implementation,包含很多modern c++的细节

#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
#include <iostream>
#include <vector>
#include <chrono>

#include "ThreadPool.h"

int main()
{
    
    ThreadPool pool(4);
    std::vector< std::future<int> > results;

    for(int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
                std::cout << "hello " << i << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << "world " << i << std::endl;
                return i*i;
            })
        );
    }

    for(auto && result: results)
        std::cout << result.get() << ' ';
    std::cout << std::endl;
    
    return 0;
}

代码分析

ThreadPool类声明

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>;
    /*
    1.class 与 typename
    2.可变参数模板(varadic template)
    3.万能引用Universal References(此处应该不是右值引用)
    4.auto + -> 模板返回值类型推导
    5.std::future 异步机制
    6.std::result_of + ::type 在编译时推导INVOKE表达式的类型
    */
    ~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;
};

函数需要在多线程中运行,但是每来一个函数就开启一个线程的方式缺点很多,所以需要固定的N个线程来跑执行,但是有的线程还没有执行完,有的又在空闲,如何分配任务呢,可以封装一个线程池来完成这些操作,有了线程池这层封装,你就只需要告诉它开启几个线程,然后直接塞任务就行了,然后通过一定的机制获取执行结果。

线程池的数据区(private),首先需要有一些工作线程workers,需要有一个队列管理任务tasks,为了实现线程间的异步逻辑,需要一把锁queue_mutex,一个条件变量condition来完成线程等待和唤醒(有task唤醒,无task等待)。

数据区都算好理解,比较复杂的是enqueue这个函数,要素众多,都写在注释里

构造析构

// 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();
                }
            }
        );
}

// 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();
}
  • 构造函数在做什么?

在主线程中使用std::thread创建几个子线程(创建好后塞入vector里),线程入口函数是一个lambda function,注意此时主线程把lambda塞完就继续往下走了,构造函数执行完了。

那我们放下主线程(假设主线程先sleep一会儿好了),先看看子线程去干吗了,子线程执行这个lambda function,这个lambda 做了什么呢?让子线程执行一个死循环,那循环里做了什么?首先某个先进来的子线程通过std::unique_lock拿到对象的mutex,吧唧,锁上,(其他子线程拿不到锁,给我看着别动),好的,此时第一个进来的子线程召唤对象的条件变量condition,等待某个条件,那么等待什么条件呢this->stop || !this->tasks.empty(),指的是线程池关闭了或者是任务队列不为空,如果条件满足,那就往下走,不满足就直接锁住,直到条件满足,或者其他其他线程调用了条件变量的 nofity 函数来唤醒(注意哦,现在其他几个子线程是被阻塞住的,是不可能来唤醒的,只能让主线程来唤醒),先不考虑线程池关闭,第一次进来当前任务是空的呀,那就等着吧。此时应该可以想到,要让主线程快往任务队列里塞东西呀,子线程好往下走。

假设此时主线程靓仔睡醒了,往任务队列里塞了一个任务,再notify_one通知刚才那个在等的条件变量,(注意子线程要通过条件变量通知wait住的子线程,否则wait是不会持续不断的检测条件变量是否满足的)假设靓仔马上又睡着了,我们再去看刚才的第一个子线程,锁住的第一个进来的子线程收到通知,发现条件满足了,队列有东西了,继续往下走,取出队列里的第一个任务,然后离开了unique_lock的作用域,第一个线程开始执行刚拿到的任务,第一个线程开始执行刚拿到的任务,第二个进来的线程取到锁重复上述过程。

也就是说,构造函数里创建了子线程,如果任务队列里一直有任务,子线程就会马上去执行这些任务,不会阻塞。

  • 析构在做什么?

需要等子线程把任务都执行完了,主线程再取到锁,修改stop的状态,通知子线程的conditiion 不再wait后释放线程资源。

  • enque在做什么?

流程很简单,接收一个functor任务,取到线程池的锁,将functor推入任务队列后通知某个空闲的子线程来取。比较复杂的地方在模板元的部分,在此不赘述