I recently implemented a thread pool using std::functions. I only supported functions taking no arguments on the assumption that there would be no need to have them - one could just bind the argument to the std::function or capture it in a lambda.
I quickly realised the limitation with this approach - std::function requires bound objects to be copyable (I wanted to bind a std::unique_ptr). Binding by reference is also not an option since it's not safe in asynchronous contexts. The solution I've come up with is to support functions with arguments and store the parameters alongside the function. My implementation for storing parameters is below. I've left out the actual thread pool part and all the asynchronous bits and bobs because it's unnecessary noise.
#include <functional>
#include <utility>
#include <tuple>
#include <queue>
#include <iostream>
template <typename TupleT, std::size_t... I>
void invoke_impl(TupleT&& t, std::index_sequence<I...>)
{
std::get<0>(t)(std::move(std::get<1 + I>(t))...);
}
template <typename... ArgTypes>
class thread_pool
{
// First element is the function to be invoked. Remaining
// elements are the function arguments.
using job_t = std::tuple<
std::function<void(ArgTypes...)>,
ArgTypes...>;
std::queue<job_t> _jobs;
constexpr static auto param_index_sequence()
{
return std::make_index_sequence<sizeof...(ArgTypes)>();
}
public:
template <typename FuncT, typename ...Args>
void post_job(FuncT&& func, Args&&... args)
{
_jobs.emplace(
std::forward<FuncT>(func),
std::forward<Args>(args)...);
}
// This would normally be run by the worker threads.
void run_job()
{
job_t j = std::move(_jobs.front());
_jobs.pop();
invoke_impl(std::move(j), param_index_sequence());
}
};
struct obj
{
obj() { std::cout << "constructed\n"; }
~obj() { std::cout << "destructed\n"; }
obj(const obj&) { std::cout << "copied\n"; }
obj& operator=(const obj&) { std::cout << "copy-assigned\n"; return *this; }
obj(obj&&) { std::cout << "moved\n"; }
obj& operator=(obj&&) { std::cout << "move-assigned\n"; return *this; }
};
int main()
{
thread_pool<obj> tp;
// 4 moves
tp.post_job([] (obj) {}, obj());
tp.run_job();
// 3 moves
tp.post_job([] (const obj&) {}, obj());
tp.run_job();
}
I'd especially appreciate thoughts on the following:
- Is this the best approach?
- Can I reduce the number of potentially-expensive moves required? Perhaps storing the arguments separately in a
std::unique_ptr<std::tuple<ArgTypes...>>would be a better option... - Am I using
std::moveandstd::forwardoptimally? - Can I make my templates more flexible/robust? e.g. by using
std::remove_reference,std::remove_cvetc. - I've intentionally kept to C++14 (hence my using
std::get<0>(j)(...)rather thanstd::invoke). I'd like to keep to C++14 but I'd be interested about newer features that make my implementation simpler or even completely redundant.