| Document Number: | N2561=08-0071 |
| Date: | 2008-03-10 |
| Project: | Programming Language C++ |
Detlef Vollmann <dv@vollmann.ch>
Howard Hinnant <howard.hinnant@gmail.com>
Anthony Williams <anthony@justsoftwaresolutions.co.uk>
'WG21 resolves that for this revision of the C++ standard (aka "C++0x") the scope of concurrency extensions shall be constrained as follows:
The three papers that serve as base for this paper all propose
essentially the same machanism for transferring results from
one thread to the other, but differ in some minor aspects.
This paper is a joint effort to provide a proposal to implement
the "concurrency compromise".
N2094
provides some background information on the design decisions though
this proposal differs from N2094 in naming and structuring.
N2276
provides proposed wording, but this proposal again is a bit differently
structured.
This paper is not a finished proposal.
It is meant to serve as a base for discussion.
The intent of the authors is to have this discussion on the reflector
and provide a finished proposal for the pre-France mailing.
So, this paper provides hopefully enough details for people to know
what can be done with this facility, and what can't be done,
so that they can form an opinion whether they like this proposal
the way it is or whether they would like to see changes to specific
parts.
Here's a small program to demonstrate how a task is distributed to a number of detached threads and how the results can be collected without explicit locking.
// simple example on usage of future
#include <thread>
#include <future>
struct MBLine
{
// ...
};
void displayLine(MBLine const &);
struct ComputeMBLine
{
ComputeMBLine(double y_, double x1_, double x2_)
: y(y_), x1(x1_), x2(x2_)
{}
MBLine operator()()
{
MBLine myLine;
// compute the line
// ...
return myLine;
}
double y, x1, x2;
};
int main()
{
std::unique_future<MBLine> set[400];
double y = 2.0;
for (int i = 0; i != 400; ++i, y -= 0.01)
{
std::packaged_task<MBLine> t(ComputeMBLine(y, -2.0, 2.0));
set[i] = t.get_future();
std::thread(std::move(t)).detach();
}
bool done;
do
{
done = true;
for (int i = 0; i != 400; ++i)
{
MBLine l;
if (!set[i].was_moved())
{
if (set[i].try_move(l) == std::unique_future<MBLine>::ready)
{
displayLine(l);
} else {
done = false;
}
}
}
// do some other work ...
} while (!done);
return 0;
}
This example uses a packaged_task to help to associate a future
with a thread. Another option would have been to use a promise
directly as a parameter to the ctor of ComputeMBLine.
This paper proposes a kind of return buffer that takes a value (or an exception) in one (sub-)thread and provides the value in another (controlling) thread. This buffer provides essentially two interfaces:
promise and
unique_future and
shared_future.
While a unique_future provides move semantics where
the value (or exception) can be retrieved only once,
the shared_future provides copy semantics where
the value can be retrieved arbitrarily often.
A typical procedure for working with promises and futures looks like:
Also proposed is a packaged_task that wraps one
callable object and provides another one that can be started
in its own thread and assigns the return
value (or exception) to a return buffer that can be accessed
through one of the future classes.
With a packaged_task a typical procedure looks like:
namespace std
{
class future_unitialized : public logic_error { };
class future_moved : public logic_error { };
template <typename R> class unique_future;
template <> class unique_future<void>;
template <typename R> class shared_future;
template <> class shared_future<void>;
template <typename R> class promise;
template <> class promise<void>;
template <typename R> class promise<R&>;
template <typename R> class packaged_task;
}
unique_future
template <typename R>
class unique_future
{
public:
enum state { uninitialized, waiting, ready, moved };
unique_future();
unique_future(unique_future &&);
unique_future(unique_future const & rhs) = delete;
~unique_future();
unique_future& operator=(unique_future &&);
unique_future& operator=(unique_future const & rhs) = delete;
// retrieving the value
R move();
bool try_move(R &);
bool timed_move(R &, some_rel_time rel_time);
bool timed_move_until(R &, some_abs_time abs_time);
// functions to check state, and wait for ready
state get_state() const;
bool is_ready() const;
bool has_exception() const;
bool has_value() const;
bool was_moved() const;
void wait() const;
bool timed_wait(some_rel_time rel_time) const;
bool timed_wait_until(some_abs_time abs_time) const;
};
unique_future();
Effects:
constructs a unique_future whose associated state holds
no value, no exception and a null thread handle.
Postconditions:
get_state() == uninitialized.
unique_future(const unique_future&& rhs);
Effects:
MoveConstructs a unique_future whose associated state is the same
as the state of rhs before.
Postconditions:
rhs.get_state() == uninitialized.
~unique_future();
Effects:
destroys *this and its associated state if no other instance refers to it.
unique_future & operator=(const unique_future&& rhs);
Effects:
MoveAssigns rhs to *this whose associated state
is the same as the state of rhs before.
Returns:
*this.
Postconditions:
rhs.get_state() == uninitialized.
R move();
Effects:
Retrieves the value stored in the associated promise or
the return value of the callable object given to the constructor
of the associated packaged_task.
Sychronization:
If *this is associated with a promise,
the completion of set_value() or set_exception()
to that promise
happens before ([intro.multithread])
move() returns.
If *this is associated with a packaged_task,
the completion of the call to the operator()()
happens before ([intro.multithread]).
move() returns.
Returns: the moved value.
Postconditions:
get_state() == moved if
get_state() == waiting on entry.
Throws:
future_unitialized if
get_state() == uninitialized on entry.
future_moved if
get_state() == moved on entry.
bool try_move(R & val);
Effects:
If get_state() is ready on entry retrieves
the value stored in the associated promise and moves it
into val.
Returns:
the value of get_state() on entry.
Postconditions:
get_state() == moved if a value was moved
or a stored exception was propagated,
get_state() retains its value upon entry otherwise.
Throws: the stored exception, if an exception was stored and not retrieved before.
bool timed_move(R & val, some_rel_time rel_time);
Effects:
Blocks until *this is ready
or until rel_time elapsed.
If the future gets ready before the waiting time elapsed,
the value stored in the associated promise is moved
into val.
Returns:
unitialized or moved if get_state() was
uninitialized or moved on entry, respectively,
waiting if the waiting time elapses before the future
becomes ready,
ready if a value was moved
Postconditions:
get_state() == moved if a value was moved,
or a stored exception was propagated,
get_state() retains its value upon entry otherwise.
Throws: the stored exception, if an exception was stored and not retrieved before.
bool timed_move_until(R & val, some_abs_time abs_time);
Same as timed_move, except that it blocks until
abs_time is reached if the future is not ready.
state get_state() const;
Returns:
uninitialized if the future was not yet initialized.
waiting if the future was initialized, but there wasn't
yet assigned a value or exception to the associated promise,
ready if the associated promise was assigned
a value or exception that wasn't yet moved from the future.
moved if a value was moved
or a stored exception was propagated out of the future.
bool is_ready() const;
Returns:
get_state() == ready.
bool has_exception() const;
Returns:
true if get_state() == ready and the
promise or packaged_task
associated with the future contains an exception,
false otherwise.
bool has_value() const;
Returns:
true if get_state() == ready and the
promise or packaged_task
associated with the future contains a value,
false otherwise.
bool was_moved() const;
Returns:
get_state() == moved.
void wait() const;
Sychronization:
If *this is associated with a promise,
the completion of set_value() or set_exception()
to that promise
happens before ([intro.multithread])
move() returns.
If *this is associated with a packaged_task,
the completion of the call to the operator()()
happens before ([intro.multithread]).
move() returns.
Postconditions:
get_state() == ready.
Throws:
future_unitialized if
get_state() == uninitialized on entry.
future_moved if
get_state() == moved on entry.
bool timed_wait(some_rel_time rel_time) const;
Effects:
Blocks until *this is ready
or until rel_time elapsed.
Returns:
unitialized or moved if get_state() was
uninitialized or moved on entry, respectively,
waiting if the waiting time elapses before the future
becomes ready,
ready otherwise.
Postconditions:
get_state() equals the return value.
bool timed_wait_until(some_abs_time abs_time);
Same as timed_wait, except that it blocks until
abs_time is reached if the future is not ready.
unique_future<void>
template <>
class unique_future<void>
{
public:
enum state { uninitialized, waiting, ready, moved };
unique_future();
unique_future(unique_future &&);
unique_future(unique_future const & rhs) = delete;
~unique_future();
unique_future& operator=(unique_future &&);
unique_future& operator=(unique_future const & rhs) = delete;
void move();
bool try_move(void);
bool timed_move(some_rel_time rel_time);
bool timed_move_until(some_abs_time abs_time);
// functions to check state, and wait for ready
state get_state() const;
// ???: do these functions throw if the future is not yet initialized?
bool is_ready() const;
bool has_exception() const;
bool has_value() const;
bool was_moved() const;
void wait() const;
bool timed_wait(some_rel_time rel_time) const;
bool timed_wait_until(some_abs_time abs_time) const;
};
shared_future
template <typename R>
class shared_future
{
public:
shared_future();
shared_future(shared_future const & rhs);
shared_future(unique_future<R> &&);
shared_future(const unique_future<R> &) = delete;
~shared_future();
shared_future & operator=(shared_future const & rhs);
// retrieving the value
operator R const & () const;
R const & get() const;
bool try_get(R &) const;
bool timed_get(R &, some_rel_time rel_time) const;
bool timed_get_until(R &, some_abs_time abs_time) const;
// functions to check state, and wait for ready
bool is_ready() const;
bool has_exception() const;
bool has_value() const;
void wait() const;
bool timed_wait(some_rel_time rel_time) const;
bool timed_wait_until(some_abs_time abs_time) const;
};
shared_future<void>
template <>
class shared_future<void>
{
public:
shared_future();
shared_future(shared_future const & rhs);
shared_future(unique_future<R> &&);
shared_future(const unique_future<R> &) = delete;
~shared_future();
shared_future & operator=(shared_future const & rhs);
// retrieving the value
void get() const;
bool try_get() const;
bool timed_get(some_rel_time rel_time) const;
bool timed_get_until(some_abs_time abs_time) const;
// functions to check state, and wait for ready
bool is_ready() const;
bool has_exception() const;
bool has_value() const;
void wait() const;
bool timed_wait(some_rel_time rel_time) const;
bool timed_wait_until(some_abs_time abs_time) const;
};
promise
template <typename R>
class promise
{
public:
template <class Allocator> explicit promise(Allocator a);
promise();
promise(promise && rhs);
promise(promise const & rhs) = delete;
~promise();
// Assignment
promise & operator=(promise && rhs);
promise & operator=(promise const & rhs) = delete;
void swap(promise& other);
// Result retrieval
unique_future<R> get_future();
void set_value(R const & r);
void set_value(R && r);
void set_exception(exception_ptr p);
};
promise<void>
template <>
class promise<void>
{
public:
template <class Allocator> explicit promise(Allocator a);
promise();
promise(promise && rhs);
promise(promise const & rhs) = delete;
~promise();
// Assignment
promise & operator=(promise && rhs);
promise & operator=(promise const & rhs) = delete;
void swap(promise& other);
// Result retrieval
unique_future<void> get_future();
void set_value();
void set_exception(exception_ptr p);
};
promise<R &>
template <typename R>
class promise<R &>
{
public:
template <class Allocator> explicit promise(Allocator a);
promise();
promise(promise && rhs);
promise(promise const & rhs) = delete;
~promise();
// Assignment
promise & operator=(promise && rhs);
promise & operator=(promise const & rhs) = delete;
void swap(promise& other);
// Result retrieval
unique_future<R&> get_future();
void set_value(R& r);
void set_exception(exception_ptr p);
};
packaged_task
template<typename R>
class packaged_task
{
public:
// construction and destruction
template <class F>
explicit packaged_task(F const& f);
template <class F, class Allocator>
explicit packaged_task(F const& f, Allocator a);
template <class F>
explicit packaged_task(F&& f);
template <class F, class Allocator>
explicit packaged_task(F&& f, Allocator a);
packaged_task(packaged_task&& other);
packaged_task(packaged_task&) = delete;
~packaged_task();
// assignment
packaged_task& operator=(packaged_task&& other);
packaged_task& operator=(packaged_task&) = delete;
void swap(packaged_task& other);
// result retrieval
unique_future<R> get_future();
// execution
void operator()();
};