Document Number: N2709=08-0219
Date: 2008-07-28
Project: Programming Language C++

N2709 Packaging Tasks for Asynchronous Execution


Table of Contents

Overview
Rationale
Spawning a task with std::packaged_task
Spawning a task with std::promise
Using std::packaged_task with thread pools
Single-threaded usage
Proposed Wording
packaged_task class template
Task Constructor
Move Constructor
Move Assignment Operator
Destructor
Member Function operator bool
Member Function get_future()
Member Function operator()
Member Function reset

This proposal is based on the proposal for std::packaged_task in N2561 and N2627. It has been separated from the futures proposal at the request of the LWG, and revised in light of LWG comments at the Sophia-Antipolis meeting in June 2008.

A std::packaged_task provides a means of binding a function to an asynchronous future values so that when the function is invoked, the asynchronous future value becomes populated, rather than the return value or exception being propagated to the caller.

This provides an answer to the perennial question "how do I return a value from a thread?": package the function you wish to run as a std::packaged_task and pass the packaged task to the thread constructor. The future retrieved from the packaged task can then be used to obtain the return value. If the function throws an exception, that is stored in the future in place of the return value.

int calculate_the_answer_to_life_the_universe_and_everything()
{
    return 42;
}

std::packaged_task<int()> pt(calculate_the_answer_to_life_the_universe_and_everything);
std::unique_future<int> fi=pt.get_future();

std::thread task(std::move(pt)); // launch task on a thread

fi.wait(); // wait for it to finish
    
assert(fi.is_ready());
assert(fi.has_value());
assert(!fi.has_exception());
assert(fi.get_state()==std::future_state::ready);
assert(fi.get()==42);

Of course, this is not the only use for std::packaged_task: it can be used to store a task for later invocation, much like std::function.

With this revision, std::packaged_task has been remodelled to be more in line with std::function than in N2627: the function call operator now accepts arguments which are passed on to the encapsulated function, and the template parameter is a function call signature. The key difference is that the result of the function invocation is stored in the associated future rather than returned directly to the caller.

Though std::packaged_task doesn't provide any features that can't be built using a combination of std::promise from N2671 and std::function, I believe it to be a common-enough usage scenario that it warrants being standardized. In particular, the equivalent code using std::promise is cumbersome. For comparison, let's look at what's needed to spawn a task on a new thread and retrieve the return value in a std::unique_future.

template<typename F>
std::unique_future<typename std::result_of<F()>::type> spawn_task(F f)
{
    typedef typename std::result_of<F()>::type result_type;
    std::packaged_task<result_type()> task(std::move(f));
    std::unique_future<result_type> res(task.get_future());
    std::thread(std::move(task));
    return res;
}

This builds a std::packaged_task for the function, obtains a std::unique_future for the result, moves that task onto a thread (which is automatically detached), and returns the future.

This can easily be extended to handle additional arguments.

template<typename F>
std::unique_future<typename std::result_of<F()>::type> spawn_task(F f)
{
    typedef typename std::result_of<F()>::type result_type;

    struct local_task
    {
        std::promise<result_type> promise;
        F func;

        local_task(local_task const& other)=delete;

        local_task(F func_):
            func(func_)
        {}

        local_task(local_task&& other):
            promise(std::move(other.promise)),
            f(std::move(other.f))
        {}

        void operator()
        {
            try
            {
                promise.set_value(f());
            }
            catch(...)
            {
                promise.set_exception(std::current_exception());
            }
        }
    };
    
    local_task task(std::move(f));

    std::unique_future<result_type> res(task.promise.get_future());
    std::thread(std::move(task));
    return res;
}

local_task performs the same function as std::packaged_task, but in a much more limited manner. It is easy to imagine users getting it wrong by forgetting to catch the exception, or creating the promise as a local variable and using a lambda as the thread function (and thus getting a dangling reference to the promise).

std::packaged_task is intended to be usable as a basic building block of a thread pool. Whereas with spawn_task as presented above the task is immediately associated with a thread, in a thread pool the task could be put on a queue to be executed by a worker thread at a later point. The worker threads would then only need to know they were executing tasks from the queue, without having to worry about what the functions were, or the details of storing the results.

Advanced thread pool implementations would potentially require access to the internals of std::packaged_task and/or std::unique_future, and would therefore have to be provided as part of the same implementation, or require extensions to the interfaces. Such extensions are left for a later proposal, as thread pools are not being standardized for C++0x.

