Bootstrap

c++ 简单线程池

头文件

#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>

class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)->std::future<typename std::result_of<F(Args...)>::type>;
private:
    void workerThread();
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;

    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;
};


cpp

 #include "threadPool.h"

ThreadPool::ThreadPool(size_t numThreads) : stop(false)
{
    for (size_t i = 0; i < numThreads; ++i)
    {
        workers.emplace_back(&ThreadPool::workerThread, this);
    }
}

ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread& worker : workers) {
        worker.join();
    }
}

void ThreadPool::workerThread() {
    while (true) {
        std::function<void()> task;
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            condition.wait(lock, [this] { return stop || !tasks.empty(); });
            if (stop && tasks.empty()) {
                return;
            }
            if (!tasks.empty()) {
                task = std::move(tasks.front());
                tasks.pop();
            }
        }
        if (task) {
            task();
        }
    }
}

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

    auto task = std::make_shared<std::packaged_task<returnType()>>(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

    std::future<returnType> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queueMutex);

        if (stop) {
            throw std::runtime_error("enqueue on stopped ThreadPool");
        }

        tasks.emplace([task]() { (*task)(); });
    }
    condition.notify_one();
    return res;
}

int main() {
    ThreadPool pool(4);

    auto result1 = pool.enqueue([](int answer) { return answer; }, 42);
    auto result2 = pool.enqueue([](int a, int b) { return a + b; }, 5, 7);

    std::cout << "Result1: " << result1.get() << std::endl;
    std::cout << "Result2: " << result2.get() << std::endl;

    return 0;
}

解释

enqueue

Sure, let’s break down the enqueue function template step-by-step:

Template Definition

template<class F, class... Args>

This defines a function template that can take any callable object F (such as a function, function pointer, lambda, or functor) and a variadic list of arguments Args. The class... Args syntax allows the function to accept any number of additional arguments, making it very flexible.

Return Type

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

The return type of the function is a std::future that will hold the result of invoking the callable object F with arguments Args.... The std::result_of<F(Args...)>::type part determines the type that will be returned by calling F with Args....

  • std::result_of<F(Args...)>::type: This uses the std::result_of type trait to deduce the return type of calling the function F with arguments Args....

Function Body

auto task = std::make_shared<std::packaged_task<returnType()>>(
    std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);

Here, we are creating a std::shared_ptr to a std::packaged_task. A std::packaged_task wraps a callable object, allowing it to be executed asynchronously and providing a future to retrieve the result.

  • returnType: This is a type alias for the return type of the callable object F with arguments Args....
  • std::make_shared<std::packaged_task<returnType()>>: This creates a shared pointer to a std::packaged_task that will eventually execute the callable F with the provided arguments.
  • std::bind(std::forward<F>(f), std::forward<Args>(args)...): This binds the callable F with the provided arguments Args..., allowing them to be stored and called later.

Storing the Task

{
    std::unique_lock<std::mutex> lock(queueMutex);

    if (stop) {
        throw std::runtime_error("enqueue on stopped ThreadPool");
    }

    tasks.emplace([task](){ (*task)(); });
}

This block ensures that access to the task queue is synchronized using a mutex lock. It checks if the thread pool is stopped and throws an exception if it is. Otherwise, it adds the task to the queue.

  • std::unique_lock<std::mutex> lock(queueMutex): Locks the mutex to ensure thread-safe access to the task queue.
  • if (stop) { throw std::runtime_error("enqueue on stopped ThreadPool"); }: Checks if the thread pool has been stopped. If so, it throws an exception.
  • tasks.emplace([task](){ (*task)(); }): Adds a new task to the task queue. The task is a lambda function that calls the packaged_task to execute the callable F with its arguments.

Notifying Workers

condition.notify_one();

This line notifies one of the worker threads that a new task is available. The worker threads are waiting on this condition variable and will wake up to process the new task.

Returning the Future

return res;

Finally, the function returns the std::future associated with the packaged_task. This future can be used by the caller to get the result of the task once it has been executed by a worker thread.

Full Example for Clarity

Here’s the complete enqueue function with the detailed explanation:

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

    // Create a packaged_task to wrap the callable and its arguments
    auto task = std::make_shared<std::packaged_task<returnType()>>(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    );

    // Get the future associated with the task
    std::future<returnType> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queueMutex);

        // Ensure the thread pool is not stopped
        if (stop) {
            throw std::runtime_error("enqueue on stopped ThreadPool");
        }

        // Add the task to the queue
        tasks.emplace([task](){ (*task)(); });
    }

    // Notify one of the waiting worker threads
    condition.notify_one();
    return res;
}

