Document Number: | |
---|---|
Date: | |
Revises: | |
Editor: | Microsoft Corp. |
Note: this is an early draft. It’s known to be incomplet and incorrekt, and it has lots of bad formatting.
This technical specification describes a number of concurrency extensions to
the C++ Standard Library (
This technical specification is non-normative. Some of the library components in this technical specification may be considered for standardization in a future version of C++, but they are not currently part of any C++ standard. Some of the components in this technical specification may never be standardized, and others may be standardized in a substantially changed form.
The goal of this technical `specification` is to build more widespread existing practice for an expanded C++ standard library. It gives advice on extensions to those vendors who wish to provide them.
The following referenced document is indispensable for the application of this document. For dated references, only the edition cited applies. For undated references, the latest edition of the referenced document (including any amendments) applies.
ISO/IEC 14882:— is herein called the C++ Standard. References to clauses within the C++ Standard are written as "C++14 §3.2". The library described in ISO/IEC 14882:— clauses 17–30 is herein called the C++ Standard Library.
Some of the extensions described in this Technical Specification represent
types and functions that are currently not part of the C++ Standards Library,
and because these extensions are experimental, they should not be declared
directly within namespace std
. Instead, such extensions are
declared in namespace std::experimental
.
std
.
— end note ]
Unless otherwise specified, references to such entities described in this
Technical Specification are assumed to be qualified with
std::experimental
, and references to entities described in the C++
Standard Library are assumed to be qualified with std::
.
This proposal includes two abstract base classes, executor
and
scheduled_executor
(the latter of which inherits from the former); several
concrete classes that inherit from executor
or scheduled_executor
; and
several utility functions.
Subclause | Header(s) |
---|---|
[executors.base] | <executor> |
[executors.classes] | |
[executors.classes.thread_pool] | <thread_pool> |
[executors.classes.serial] | <serial_executor> |
[executors.classes.loop] | <loop_executor> |
[executors.classes.inline] | <inline_executor> |
[executors.classes.thread] | <thread_per_task_executor> |
The <executor>
header defines abstract base classes for executors, as well as
non-member functions that operate at the level of those abstract base classes.
<experimental/executor>
synopsisnamespace std { namespace experimental { inline namespace concurrency_v1 { class executor; class scheduled_executor; } // namespace concurrency_v1 } // namespace experimental } // namespace std
executor
Class executor
is an abstract base class defining an abstract interface of
objects that are capable of scheduling and coordinating work submitted by
clients. Work units submitted to an executor may be executed in one or more
separate threads. Implementations are required to avoid data races when work
units are submitted concurrently.
All closures are defined to execute on some thread, but which thread is largely
unspecified. As such accessing a thread_local
variable is defined behavior,
though it is unspecified which thread's thread_local
will be accessed.
The initiation of a work unit is not necessarily ordered with respect to other initiations.
serial_executor
wrapper.
— end note ]
There is no defined ordering of the execution or completion of closures added to the executor.
class executor { public: virtual ~executor(); virtual void add(function<void()> closure) =0; virtual size_t uninitiated_task_count() const =0; };
executor::~executor()
void executor::add(std::function<void> closure);
add
cannot complete
(due to shutdown or other conditions)size_t executor::uninitiated_task_count();
scheduled_executor
Class scheduled_executor
is an abstract base class that extends the executor
interface by allowing clients to pass in work items that will be executed some
time in the future.
class scheduled_executor : public executor { public: virtual void add_at(const chrono::system_clock::time_point& abs_time, function<void()> closure) = 0; virtual void add_after(const chrono::steady_clock::duration& rel_time, function<void()> closure) = 0; };
void add_at(const chrono::system_clock::time_point& abs_time, function<void()> closure);
abs_time
.void add_after(const chrono::steady_clock::duration& rel_time, function<void()> closure);
rel_time
from now.thread_pool
<experimental/thread_pool>
synopsisnamespace std { namespace experimental { inline namespace concurrency_v1 { class thread_pool; } // namespace concurrency_v1 } // namespace experimental } // namespace std
Class thread_pool
is a simple thread pool class that creates a fixed number of
threads in its constructor and that multiplexes closures onto them.
class thread_pool : public scheduled_executor { public: explicit thread_pool(int num_threads); ~thread_pool(); // [executor methods omitted] };
thread_pool::thread_pool(int num_threads)
num_threads
threads.system_error
if the threads can't be created and started.resource_unavailable_try_again
— the system lacked
the necessary resources to create another thread, or the system-imposed limit on
the number of threads in a process would be exceeded.
thread_pool::~thread_pool()
serial_executor
<experimental/serial_executor>
synopsisnamespace std { namespace experimental { inline namespace concurrency_v1 { class serial_executor; } // namespace concurrency_v1 } // namespace experimental } // namespace std
Class serial_executor
is an adaptor that runs its closures by scheduling them
on another (not necessarily single-threaded) executor. It runs added closures
inside a series of closures added to an underlying executor in such a way so
that the closures execute serially. For any two closures c1
and c2
added to
a serial_executor
e
, either the completion of c1
happens before
(as per c2
begins, or vice versa.
If e.add(c1)
happens before e.add(c2)
, then c1
is
executed before c2
.
class serial_executor : public executor { public explicit serial_executor(executor& underlying_executor); virtual ~serial_executor(); executor& underlying_executor(); // [executor methods omitted] };
serial_executor::serial_executor(executor& underlying_executor)
serial_executor
that executes closures in FIFO order by
passing them to underlying_executor
.
serial_executor
objects may share a single underlying executor. serial_executor::~serial_executor()
If a serial_executor
is destroyed inside a closure running on
that serial_executor
object, the behavior is undefined.
executor& serial_executor::underlying_executor()
loop_executor
<experimental/loop_executor>
synopsisnamespace std { namespace experimental { inline namespace concurrency_v1 { class loop_executor; } // namespace concurrency_v1 } // namespace experimental } // namespace std
Class loop_executor
is a single-threaded executor that executes closures by
taking control of a host thread. Closures are executed via one of three closure-
executing methods: loop()
, run_queued_closures()
, and
try_run_one_closure()
. Closures are executed in FIFO order. Closure-executing
methods may not be called concurrently with each other, but may be called
concurrently with other member functions.
class loop_executor : public executor { public: loop_executor(); virtual ~loop_executor(); void loop(); void run_queued_closures(); bool try_run_one_closure(); void make_loop_exit(); // [executor methods omitted] };
loop_executor::loop_executor()
loop_executor
object. Does not spawn any threads. loop_executor::~loop_executor()
loop_executor
object. Any closures that haven't been
executed by a closure-executing method when the destructor runs will never be
executed.void loop_executor::loop()
make_loop_exit()
is called.void loop_executor::run_queued_closures()
make_loop_exit()
is called. Does not execute any additional closures that
have been added after this function is called. Invoking make_loop_exit()
from
within a closure run by run_queued_closures()
does not affect the behavior of
subsequent closure-executing methods.void run_queued_closures() { add([](){make_loop_exit();}); loop(); }because that would cause early exit from a subsequent invocation of
loop()
.
— end note ]
bool loop_executor::try_run_one_closure()
true
if a closure was run, otherwise false
.void loop_executor::make_loop_exit()
loop()
or run_queued_closures()
to finish executing
closures and return as soon as the current closure has finished. There is no
effect if loop()
or run_queued_closures()
isn't currently executing.
make_loop_exit()
is typically called from a closure. After a closure-
executing method has returned, it is legal to call another closure-executing
function.
inline_executor
<experimental/inline_executor>
synopsisnamespace std { namespace experimental { inline namespace concurrency_v1 { class inline_executor; } // namespace concurrency_v1 } // namespace experimental } // namespace std
Class inline_executor
is a simple executor which intrinsically only provides the add()
interface as it provides no queuing and instead immediately executes work on the calling thread.
This is effectively an adapter over the executor interface but keeps everything on the caller's
context.
class inline_executor : public executor { public explicit inline_executor(); // [executor methods omitted] };
inline_executor::inline_executor()
add()
call by immediately executing the provided function in the caller's thread.
thread_per_task_executor
<experimental/thread_per_task_executor>
synopsisnamespace std { namespace experimental { inline namespace concurrency_v1 { class thread_per_task_executor; } // namespace concurrency_v1 } // namespace experimental } // namespace std
Class thread_per_task_executor
is a simple executor that executes each task (closure)
on its own std::thread
instance.
class thread_per_task_executor : public executor { public: explicit thread_per_task_executor(); ~thread_per_task_executor(); // [executor methods omitted] };
thread_per_task_executor::thread_per_task_executor()
thread_per_task_executor::~thread_per_task_executor()
std::future<T>
and Related APIs
The extensions proposed here are an evolution of the functionality of
std::future
and std::shared_future
. The extensions enable wait free
composition of asynchronous operations.
future
To the class declaration found in
bool is_ready() const; future(future<future<R>>&& rhs) noexcept; template<typename F> see below then(F&& func); template<typename F> see below then(executor &ex, F&& func); template<typename F> see below then(launch policy, F&& func);
In
future(future<future<R>>&& rhs) noexcept;
future
object by moving the instance referred to by
rhs
and unwrapping the inner future.valid()
returns the same value as rhs.valid()
prior to the
constructor invocation.rhs.valid() == false
.
After
template<typename F>
see below then(F&& func);
template<typename F>
see below then(executor &ex, F&& func);
template<typename F>
see below then(launch policy, F&& func);
future
object as a parameter. The
second function takes an executor
as the first parameter and a callable object
as the second parameter. The third function takes a launch policy as the first
parameter and a callable object as the second parameter.
INVOKE(DECAY_COPY (std::forward<F>(func)))
is called when the object's shared state is ready (has a value or exception stored).executor
.executor
or launch policy is not provided the continuation inherits
the parent's launch policy or executor
.future
. Any exception propagated from the execution of
the continuation is stored as the exceptional result in the shared state of the resulting future
.
std::promise
or with a packaged_task
(has
no associated launch policy), the continuation behaves the same as the third
overload with a policy argument of launch::async | launch::deferred
and the
same argument for func
.launch::deferred
, then it is filled by
calling wait()
or get()
on the resulting future
.
auto f1 = async(launch::deferred, [] { return 1; }); auto f2 = f1.then([](future— end example ]n) { return 2; }); f2.wait(); // execution of f1 starts here, followed by f2
then
depends on the return type of the closure
func
as defined below:
result_of_t<decay_t<F>()>
is future<R>
, the function returns future<R>
.
future<result_of_t<decay_t<F>()>>
.
then
taking a closure returning a
future<R>
would have been future<future<R>>
.
This rule avoids such nested future
objects.
f2
below is
future<int>
and not future<future<int>>
:
future<int> f1 = g(); future<int> f2 = f1.then([](future<int> f) { future<int> f3 = h(); return f3; });— end example ]
future
object is moved to the parameter of the continuation function.valid() == false
on original future
object immediately after it returns.bool is_ready() const;
true
if the shared state is ready, false
if it isn't.shared_future
bool is_ready() const; template<typename F> see below then(F&& func); template<typename F> see below then(executor &ex, F&& func); template<typename F> see below then(launch policy, F&& func);
template<typename F>
see below shared_future::then(F&& func);
template<typename F>
see below shared_future::then(executor &ex, F&& func);
template<typename F>
see below shared_future::then(launch policy, F&& func);
shared_future
object as a
parameter. The second function takes an executor
as the first parameter and a
callable object as the second parameter. The third function takes a launch
policy as the first parameter and a callable object as the second parameter.
INVOKE(DECAY_COPY (std::forward<F>(func)))
is called when the object's shared state is ready (has a value or exception stored).future
. Any exception propagated from the execution of
the continuation is stored as the exceptional result in the shared state of the resulting future
.
std::promise
(has no associated launch
policy), the continuation behaves the same as the third function with a policy
argument of launch::async | launch::deferred
and the same argument for func
.launch::deferred
, then it is filled by
calling wait()
or get()
on the resulting shared_future
.
future
. See example in then
depends on the return type of the closure
func
as defined below:
result_of_t<decay_t<F>()>
is future<R>
, the function returns future<R>
.
future<result_of_t<decay_t<F>()>>
.
future
. See the notes on future::then
return type in shared_future
passed to the continuation function is
a copy of the original shared_future
.
valid() == true
on the original shared_future
object.
bool is_ready() const;
true
if the shared state is ready, false
if it isn't.when_all
A new section 30.6.10 shall be inserted at the end of
template
see below when_all(InputIterator first, InputIterator last);
template <typename... T>
see below when_all(T&&... futures);
T
is of type future<R>
or
shared_future<R>
.when_all
. The first version takes a pair of
InputIterators
. The second takes any arbitrary number of future<R0>
and
shared_future<R1>
objects, where R0
and R1
need not be the same type.when_all
where InputIterator
first
equals last, returns a future with an empty vector that is immediately
ready.when_any
with no arguments returns a
future<tuple<>>
that is immediately ready.future
and shared_future
is waited upon and then copied into the
collection of the output (returned) future, maintaining the order of the
futures in the input collection.when_all
will not throw an exception, but the
futures held in the output collection may.future<tuple<>>
if when_all
is called with zero arguments.future<vector<future<R>>>
if the input cardinality is unknown at compile
and the iterator pair yields future<R>
. R
may be void
. The order of the
futures in the output vector will be the same as given by the input iterator.future<vector<shared_future<R>>>
if the input cardinality is unknown at
compile time and the iterator pair yields shared_future<R>
. R
may be
void
. The order of the futures in the output vector will be the same as given
by the input iterator.future<tuple<future<R0>, future<R1>, future<R2>...>>
if inputs are fixed in
number. The inputs can be any arbitrary number of future
and shared_future
objects. The type of the element at each position of the tuple corresponds to
the type of the argument at the same position. Any of R0
, R1
, R2
, etc.
may be void
.future<T>
s valid() == false
.shared_future<T>
valid() == true
.when_any
A new section 30.6.11 shall be inserted at the end of
template <class InputIterator>
see below when_any(InputIterator first, InputIterator last);
template <typename... T>
see below when_any(T&&... futures);
T
is of type future<R>
or shared_future<R>
.when_any
. The first version takes a pair of
InputIterators
. The second takes any arbitrary number of future<R>
and
shared_future<R>
objects, where R
need not be the same type.when_any
where InputIterator
first
equals last, returns a future with an empty vector that is immediately
ready.when_any
with no arguments returns a
future<tuple<>>
that is immediately ready.future
and shared_future
is waited upon. When at least one is ready,
all the futures are copied into the collection of the output (returned) future,
maintaining the order of the futures in the input collection.future
returned by when_any
will not throw an exception, but the
futures held in the output collection may.future<tuple<>>
if when_any
is called with zero arguments. future<vector<future<R>>>
if the input cardinality is unknown at compile
time and the iterator pair yields future<R>
. R
may be void. The order of
the futures in the output vector will be the same as given by the input
iterator.future<vector<shared_future<R>>>
if the input cardinality is unknown at
compile time and the iterator pair yields shared_future<R>
. R
may be
void
. The order of the futures in the output vector will be the same as given
by the input iterator.future<tuple<future<R0>, future<R1>, future<R2>...>>
if inputs are fixed in
number. The inputs can be any arbitrary number of future
and shared_future
objects. The type of the element at each position of the tuple corresponds to
the type of the argument at the same position. Any of R0
, R1
, R2
, etc.
maybe void
.future<T>
s valid() == false
.shared_future<T> valid() == true
.when_any_back
A new section 30.6.12 shall be inserted at the end of
template <class InputIterator>
see below when_any_back(InputIterator first, InputIterator last);
InputIterator
's value type shall be convertible to future<R>
or shared_future<R>
. All R
types must be the same.
when_any_back
takes a pair of InputIterators
.when_any_back
where InputIterator
first equals
last, returns a future
with an empty vector that is immediately ready.future
and shared_future
is waited upon. When at least one is ready,
all the futures are copied into the collection of the output (returned)
future
.future
or shared_future
that was first detected as
being ready swaps its position with that of the last element of the result
collection, so that the ready future
or shared_future
may be identified in
constant time. Only one future
or shared_future
is thus moved.future
returned by when_any_back
will not throw an exception, but
the futures held in the output collection may.future<vector<future<R>>>
if the input cardinality is unknown at compile
time and the iterator pair yields future<R>
. R
may be void
.future<vector<shared_future<R>>>
if the input cardinality is unknown at
compile time and the iterator pair yields shared_future<R>
. R
may be
void
.future<T>
s valid() == false
.shared_future valid() == true
.make_ready_future
A new section 30.6.13 shall be inserted at the end of
template <typename T>
future<decay_t<T>> make_ready_future(T&& value);
future<void> make_ready_future();
future
if it
is an rvalue. Otherwise the value is copied to the shared state of the returned future
.
future<decay_t<T>>
, if function is given a value of type T
.future<void>
, if the function is not given any inputs. future<decay_t<T>>, valid() == true
.future<decay_t<T>>, is_ready() == true
.async
Change
The function template async
provides a mechanism to launch a function
potentially in a new thread and provides the result of the function in a future
object with which it shares a shared state.
template <class F, class... Args> future<result_of_t<decay_t<F>(decay_t<Args>...)>> async(F&& f, Args&&... args); template <class F, class... Args> future<result_of_t<decay_t<F>(decay_t<Args>...)>> async(launch policy, F&& f, Args&&... args); template<class F, class... Args> future<result_of_t<decay_t<F>(decay_t<Args>...)>> async(executor& ex, F&& f, Args&&... args);Change
launch::async | launch::deferred
and the
same arguments for F
and Args
. The second and third functions createpolicy & launch::async
is non-zero — calls
INVOKE (DECAY_COPY (std::forward<F>(f))
, DECAY_COPY (std::forward<Args>(args))...)
(20.8.2, 30.3.1.2) as if in a new thread of execution represented by a thread object
with the calls to DECAY_COPY ()
being evaluated in the thread that called
async
. Any return value is stored as the result in the shared state. Any
exception propagated from the execution of
INVOKE (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.policy & launch::deferred
is non-zero — 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 evaluates
INVOKE std::move(g), std::move(xyz))
where g
is the stored value of
DECAY_COPY (std::forward<F>(f))
and xyz
is the stored copy of
DECAY_COPY (std::forward<Args>(args))...
. The shared state is not made ready until the
function has completed. The first call to a non-timed waiting function (30.6.4)
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 of INVOKE (std::move(g), std::move(xyz))
begins, the function is no
longer considered deferred. launch::async | launch::deferred
, implementations should defer invocation or the selection of
the policy when no more concurrency can be effectively exploited.
— end note ]
Theexecutor::add()
function is given afunction<void()>
which callsINVOKE (DECAY_COPY (std::forward<F>(f)) DECAY_COPY (std::forward<Args>(args))...)
. The implementation of the executor is decided by the programmer.