头文件
cpp
#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
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
cpp
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
cpp
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 thestd::result_of
type trait to deduce the return type of calling the functionF
with argumentsArgs...
.
Function Body
cpp
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 objectF
with argumentsArgs...
.std::make_shared<std::packaged_task<returnType()>>
: This creates a shared pointer to astd::packaged_task
that will eventually execute the callableF
with the provided arguments.std::bind(std::forward<F>(f), std::forward<Args>(args)...)
: This binds the callableF
with the provided argumentsArgs...
, allowing them to be stored and called later.
Storing the Task
cpp
{
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 thepackaged_task
to execute the callableF
with its arguments.
Notifying Workers
cpp
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
cpp
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:
cpp
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
- Template Parameters :
F
is the callable,Args...
are the arguments. - Return Type : A
std::future
that holds the result of the callable. - Creating Packaged Task: Wraps the callable and its arguments to be executed asynchronously.
- Mutex Lock: Ensures thread-safe access to the task queue.
- Task Queue: Adds the task to the queue.
- Notify Worker: Notifies a worker thread to process the new task.
- 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
好的,下面是对每一行代码的详细解释:
函数模板声明
cpp
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) :
enqueue
是ThreadPool
类的成员函数,它使用了模板参数F
和Args...
,并通过右值引用接收这些参数(即使用&&
)。 - -> std::future<typename std::result_of<F(Args...)>::type> : 函数返回类型是一个
std::future
,其类型由F
调用Args...
参数后返回的结果类型决定。
使用别名 returnType
cpp
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
cpp
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_task
。std::forward
确保参数的完美转发。
获取 future
cpp
std::future<returnType> res = task->get_future();
- std::future res : 定义一个
std::future
对象res
,用于保存packaged_task
的结果。 - task->get_future() : 调用
packaged_task
的get_future
方法,获取与任务关联的std::future
对象。
加锁并检查线程池状态
cpp
{
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()
来执行实际的任务。
通知一个等待的线程
cpp
condition.notify_one();
- condition.notify_one() : 通知一个正在等待
condition
的线程,让它从等待中醒来,以便处理新添加的任务。
返回 future
cpp
return res;
- return res : 返回先前获取的
std::future
对象,让调用者可以在稍后获取任务的结果。
为什么使用 std::future 的详细解释:
- 异步任务的结果获取
当我们将一个任务提交到线程池时,任务是异步执行的。std::future 提供了一种机制,让我们可以在任务执行完成后,获取其结果,而不需要阻塞主线程或者轮询状态。
cpp
auto result = pool.enqueue([](int x) { return x * x; }, 10);
std::cout << result.get() << std::endl; // 获取任务结果
- 同步等待任务完成
std::future 提供了 get() 方法,这个方法会阻塞调用它的线程,直到任务完成并返回结果。这样,我们可以在需要的时候等待任务完成并获取结果。
cpp
auto result = pool.enqueue([](int x) { return x * x; }, 10);
result.get(); // 阻塞等待任务完成并获取结果
- 异常传播
如果任务在执行过程中抛出异常,std::future 会捕获这个异常,并在调用 get() 时重新抛出。这使得我们可以在调用 get() 时处理任务中的异常。
cpp
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;
}
- 使用简单方便
std::future 和 std::promise 以及 std::packaged_task 的组合使用,使得在多线程环境中管理任务和获取任务结果变得非常简单和方便。
参考资料
chatgpt
现代 C++ 编程实战