Creating a thread pool with `std::jthread`

In this blog post, we will explore how to create a thread pool using the new std::jthread class introduced in C++20. Thread pools are widely used in multi-threaded applications to efficiently manage and reuse threads for concurrent processing.

What is std::jthread?

First, let’s get familiar with the std::jthread class. It is a new addition to the C++ standard library in C++20. std::jthread is similar to std::thread, but with the added benefit of managing the lifecycle of the thread automatically.

Thread Pool Implementation

Now let’s move on to creating a thread pool using std::jthread. Here’s an example implementation:

#include <iostream>
#include <vector>
#include <functional>
#include <thread>

class ThreadPool {
public:
    explicit ThreadPool(size_t numThreads) : stop(false) {
        threads.reserve(numThreads);
        for (size_t i = 0; i < numThreads; ++i) {
            threads.emplace_back([this] {
                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;
                        }
                        
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    
                    task();
                }
            });
        }
    }
    
    template <typename F, typename... Args>
    auto 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> result = 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 result;
    }
    
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            stop = true;
        }
        
        condition.notify_all();
        
        for (std::thread& thread : threads) {
            thread.join();
        }
    }
    
private:
    std::vector<std::thread> threads;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;
};

How to Use the Thread Pool

To use the thread pool in your application, you can follow these simple steps:

  1. Create an instance of the ThreadPool class, specifying the desired number of threads in the pool.
  2. Use the enqueue method to submit tasks to the thread pool. The tasks can be any callable object that returns a value.
  3. Retrieve the result of the tasks using the returned std::future object.

Here’s an example usage of the thread pool:

ThreadPool pool(4);

auto task1 = pool.enqueue([]() {
    std::cout << "Task 1 executed" << std::endl;
});

auto task2 = pool.enqueue([](int x, int y) {
    return x + y;
}, 5, 10);

task1.get();
std::cout << "Task 2 result: " << task2.get() << std::endl;

Conclusion

In this blog post, we covered how to create a thread pool using std::jthread. Thread pools are a powerful tool for managing and reusing threads in multi-threaded applications, leading to improved performance and resource utilization.

By utilizing the new features in C++20, such as std::jthread, we can make our code more idiomatic and simplify the management of threads in our applications.

#threads #threadpool #stdjthread