Doc. no: N2744=08-0254 Date: 2008-08-22 Project: Programming Language C++ Reply to: Christopher Kohlhoff <chris@kohlhoff.com>
This paper contains some comments on the asynchronous future value library described in N2671 "An Asynchronous Future Value: Proposed Wording". Some of the comments below may have been rendered obsolete by a subsequent revision of that document.
N2671 specifies that shared_future<R>::get()
returns a
const
reference to the value stored in the asynchronous result.
This may result in non-obvious sharing of objects between threads, and has the
potential to cause race conditions in user code.
Given a class with a mutable data member:
class widget { public: ... double get_value() const { if (value_is_cached_) return value_; else { value_ = /* expensive calculation */ value_is_cached_ = true; return value_; } } ... private: ... mutable bool value_is_cached_; mutable double value_; };
Two or more threads may perform the following concurrently:
std::shared_future<widget> w = ...; double d = w.get().get_value();
If the variable w
has the same associated state in all threads,
then the program may encounter a race condition in
widget::get_value()
. Therefore the current specification of
shared_future<R>
in N2671 appears to require that
R
provide a thread safety guarantee on all const member
functions.
The programmer can, of course, prevent concurrent modification of the
mutable
data members by using a mutex
. However, it is
the author's belief that most programmers will assume a standardised mechanism
to transfer values between threads means that they can safely transfer
thread-unaware value types between threads.
Change the specification of shared_future<R>::get()
to
return by value. This would allow a type that provides no thread safety
guarantees to be safely transported between threads (assuming no shared data
between instances of the type).
The proposed wording in N2671 specifies that promise
objects are
movable but not copyable. However, difficulties arise whenever two or more
control paths may need to set the promise
. In particular, the need
to deal with exceptions can be error prone.
Consider a scenario where a programmer has been asked to implement the following function:
void async_operation(std::promise<int> p);
to meet these requirements:
promise
that is to be set once the
operation completes.
promise
's
set_exception()
, and must not allow it to propagate out.
These are not artificial requirements -- the function may in fact be just one
in a long chain of asynchronous operations. The responsibility for fulfilling
the promise
is transferred along the chain. Any given step in the
chain would therefore be decoupled from the creation of the associated
unique_future
and executing in a thread of control quite distinct
from the original point of initiation.
Now, suppose the programmer's initial (and wrong) attempt looks like this:
void perform_operation(std::promise<int> p) { ... } void async_operation(std::promise<int> p) { try { std::thread(perform_operation, std::move(p)); // Can throw system_error. } catch (...) { p.set_exception(std::current_exception()); } }
The thread
constructor might throw an exception due to a system
resource error. However, it isn't possible for the programmer to know whether
the exception will be thrown before or after the value is moved out of the
promise
object p
. (In the obvious implementation of
thread
, the exception is thrown after the
promise
has been moved.) The result is that the
set_exception
function may be called on an invalid
promise
object.
To avoid making this sort of error, the programmer must assume that once the
thread
constructor begins, any object moved to the thread is no
longer accessible. To be able to call set_exception()
on the
promise
some type of shared ownership is required:
void perform_operation(std::shared_ptr<std::promise<int>> p) { ... } void async_operation(std::promise<int> p) { std::shared_ptr<std::promise<int>> new_p; // Throws nothing. try { new_p.reset(new promise<int>); // Can throw bad_alloc. } catch (...) { p.set_exception(std::current_exception()); return; } *new_p = std::move(p); // Throws nothing. try { std::thread(perform_operation, new_p); // Can throw system_error. } catch (...) { new_p->set_exception(std::current_exception()); } }
This code illustrates that a lot of care must be taken to satisfy the
requirement that all exceptions be captured and passed to the
promise
's set_exception()
function.
On the other hand, it is likely that the need to use a shared_ptr
would exist across the entire chain of asynchronous operations, and so the
original function would instead be specified as:
void async_operation(std::shared_ptr<std::promise<int>> p);
and the creation of the shared_ptr
would only need to be performed
at the beginning of the chain, simplifying the async_operation()
implementation.
It is not clear why N2671 explicitly marks the promise
copy
constructor and copy assignment operator as deleted functions. Changing
promise
so that it is CopyConstructible and CopyAssignable (while
keeping it MoveConstructible and MoveAssignable) would not appear to place any
additional burden on implementors, as a promise
must already share
its associated state with a unique_future
or one or more
shared_future
s. If promise
s were CopyConstructible,
the async_operation
function may simply be written as follows:
void perform_operation(std::promise<int> p) { ... } void async_operation(std::promise<int> p) { try { std::thread(perform_operation, p); } catch (...) { p.set_exception(std::current_exception()); } }
Note: the use of a thread
object in this example is not intended
to imply that this problem is specific to threads. The problem can arise in any
asynchronous operation (such as socket I/O, a wait on a timer, etc.) where the
ownership of the movable but noncopyable promise
is transferred to
an asynchronous execution context. Furthermore, more complex scenarios where
parallel chains of asynchronous operations "compete" to fulfill a
promise
would have similar issues. These scenarios are also
explored in the next item.
N2671 considers all errors to be errors in program logic. However, in some
scenarios, programs may legitimately need to cater for setting a
promise
(or at least attempting to) multiple times. In these
cases, an already-satisfied promise
is an expected error.
First, let us return to the hypothetical problem above of implementing
async_operation()
, this time considering the
perform_operation()
function that performs the operation in the
thread and sets the promise
once the result is ready.
A paranoid programmer will also be concerned about the possibility of an
exception being thrown after the promise has been satisfied. Since
calling set_exception()
would throw in that case, the programmer
must write something like:
void perform_operation(std::promise<int> p) { try { ... calculate and satisfy promise ... } catch (...) { try { p.set_exception(std::current_exception()); } catch (...) { // Swallow exception. } } }
Second, consider use cases where two or more asynchronous operations are performed in parallel and "compete" to satisfy the promise. Some examples include:
In both examples, the first asynchronous operation to complete is the one that
satisfies the promise. Since either operation may complete second, the code for
both must be written to expect that calls to set_value()
may fail.
The implementation options are:
set_value()
in a
try
/catch
block and swallow any exception; or
promise
to record whether it
has been satisfied or not.
The latter option requires that the state support thread-safe access, placing
additional burden on the programmer. Clearly, the promise
itself
already maintains this knowledge in its associated state.
N2671 already specifies error_code
s for the various error
conditions. These may be used to add non-throwing overloads of the
promise
member functions set_value()
and
set_exception()
:
error_code set_value(R const & r, error_code & ec); error_code set_value(R && r, error_code & ec); error_code set_exception(exception_ptr p, error_code & ec);
These behave identically to the existing member functions, except that rather
than throwing an exception on failure, the error_code
parameter
ec
is set to reflect the error condition. The value in
ec
is returned. A program may set the promise
's value
without throwing an exception as follows:
std::error_code ec; if (p.set_value(123, ec)) { // Error occurred, take action accordingly... }
To ignore the error, the program may simply perform something like:
void perform_operation(std::promise<int> p) { try { ... } catch (...) { std::error_code ignored_ec; p.set_exception(std::current_exception(), ignored_ec); } }
For consistency across interfaces that produce error_code
s, the
exception type for the throwing overloads may also be changed to
system_error
or a class derived from system_error
.