Summary

  1. Template Parameters: F is the callable, Args... are the arguments.
  2. Return Type: A std::future that holds the result of the callable.
  3. Creating Packaged Task: Wraps the callable and its arguments to be executed asynchronously.
  4. Mutex Lock: Ensures thread-safe access to the task queue.
  5. Task Queue: Adds the task to the queue.
  6. Notify Worker: Notifies a worker thread to process the new task.
  7. Return Future: Provides a future to get the result of the task.

Next Steps

a. Add unit tests to verify the ThreadPool implementation.
b. Implement error handling for tasks within the ThreadPool.

enqueue

好的,下面是对每一行代码的详细解释:

函数模板声明

template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>
  • template<class F, class… Args>: 定义一个模板函数,该函数接受一个可调用对象 F 和一组参数 Args...
  • auto ThreadPool::enqueue(F&& f, Args&&… args): enqueueThreadPool 类的成员函数,它使用了模板参数 FArgs...,并通过右值引用接收这些参数(即使用 &&)。
  • -> std::future<typename std::result_of<F(Args…)>::type>: 函数返回类型是一个 std::future,其类型由 F 调用 Args... 参数后返回的结果类型决定。

使用别名 returnType

using returnType = typename std::result_of<F(Args...)>::type;
  • using returnType: 使用类型别名将 std::result_of<F(Args...)>::type 定义为 returnType
  • std::result_of<F(Args…)>::type: 通过 std::result_of 获取可调用对象 F 使用参数 Args... 后的返回类型。

创建 packaged_task

auto task = std::make_shared<std::packaged_task<returnType()>>(
    std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
  • std::make_shared<std::packaged_task<returnType()>>: 创建一个 std::shared_ptr,指向一个 std::packaged_task 对象。
  • std::packaged_task<returnType()>: std::packaged_task 是一个模板类,用于包装一个可调用对象,以便异步调用并获取其结果。
  • std::bind(std::forward(f), std::forward(args)…): 使用 std::bind 将可调用对象 F 和参数 Args... 绑定在一起,生成一个新的可调用对象,并将其传递给 std::packaged_taskstd::forward 确保参数的完美转发。

获取 future

std::future<returnType> res = task->get_future();
  • std::future res: 定义一个 std::future 对象 res,用于保存 packaged_task 的结果。
  • task->get_future(): 调用 packaged_taskget_future 方法,获取与任务关联的 std::future 对象。

加锁并检查线程池状态

{
    std::unique_lock<std::mutex> lock(queueMutex);
    if (stop) {
        throw std::runtime_error("enqueue on stopped ThreadPool");
    }
    tasks.emplace([task](){ (*task)(); });
}
  • std::unique_lockstd::mutex lock(queueMutex): 创建一个互斥锁对象 lock,并锁定 queueMutex,确保对任务队列的访问是线程安全的。
  • if (stop): 检查线程池是否已停止接受新任务。
  • throw std::runtime_error(“enqueue on stopped ThreadPool”): 如果线程池已停止,抛出一个运行时错误异常。
  • tasks.emplace(task{ (*task)(); }): 将一个新的任务添加到任务队列中。这个任务是一个 lambda 函数,调用 task 对象的 operator() 来执行实际的任务。

通知一个等待的线程

condition.notify_one();
  • condition.notify_one(): 通知一个正在等待 condition 的线程,让它从等待中醒来,以便处理新添加的任务。

返回 future

return res;
  • return res: 返回先前获取的 std::future 对象,让调用者可以在稍后获取任务的结果。

为什么使用 std::future 的详细解释:

  1. 异步任务的结果获取
    当我们将一个任务提交到线程池时,任务是异步执行的。std::future 提供了一种机制,让我们可以在任务执行完成后,获取其结果,而不需要阻塞主线程或者轮询状态。
auto result = pool.enqueue([](int x) { return x * x; }, 10);
std::cout << result.get() << std::endl; // 获取任务结果
  1. 同步等待任务完成
    std::future 提供了 get() 方法,这个方法会阻塞调用它的线程,直到任务完成并返回结果。这样,我们可以在需要的时候等待任务完成并获取结果。
auto result = pool.enqueue([](int x) { return x * x; }, 10);
result.get(); // 阻塞等待任务完成并获取结果
  1. 异常传播
    如果任务在执行过程中抛出异常,std::future 会捕获这个异常,并在调用 get() 时重新抛出。这使得我们可以在调用 get() 时处理任务中的异常。
auto result = pool.enqueue([]() { throw std::runtime_error("Error"); });
try {
    result.get();
} catch (const std::exception& e) {
    std::cout << "Caught exception: " << e.what() << std::endl;
}
  1. 使用简单方便
    std::future 和 std::promise 以及 std::packaged_task 的组合使用,使得在多线程环境中管理任务和获取任务结果变得非常简单和方便。

参考资料
chatgpt
现代 C++ 编程实战

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;