C++11 ThreadPool(一个头文件实现线程池)

266 阅读5分钟
#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

enqueue

enqueue函数定义

该函数的目的是将任务添加到线程池的任务队列中,并返回一个 std::future 对象,以便调用者可以在任务完成后获取其结果。

具体来说,enqueue 函数是一个模板函数,接受一个可调用对象 f 和一组参数 args。这些参数使用了完美转发(perfect forwarding),即通过 F&& 和 Args&&... 来实现。这种方式确保了参数的原始类型和值类别(左值或右值)得以保留。

函数的返回类型是 std::future<typename std::result_of<F(Args...)>::type>。这里使用了 std::result_of 元编程工具来推断可调用对象 f 使用参数 args... 调用后的返回类型。std::future 是一个标准库类模板,用于表示异步操作的结果。通过返回 std::future 对象,调用者可以在稍后某个时间点等待任务完成并获取其结果。

enqueue函数实现

首先,函数模板参数 F 和 Args 分别表示任务函数类型和其参数类型。函数返回类型是一个 std::future 对象,类型为任务函数的返回类型。通过 std::result_of<F(Args...)>::type 获取任务函数的返回类型,并将其定义为 return_type

接下来,使用 std::make_shared 创建一个 std::packaged_task 对象,并将任务函数和参数绑定到一起。std::packaged_task 是一个包装器,它将任务函数封装起来,使其可以异步执行。std::bind 用于将任务函数和参数绑定在一起,而 std::forward 则确保参数的完美转发。

然后,通过 task->get_future() 获取一个 std::future 对象 res,用于稍后获取任务的结果。接着,使用 std::unique_lock 锁住互斥量 queue_mutex,以确保线程安全地访问任务队列。

在锁的保护下,首先检查线程池是否已经停止,如果是,则抛出一个运行时错误。否则,将任务添加到任务队列中。任务被封装在一个 lambda 表达式中,该表达式在执行时会调用 task 对象。

最后,使用 condition.notify_one() 唤醒一个等待的线程,以便它可以从任务队列中取出并执行任务。函数返回 std::future 对象 res,调用者可以通过它获取任务的执行结果。

析构函数

首先,使用 std::unique_lock 锁住互斥量 queue_mutex,并将 stop 标志设置为 true,表示线程池即将停止,不再接受新的任务。

接下来,调用 condition.notify_all(),唤醒所有等待条件变量的线程。这些线程会检查 stop 标志,并终止它们的执行。

最后,遍历 workers 容器中的每一个线程对象,并调用 join() 方法,确保主线程等待所有工作线程完成执行后再继续。这一步是必要的,以确保所有线程在析构函数返回之前都已安全地终止,从而避免资源泄漏或未定义行为。

构造函数

首先,构造函数接受一个参数 threads,表示要创建的线程数量。初始化列表中将 stop 标志设置为 false,表示线程池开始时不停止。

接下来,使用一个 for 循环创建指定数量的线程。每个线程通过 workers.emplace_back 方法添加到 workers 容器中。每个线程执行一个无限循环 for(;;),不断从任务队列中提取任务并执行。

在每个线程的循环内部,首先声明一个 std::function<void()> 类型的变量 task,用于存储从任务队列中提取的任务。然后,使用 std::unique_lock<std::mutex> 锁住 queue_mutex 互斥量,确保对任务队列的访问是线程安全的。

接下来,线程调用 condition.wait 方法等待条件变量,直到 stop 标志为 true 或任务队列不为空。如果 stop 标志为 true 且任务队列为空,线程将返回并终止执行。否则,从任务队列中取出一个任务并将其移到 task 变量中,然后从队列中移除该任务。

最后,调用 task() 执行提取的任务。这个过程在每个线程中不断重复,直到线程池被停止。

通过这种方式,线程池能够并发地处理多个任务,并在需要时安全地停止所有线程。