Search code examples
c++c++17scheduler

C++Common std::make_unique, std::packaged_task and std::promise Problem


The Problem When creating schedulers the last copy or move of a function object is the last place that the function object is ever referenced (by a worker thread). If you were to use a std::function to store functions in the scheduler then any std::promises or std::packaged_task or other similarly move only types don't work as they cannot be copied by std::function.

Similarly, if you were to use std::packaged_task in the scheduler it imposes unnecessary overhead as many tasks do not require the std::future returned by packaged task at all.

The common and not great solution is to use a std::shared_ptr<std::promise> or a std::shared_ptr<std::packaged_task> which works but it imposes quite a lot of overhead.

The solution A make_owner, similar to make_unique with one key difference, a move OR copy simply transfers control of destruction of the object. It is basically identical to std::unique_ptr, except that it is copyable (it basically always moves, even on a copy). Grosss....

This means that moving of std::functions doesn't require copies of the std::shared_ptr which require reference counting and it also means there is significantly less overhead on the reference counting etc. A single atomic pointer to the object would be needed and a move OR copy would transfer control. The major difference being that copy also transfers control, this might be a bit of a no-no in terms of strict language rules but I don't see another way around it.

This solution is bad because:

  • It ignores copy symantics.
  • It casts away const (in copy constructor and operator =)

Grrr It isn't as nice of a solution as I'd like so if anybody knows another way to avoid using a shared pointer or only using packaged_tasks in a scheduler I'd love to hear it because I'm stumped...

I am pretty unsatisfied with this solution.... Any ideas? I am able to re-implement std::function with move symantics but this seems like a massive pain in the arse and it has its own problems regarding object lifetime (but they already exist when using std::function with reference capture).

Some examples of the problem:

EDIT Note in the target application I cannot do std::thread a (std::move(a)) as the scheduler threads are always running, at most they are put in a sleep state, never joined, never stopped. A fixed number of threads are in the thread pool, I cannot create threads for each task.

auto proms = std::make_unique<std::promise<int>>();
auto future = proms->get_future();

std::thread runner(std::move(std::function( [prom = std::move(proms)]() mutable noexcept
{
    prom->set_value(80085);
})));

std::cout << future.get() << std::endl;
std::cin.get();

And an example with a packaged_task

auto pack = std::packaged_task<int(void)>
(   [] 
    {   
        return 1; 
    });
auto future = pack.get_future();

std::thread runner(std::move(std::function( [pack = std::move(pack)]() mutable noexcept
{
    pack();
})));

std::cout << future.get() << std::endl;
std::cin.get();

EDIT

I need to do this from the context of a scheduler, I won't be able to move to the thread.

Please note that the above is minimum re-producible, std::async is not adequate for my application.


Solution

  • The main question is: Why you want to wrap a lambda with std::function before passing it to the std::thread constructor?

    It is perfectly fine to do this:

    std::thread runner([prom = std::move(proms)]() mutable noexcept
    {
        prom->set_value(80085);
    });
    

    You can find the explanation of why std::function does not allow you to store a move-only lambda here.

    If you were going to pass std::function with wrapped lambda to some function, instead of:

    void foo(std::function<void()> f)
    {
        std::thread runner(std::move(f));
        /* ... */
    }
    
    foo(std::function<void()>([](){}));
    

    You can do this:

    void foo(std::thread runner)
    {
        /* ... */
    }
    
    foo(std::thread([](){}));
    

    Update: It can be done in an old-fashioned way.

    std::thread runner([prom_deleter = proms.get_deleter(), prom = proms.release()]() mutable noexcept
    {
        prom->set_value(80085);
        // if `proms` deleter is of a `default_deleter` type
        // the next line can be simplified to `delete prom;`
        prom_deleter(prom);
    });