Document number: N2096=06-0166
Programming Language C++, Evolution and Library Subgroups
 
Peter Dimov, <pdimov@pdimov.com>
 
2006-09-07

Transporting Values and Exceptions between Threads

I. Overview

A. Future

Most existing proposals and implementations for returning a value from a function executed in a separate thread tie the transporting mechanism to the particular threading API. This document proposes a different approach. It describes a general purpose component, future<R>, with the following synopsis:

    template<class R> class future
    {
    public:

        future(); // creates an empty future

        bool ready() const throw(); // queries whether the future contains a value or an exception

        void wait(); // wait for ready()
        bool timed_wait( timespec const & abstime ); // as above, with timeout

        R const & operator()() const; // waits for a value, then returns it

        void set_value( R const & r ); // sets the value r and transitions to ready()

        template<class E> void set_exception() throw(); // as if stores E() and transitions to ready()
        template<class E> void set_exception( E const & e ) throw(); // stores the exception e and transitions to ready()
    };

A future is DefaultConstructible, CopyConstructible and Assignable. It has reference semantics; all copies of the same future are equivalent and interchangeable. All operations on a future except assignment are strongly thread safe or sequentially consistent; that is, the behavior of concurrent calls is as if the calls have been issued sequentially in an unspecified order.

A future can be thought of as a container for a single element, the value that is being transported. In a typical scenario, a producer (a background thread, for example) and a consumer (the caller issuing the asyunchronous call) are given a copy of the same empty future. When the producer computes its return value, it places it into the future by using set_value. When the consumer needs the result of the asynchronous computation, it uses operator() to obtain it.

In case the producer has no value to return since the computation has failed with an error, it can place an exception into the future, which will then be thrown by operator() at the general direction of the consumer.

The accessor ready() can be used to query whether a value or an exception is available. When it returns true, operator() will return immediately, without blocking. ready() is non-blocking and hence, not a cancelation point.

Two calls, wait and timed_wait, are provided that allow the consumer to block until the future is in the ready state. They are both cancelation points.

operator() will block until ready(), then it will return the stored value r after set_value( r ) has been called, or throw the stored exception E() or e after set_exception<E>() or set_exception( e ) has been called. operator() is a cancelation point unless the future is ready(). If a cancelation exception has been stored by set_exception, this can lead to an ambiguity; in this case, the caller may wish to explicitly wait() first.

set_value( r ) stores r into the future; a subsequent operator() will return r.

After set_exception<E>(), a subsequent operator() will throw E().

After set_exception( e ), a subsequent operator() will throw a copy of e, or bad_alloc, if the attempt to store e throws.

B. Task

In principle, a future is a sufficient answer to the problem of transporting values and exceptions across thread boundaries; however, one almost trivial supporting component greatly simplifies its usage:

    template< class F, class R = result_of<F()>::type > class task
    {
    public:

        task( F fn, future<R> ft ); // stores fn and ft
        void operator()() throw(); // executes fn() and places the outcome into ft
    };

task is a wrapper around the function object fn. Its operator() calls fn() and, if the call returns a value r, calls ft.set_value( r ); otherwise, it catches the exception thrown by fn() and attempts to store an accurate representation of it into ft by using ft.set_exception. The actual mapping would be left up to the implementation; given a mechanism to clone and rethrow an arbitrary std::exception, the exception could be transported intact. Unknown exceptions are transported as std::bad_exception.

C. Executor

The components presented so far in this paper allow us to build models of the Executor concept:

    class Executor
    {
    public:

        template<class Fn> future< typename result_of< Fn() >::type > execute( Fn fn );
    };

An Executor accepts a function object and gives back a future return value. Various models of the same concept can offer different execution strategies. The trivial examples of a synchronous executor and a threaded executor are given below:

    class synchronous_executor
    {
    public:

        template<class Fn> future< typename result_of<Fn()>::type > execute( Fn fn )
        {
            typedef typename result_of<Fn()>::type R;

            future<R> ft;
            task<Fn, R> fw( fn, ft );

            fw();

            return ft;
        }
    };

    class threaded_executor
    {
    public:

        template<class Fn> future< typename result_of<Fn()>::type > execute( Fn fn )
        {
            typedef typename result_of<Fn()>::type R;

            future<R> ft;
            task<Fn, R> fw( fn, ft );

            thread::create( fw );

            return ft;
        }
    };

The prototype implementation contains one additional Executor example that executes its tasks using a thread pool.

II. Future Directions (no pun intended)

Based on preliminary feedback from Howard Hinnant, Alexander Terekhov, Ion GaztaƱaga and Beman Dawes, the design is very likely to be refined in the following directions:

In addition, the following enhancements are also planned:

III. Implementation

A preliminary implementation of a slightly outdated version of the design is available at:

http://www.pdimov.com/cpp/future.cpp

IV. Proposed Text

A subsequent revision of this document will provide a proposed text for addition to the working paper or a technical report, if the proposal gathers sufficient interest from the working groups.

--end