Though std::packaged_task is ideally suited to multi-threaded usage, it can also be used in a single-threaded scenario too. In this case, it is important not to wait() for the future result, since there isn't a background thread to prepare the result, but it does allow an unspecified task to be invoked only when necessary.

std::packaged_task<int(int,int)> task(create_task());
std::unique_future<int> future;

while(!done())
{
    int a,b;
    if(future_value_needed())
    {
        if(!future.is_ready())
            task(a,b);

        do_something(future.get());
    }
}

If the task needs to be invoked multiple times, but should only be invoked when necessary, then reset() can be called after the task has been invoked in order to reset the state for a subsequent invocation. It is important to note that in this case, a fresh std::unique_future must be obtained (by calling get_future()) after the call to reset(): any futures already obtained will continue to refer to the result of the prior invocation.

template<class> class packaged_task; // undefined

template<class R,class... ArgTypes>
class packaged_task<R(ArgTypes...)>
{
public:
    typedef R result_type;

    // construction and destruction
    packaged_task();

    template <class F>
    explicit packaged_task(F const& f);

    explicit packaged_task(R(*f)());
    
    template <class F>
    explicit packaged_task(F&& f);

    template <class F, class Allocator>
    explicit packaged_task(allocator_arg_t,Allocator const& a, F f);
    template <class F, class Allocator>
    explicit packaged_task(allocator_arg_t,Allocator const& a, F&& f);

    ~packaged_task();

    // no copy
    packaged_task(packaged_task&) = delete;
    packaged_task& operator=(packaged_task&) = delete;

    // move support
    packaged_task(packaged_task&& other);
    packaged_task& operator=(packaged_task&& other);

    void swap(packaged_task&& other);

    explicit operator bool() const;

    // result retrieval
    unique_future<R> get_future();        

    // execution
    void operator()(ArgTypes... );

    void reset();
};
template<typename F>
packaged_task(F const &f);

packaged_task(R(*f)());

template<typename F>
packaged_task(F&&f);

Preconditions:

f() is a valid expression with a return type convertible to R. Invoking a copy of f shall behave the same as invoking f.

Effects:

Constructs a new std::packaged_task with a copy of f stored as the associated task.

Throws:

Any exceptions thrown by the copy (or move) constructor of f. std::bad_alloc if memory for the internal data structures could not be allocated.

packaged_task(packaged_task && other);

Effects:

Constructs a new std::packaged_task, and transfers ownership of the task associated with other to *this, leaving other with no associated task.

Throws:

Nothing.

packaged_task& operator=(packaged_task && other);

Effects:

Transfers ownership of the task associated with other to *this, leaving other with no associated task. If there was already a task associated with *this, and that task has not been invoked, sets any futures associated with that task to ready with a std::broken_promise exception as the result.

Throws:

Nothing.

~packaged_task();

Effects:

Destroys *this. If there was a task associated with *this, and that task has not been invoked, sets any futures associated with that task to ready with a std::broken_promise exception as the result.

Throws:

Nothing.

explicit operator bool() const

Returns:

true if *this has an associated task, otherwise false.

Throws:

nothing.

unique_future<R> get_future();

Effects:

Returns a std::unique_future associated with the result of the task associated with *this.

Throws:

std::bad_function_call if !*this. std::future_already_retrieved if the future associated with the task has already been retrieved.

void operator()(ArgTypes... args);

Effects:

INVOKE (f, t1, t2, ..., tN, R), where f is the task associated with *this and t1, t2, ..., tN are the values in args.... If the task returns normally, the return value is stored as the asynchronous result associated with *this, otherwise the exception thrown is stored. Any threads blocked waiting for the asynchronous result associated with this task are woken.

Postconditions:

All futures waiting on the asynchronous result are ready

Throws:

std::bad_function_call if !*this. std::task_already_started if the task has already been invoked.

void reset();

Effects:

Return the task to a state as-if a newly-constructed instance has just been assigned to *this by *this=packaged_task(std::move(f)) where f is the task associated with *this. If there was already a task associated with *this, and that task has not been invoked, sets any futures associated with that task to ready with a std::broken_promise exception as the result. get_future() may now be called again for *this.

Postconditions:

*this has no associated futures. If *this had an associated task, then the associated task is a copy of the old task.

Throws:

std::bad_alloc if memory for the internal data structures of the new asynchronous result could not be allocated.