1. Background
C++11 introduced async() along with several other basic concurrency primitives. Many regard async() as fundamentally broken [n3637] and in need of deprecation [n3777]. Following much discussion and a passionate plea [n3780], the effort to deprecate async() prior to an executors-based replacement was abandoned. The guidance from SG1 to the authors of the various Executors proposals, since at least the 2014 Urbana-Champaign SG1 discussion of [n4242] (see: [SG1-Mins-n4242]), has been that any Executors proposal must clearly address the deficiencies of async().
At the Fall 2017 Albuquerque meeting, a companion paper [p0737] to the Executors proposal was presented that more fully defines the notion of an execution_context. The paper includes a proposal for a formal definition of an "executor for async()" and its supporting library machinery. This offers an evolutionary path away from the undesirable, but possibly widely used at this point, semantics of the current async().
At the Winter 2018 Jacksonville meeting, there was clear direction to the authors of the Executors proposal [p0443] to provide library usage examples (see: [LEWG-Mins-p0443]). This paper intends to address this request by drawing on [p0737] as it specifically relates to async().
The current wording on the behavior of std::async
is as follows -
— If launch::async is set in policy, calls
INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)
(23.14.3, 33.3.2.2) as if in a new thread of execution represented by a thread object with the calls toDECAY_COPY
being evaluated in the thread that calledasync
. Any return value is stored as the result in the shared state. Any exception propagated from the execution ofINVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)
is stored as the exceptional result in the shared state. The thread object is stored in the shared state and affects the behavior of any asynchronous return objects that reference that state.— If launch::deferred is set in policy, stores
DECAY_COPY(std::forward<F>(f)) and DECAY_COPY(std::forward<Args>(args))...)
in the shared state. These copies of f and args constitute a deferred function. Invocation of the deferred function evaluatesINVOKE(std::move(g), std::move(xyz))
where g is the stored value ofDECAY_COPY(std::forward<F>(f))
and xyz is the stored copy ofDECAY_COPY(std::forward<Args>(args))...)
. Any return value is stored as the result in the shared state. Any exception propagated from the execution of the deferred function is stored as the exceptional result in the shared state. The shared state is not made ready until the function has completed. The first call to a non-timed waiting function (33.6.5) on an asynchronous return object referring to this shared state shall invoke the deferred function in the thread that called the waiting function. Once evaluation ofINVOKE(std::move(g), std::move(xyz))
begins, the function is no longer considered deferred. [ Note: If this policy is specified together with other policies, such as when using a policy value of launch::async | launch::deferred, implementations should defer invocation or the selection of the policy when no more concurrency can be effectively exploited. — end note ]— If no value is set in the launch policy, or a value is set that is neither specified in this document nor by the implementation, the behavior is undefined.
The language run-time currently provides hidden executor types which fulfills the requirements of
these launch policies. In particular, a launch policy of launch::async
requires that the run-time
provide a new single thread executor to evaluate the supplied Callable. The policy for launch::deferred
most resembles the concept of an inline executor, which only evaluates the supplied Callable when the
returned std::future
is awaited.
[p0737] seeks the formal introduction of an execution context concept, which [p0443] currently leaves unspecified. [p0737] defines the ExecutionContext as follows -
A concurrency and parallelism execution context manages a set of execution agents on a set of execution resources of a given execution architecture.These execution agents execute work, implemented by a callable, that is submitted to the execution context by an executor.
One or more types of executors may submit work to the same execution context. Work submitted to an execution context is incomplete until (1) it is invoked and exits execution by return or exception or (2) its submission for execution is canceled.
Note: The execution context terminology used here and in the Networking TS [n4656] deviate from the traditional context of execution usage that refers to the state of a single executing callable; e.g., program counter, registers, stack frame.
This ExecutionContext concept is specified as follows -
class ExecutionContext /* exposition only */ { public: template <typename ExecutionContextProperty> /* exposition only */ detail::query_t< ExecutionContext , ExecutionContextProperty > query(ExecutionContextProperty p) const ; ~ExecutionContext(); // Not copyable or moveable ExecutionContext( ExecutionContext const & ) = delete ; ExecutionContext( ExecutionContext && ) = delete ; ExecutionContext & operator = ( ExecutionContext const & ) = delete ; ExecutionContext & operator = ( ExecutionContext && ) = delete ; // Execution resource using execution_resource_t = /* implementation defined */ ; execution_resource_t const & execution_resource() const noexcept ; // Executor generator template< class ... ExecutorProperties > /* exposition only */ detail::executor_t< ExecutionContext , ExecutorProperties... > executor( ExecutorProperties... ); // Waiting functions: void wait(); template< class Clock , class Duration > bool wait_until( chrono::time_point<Clock,Duration> const & ); template< class Rep , class Period > bool wait_for( chrono::duration<Rep,Period> const & ); }; bool operator == ( ExecutionContext const & , ExecutionContext const & ); bool operator != ( ExecutionContext const & , ExecutionContext const & ); // Execution context properties: constexpr struct reject_on_destruction_t {} reject_on_destruction; constexpr struct abandon_on_destruction_t {} abandon_on_destruction; constexpr struct abort_on_destruction_t {} abort_on_destruction; constexpr struct wait_on_destruction_t {} wait_on_destruction;
2. An Execution Context for async()
[p0737] Proposes that there exists in the underlying runtime, a hidden execution context, into which 33.6.9 Function template async launches asynchronous work, and proposes that this
execution context be exposed along with the hidden executors fulfilling std::async
launch policies.
Exposing this execution context and these executor types enables current use of std::async
to be
transliterated to the executor model as follows.
// Equivalent without- and with-executor async statements without launch policy auto f = std::async( []{ std::cout << "anonymous way\n"} ); auto f = std::async( std::async_execution_context.executor() , []{ std::cout << "executor way\n"} ); // Equivalent without- and with-executor async statements with launch policy auto f = std::async( std::launch::deferred , []{ std::cout << "anonymous way\n"} ); auto f = std::async( std::async_execution_context.executor( std::launch::deferred ) , []{ std::cout << "executor way\n"} );
Asynchronous execution behavior is unchanged following such a well-defined transliteration.
The use of std::async
is now prepared, at the developers' discretion, to replace the leading executor
argument with a different executor.
3. Proposed wording for Standard Async Execution Context and Executor
The proposed wording is extracted from [p0737] with modifications for a minimal proposal and to align with executors proposal [p0443].
namespace std { namespace experimental { inline namespace executors_v1 { namespace execution { namespace launch { struct async_t { static constexpr bool is_requirable = true; static constexpr bool is_preferable = true; }; struct deferred_t { static constexpr bool is_requirable = true; static constexpr bool is_preferable = true; }; constexpr async_t async; // executor property constexpr deferred_t deferred; // executor property } // namespace launch class async_executor_t ; // implementation defined class async_execution_context_t { public: // Not copyable or moveable async_execution_context_t( async_execution_context_t const & ) = delete ; async_execution_context_t( async_execution_context_t && ) = delete ; async_execution_context_t & operator = ( async_execution_context_t const & ) = delete ; async_execution_context_t & operator = ( async_execution_context_t && ) = delete ; // Executor generator template<class ... ExecutorProperties> /* exposition only */ detail::executor_t< ExecutionContext , ExecutorProperties... > executor( ExecutorProperties ... p ); // Execution resource - deferred // Waiting - deferred }; // Execution context into which std::async launches work extern async_execution_context_t async_execution_context; } // namespace execution } // inline namespace executors_v1 } // namespace experimental } // namespace std namespace std { template<class Function , class ... Args> future<std::result_of<std::decay_t<Function>(std::decay_t<Args>...)>> async( std::experimental::execution::async_executor_t exec , Function && f , Args && ... args); } // namespace std
experimental::execute::launch::async; experimental::execute::launch::deferred;
Executor properties isomorphic with std::launch
launch policies of the same name.
class async_executor_t;
An implementation-defined type satisfying TwoWayExecutor
requirements [p0443].
class async_execution_context_t;
An implementation-defined type partially satisfying execution context requirements [p0737]. Specification of execution context properties, construction, destruction, and wait functionality is deferred.
template< class ... ExecutorProperties > /* exposition only */ detail::executor_t< ExecutionContext , ExecutorProperties... > async_execution_context_t::executor( ExecutorProperties ... p );
Returns:
A executor with *this
execution context and execution properties p
.
If p
is empty, is std::experimental::execution::launch::async
, or is std::experimental::execution::launch::deferred
the executor type is async_executor_t
.
extern async_execution_context_t async_execution_context;
Global execution context object enabling the equivalent invocation of callables
through the with-async_executor_t std::async
and without-executor std::async
.
Guaranteed to be initialized during or before the first use.
template< class Function , class ... Args > future<std::result_of<std::decay_t<Function>(std::decay_t<Args>...)>> async( async_executor_t exec , Function && f , Args && ... args );
Effects:
If exec
has an std::experimental::execution::launch
property
then equivalent to invoking std::async(
policy , f , args... );
with policy of the same name;
otherwise equivalent to invoking std::async( f , args... );
Equivalency is symmetric with respect to the current, non-executor std::async
functions.
4. Transliteration of async()
With this proposal, transliteration of current std::async
usage to the executor model is as follows.
// Equivalent without- and with-executor async statements without launch policy auto f = std::async( []{ std::cout << "anonymous way\n"} ); auto f = std::async( std::experimental::execution::async_execution_context.executor() , []{ std::cout << "executor way\n"} ); // Equivalent without- and with-executor async statements with launch policy auto f = std::async( std::launch::deferred , []{ std::cout << "anonymous way\n"} ); auto f = std::async( std::experimental::execution::async_execution_context.executor( std::experimental::execution::launch::deferred ) , []{ std::cout << "executor way\n"} );
5. Open Questions
-
[p0737] does not propose a mechanism to migrate the executor aware
std::async
template fromstd::experimental::execute::async_executor_t
to other executors types. -
33.6.2 currently specifies
std::launch::async
andstd::launch::deferred
as:enum class launch : unspecified { async = unspecified , deferred = unspecified , implementation-defined };
33.6.2 further constrains
std::launch
to be a bitmask with the following note - [ Note: Implementations can provide bitmasks to specify restrictions on task interaction by functions launched by async() applicable to a corresponding subset of available launch policies. Implementations can extend the behavior of the first overload of async() by adding their extensions to the launch policy under the “as if” rule. -- end note]In order to preserve backward compatibility with existing usage of
std::launch
this would imply that thestd::experimental::execute::launch
property types would also need to behave as-if they were a bitmask. This implies thatstd::experimental::execution::launch::async | std::experimental::execution::launch::deferred
return another viable property type that represents the union of these two launch properties. Another option is for [p0443] to be revised to allow executor properties to be implemented asenum class
orconstexpr
values potentially of the same type. -
[n4406] and [p0761] suggest a
.on()
member taking an executor to allow parallel algorithm launch policies to compose with executors. Do the policies ofstd::launch
differ enough, conceptually, from the launch policies specified in 23.19.2 to warrant a different mechanism for composition with executors?