Search code examples
c++multithreadingc++11stdthreadpackaged-task

Implementing a simple, generic thread pool in C++11


I want to create a thread pool for experimental purposes (and for the fun factor). It should be able to process a wide variety of tasks (so I can possibly use it in later projects).


In my thread pool class I'm going to need some sort of task queue. Since the Standard Library provides std::packaged_task since the C++11 standard, my queue will look like std::deque<std::packaged_task<?()> > task_queue, so the client can push std::packaged_tasks into the queue via some sort of public interface function (and then one of the threads in the pool will be notified with a condition variable to execute it, etc.).


My question is related to the template argument of the std::packaged_task<?()>s in the deque.

The function signature ?() should be able to deal with any type/number of parameters, because the client can do something like:

std::packaged_task<int()> t(std::bind(factorial, 342)); thread_pool.add_task(t);

So I don't have to deal with the type/number of parameters.

But what should the return value be? (hence the question mark)

  • If I make my whole thread pool class a template class, one instance of it will only be able to deal with tasks with a specific signature (like std::packaged_task<int()>).

    I want one thread pool object to be able to deal with any kind of task.

  • If I go with std::packaged_task<void()> and the function invoked returns an integer, or anything at all, then thats undefined behaviour.


Solution

  • So the hard part is that packaged_task<R()> is move-only, otherwise you could just toss it into a std::function<void()>, and run those in your threads.

    There are a few ways around this.

    First, ridiculously, use a packaged_task<void()> to store a packaged_task<R()>. I'd advise against this, but it does work. ;) (what is the signature of operator() on packaged_task<R()>? What is the required signature for the objects you pass to packaged_task<void()>?)

    Second, wrap your packaged_task<R()> in a shared_ptr, capture that in a lambda with signature void(), store that in a std::function<void()>, and done. This has overhead costs, but probably less than the first solution.

    Finally, write your own move-only function wrapper. For the signature void() it is short:

    struct task {
      template<class F,
        class dF=std::decay_t<F>,
        class=decltype( std::declval<dF&>()() )
      >
      task( F&& f ):
        ptr(
          new dF(std::forward<F>(f)),
          [](void* ptr){ delete static_cast<dF*>(ptr); }
        ),
        invoke([](void*ptr){
          (*static_cast<dF*>(ptr))();
        })
      {}
      void operator()()const{
        invoke( ptr.get() );
      }
      task(task&&)=default;
      task&operator=(task&&)=default;
      task()=default;
      ~task()=default;
      explicit operator bool()const{return static_cast<bool>(ptr);}
    private:
      std::unique_ptr<void, void(*)(void*)> ptr;
      void(*invoke)(void*) = nullptr;
    };
    

    and simple. The above can store packaged_task<R()> for any type R, and invoke them later.

    This has relatively minimal overhead -- it should be cheaper than std::function, at least the implementations I've seen -- except it does not do SBO (small buffer optimization) where it stores small function objects internally instead of on the heap.

    You can improve the unique_ptr<> ptr container with a small buffer optimization if you want.