Doc. no:  N2744=08-0254
Date:     2008-08-22
Project:  Programming Language C++
Reply to: Christopher Kohlhoff <chris@kohlhoff.com>

N2744: Comments on Asynchronous Future Value Proposal

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.

Thread Safety and shared_future<R>::get()

Problem

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.

Context

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.

Suggested Solution

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).

Promise Non-Copyability

Problem

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.

Context

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:

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.

Suggested Solution

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_futures. If promises 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.

Error Handling and Exceptions

Problem

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.

Context

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:

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.

Suggested Solution

N2671 already specifies error_codes 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_codes, the exception type for the throwing overloads may also be changed to system_error or a class derived from system_error.