Doc. no: N4242 Date: 2014-10-13 Revises: N4046 Reply-To: Christopher Kohlhoff <chris@kohlhoff.com>
N3785 Executors and schedulers, revision 3 describes a framework for executors. Unfortunately, this framework is built around some deliberate design choices that make it unsuited to the execution of fine grained tasks and, in particular, asynchronous operations. Primary among these choices is the use of abstract base classes and type erasure, but it is not the only such issue. The sum effect of these choices is that the framework is unable to exploit the full potential of the C++ language.
In this document, we will look at an alternative executors design that uses a lightweight, template-based policy approach. To describe the approach in a nutshell:
An executor is to function execution as an allocator is to allocation.
This proposal builds on the type traits described in N4045 Library Foundations for Asynchronous Operations, Revision 2 to outline a design that unifies the following areas:
std::async()
.
In doing so, it takes concepts from Boost.Asio, many of which have been unchanged since its inclusion in Boost, and repackages them in a way that is more suited to C++14 language facilities.
N4046 introduced a comprehensive library that covered included support for composition and coordination of chains of operations. In this revision we present a minimal subset of the proposal. This includes the facilities required to support a model for asynchronous operations, as required by the networking proposal. It also includes features that enable the use cases supported by N3785.
A standalone reference implementation of the proposed library can be found at http://github.com/chriskohlhoff/executors. This implementation requires a C++14 compiler.
To better illustrate how the executors library interacts with asynchronous operations, and to allow the facility to be used in production code, the library has been backported into the variant of Asio that stands alone from Boost. This variant is available at https://github.com/chriskohlhoff/asio/tree/master.
Run a function asynchronously:
post([]{ // ... });
Run a function asynchronously, on your own thread pool:
thread_pool pool; post(pool, []{ // ... }); pool.join();
Run a function asynchronously and wait for the result:
std::future<int> f = post(package([]{ // ... return 42; })); std::cout << f.get() << std::endl;
Run a function asynchronously, on your own thread pool, and wait for the result:
thread_pool pool; std::future<int> f = post(pool, package([]{ // ... return 42; })); std::cout << f.get() << std::endl;
Run a function in the future and wait for the result:
std::future<int> f = post_after( std::chrono::seconds(1), package([]{ // ... return 42; })); std::cout << f.get() << std::endl;
The central concept of this library is the executor as a policy. An executor embodies a set of rules about where, when and how to run a function object. For example:
Executor type |
Executor rules |
---|---|
|
Function objects are allowed to run on any thread in the process. |
|
Function objects are allowed to run on any thread in the pool, and nowhere else. |
|
Run function objects according to the underlying executor's rules, but also run them in FIFO order and not concurrently. |
Executors are ultimately defined by a set of type requirements, so the set of executors isn't limited to those listed here. Like allocators, library users can develop custom executor types to implement their own rules. Executors allow us to encapsulate all sorts of additional information and behaviour on a fine-grained basis, such as:
Certain objects may have an associated executor, which specifies how any function objects related to the object should be executed. For example, we may want to say that all event callbacks associated with a network protocol implementation should execute on a particular thread, or that a task should run at a particular priority. The notion of an associated executor allows us to decouple the specification of execution behaviour from the actual point of execution.
An execution context is a place where function objects
are executed. Where executors are lightweight and cheap to copy, an execution
context is typically long-lived and non-copyable. It may contain additional
state such as timer queues, socket reactors, or hidden threads to emulate asynchronous
functionality. Examples of execution contexts include thread_pool
,
loop_scheduler
, a Boost.Asio
io_service
, and the set of
all threads in the process.
We say that a thread_pool
is an execution context, and that it has
an executor. The thread pool contains long-lived state, namely the threads
that persist until the pool is shut down. The thread pool's executor embodies
the rule: run functions in the pool and nowhere else.
The thread pool's executor may be obtained by calling its get_executor()
member function.
To submit a function object to an executor or execution context, we can choose from one of three fundamental operations: dispatch, post and defer. These operations differ in the eagerness with which they run the submitted function.
A dispatch operation is the most eager.
dispatch(ex, []{ ... });
It means: run the function immediately, in the calling thread, if the rules allow it; otherwise, submit for later execution.
Executor type |
Executor rules |
Behaviour of dispatch |
---|---|---|
|
Function objects are allowed to run on any thread in the process. |
Always runs the function object before returning from |
|
Function objects are allowed to run on any thread in the pool, and nowhere else. |
If we are inside the thread pool, runs the function object before
returning from |
|
Run function objects according to the underlying executor's rules, but also run them in FIFO order and not concurrently. |
If we are inside the strand, or if the strand queue is empty, runs
the function object before returning from |
A consequence of calling dispatch()
is that, if the executor’s rules allow
it, the compiler is able to inline the function object call.
A post operation, on the other hand, is not permitted to run the function object itself.
post(ex, []{ ... });
It means: submit the function for later execution; never run the function object immediately. A posted function is scheduled for execution as soon as possible, according to the rules of the executor.
Executor type |
Executor rules |
Behaviour of post |
---|---|---|
|
Function objects are allowed to run on any thread in the process. |
Like |
|
Function objects are allowed to run on any thread in the pool, and nowhere else. |
Adds the function object to the thread pool's work queue. |
|
Run function objects according to the underlying executor's rules, but also run them in FIFO order and not concurrently. |
Adds the function object to the strand's work queue. |
Finally, the defer operation is the least eager of the three.
defer(ex, []{ ... });
A defer operation is similar to a post operation, in that it means: submit the function for later execution; never run the function object immediately. However, a defer operation also implies a relationship between the caller and the function object being submitted. It is intended for use when submitting a function object that represents a continuation of the caller.
Executor type |
Executor rules |
Behaviour of defer |
---|---|---|
|
Function objects are allowed to run on any thread in the process. |
If the caller is executing within the system-wide thread pool, saves
the function object to a thread-local queue. Once control returns
to the system thread pool, the function object is scheduled for execution
as soon as possible. |
|
Function objects are allowed to run on any thread in the pool, and nowhere else. |
If the caller is executing within the thread pool, saves the function
object to a thread-local queue. Once control returns to the thread
pool, the function object is scheduled for execution as soon as possible. |
|
Run function objects according to the underlying executor's rules, but also run them in FIFO order and not concurrently. |
Adds the function object to the strand's work queue. |
In this section we will examine a selection of examples, to see how the proposed executors library supports a range of use cases.
The behaviour of std::async()
function, when used with std::launch::async
,
may be trivially emulated as follows:
template <class F, class... Args> auto async(F&& f, Args&&... args) { return post( package( std::bind(std::forward<F>(f), std::forward<Args>(args)...))); }
Starting from the inside out, the expression:
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
creates a function object that will invoke f
with the specified arguments. Next:
package(...)
returns an object that will be lazily converted into a std::packaged_task<>
. We could also have used std::packaged_task<>
directly, as in:
std::packaged_task< std::result_of_t< std::decay_t<F>(std::decay_t<Args>...)>>(...)
In this example, the package()
function saves on typing by determining
the return type of F
for
us. Finally:
post(...)
submits the function object for execution on another thread, and then returns
immediately. When we submit a std::packaged_task<>
, post()
automatically deduces its return type to
be the std::future<>
type produced by the task. This future object is then returned from our version
of async()
.
Note that, unlike std::async()
,
the returned future object's destructor will not block.
In the Active Object design pattern, all operations associated with an object are run in its own private thread.
To implement an active object, we begin by defining a class member that is a thread pool containing a single thread.
class bank_account { int balance_ = 0; mutable thread_pool pool_{1}; // ... };
We then define each public member function so that it posts its implementation to the thread pool.
class bank_account { // ... void deposit(int amount) { post(pool_, package([=] { balance_ += amount; })).get(); } // ... };
In more detail, to implement an active object operation we begin by defining the body of the function:
[=] { balance_ += amount; }
which we then wrap in a lazily created std::packaged_task<>
:
package(...)
Finally, we submit the packaged task to the pool and wait for it to complete.
When we submit a std::packaged_task<>
,
post()
automatically deduces its return type to be the std::future<>
type produced by the task. We can
use this future to block until the operation is complete.
post(...).get();
An Activatable object is a variant of the Active Object pattern where the object does not have a private thread of its own. Instead, it can borrow one of the calling threads to process operations[1]. However, like Active Object, it ensures that all member state changes do not occur on more than one thread at a time.
To implement an activatable object, we create a strand on the system executor.
class bank_account { int balance_ = 0; mutable strand<system_executor> strand_; // ... };
We then define each public member function so that it dispatches its implementation to the strand.
class bank_account { // ... void deposit(int amount) { dispatch(strand_, package([=] { balance_ += amount; })).get(); } // ... };
Recall that a system_executor
object embodies this rule:
Function objects are allowed to run on any thread in the process.
while a strand embodies this rule:
Run function objects according to the underlying executor's rules, but also run them in FIFO order and not concurrently.
Finally, the call to dispatch()
means:
Run the function immediately, in the calling thread, if the rules allow it; otherwise, submit for later execution.
Thus, when we combine system_executor
,
strand
and dispatch()
:
dispatch(strand_, []{ ... });
we are effectively saying: if the strand is not busy, run the function object immediately. If there is no contention on the strand, latency is minimised. If there is contention, the strand still ensures that the function object never runs concurrently with any other function object submitted through the same strand.
The Leader/Followers design pattern is a model where multiple threads take turns to wait on event sources in order to dispatch and process incoming events.
Consider an example where a connection handler is responsible for receiving
messages from a client via UDP. The Leader/Followers pattern is implemented
using a thread_pool
object:
class connection_handler { // ... private: udp_socket socket_; thread_pool thread_pool_; // ... };
and involves the sequence of operations below.
void connection_handler::receive_and_dispatch() {
The leader thread waits for the next message to arrive.
char buffer[1024]; std::size_t length = socket_.receive(buffer, sizeof(buffer));
A new message has arrived. The leader thread promotes a follower to become the new leader.
std::experimental::post(thread_pool_, [this]{ receive_and_dispatch(); });
The now former leader processes the message.
// Process the new message and pass it to the order management bus. std::istringstream is(std::string(buffer, length)); order_management::new_order event; if (is >> event) order_management_bus_.dispatch_event(event); }
When the function returns, the former leader automatically returns to the pool as a follower thread.
Asynchronous operations are often chained, and in many cases an object may be associated with two or more chains. For example, an object to manage a connection may contain one chain to do the writing, and another to do the reading:
class connection { tcp::socket socket_; mutable_buffers_1 in_buffer_; mutable_buffers_1 out_buffer_; // ... void do_read() { socket_.async_read_some(in_buffer_, [this](error_code ec, size_t n) { // ... process input data ... if (!ec) do_read(); }); } void do_write() { // ... generate output data ... async_write(socket_, out_buffer_, [this](error_code ec, size_t n) { if (!ec) do_write(); }); } };
When these chains are run on a single-threaded event loop, it is not possible for more than one completion handler to run at any given time. This means that no synchronisation is required to protected shared data. However, if handlers are executed on a thread pool then some form of synchronisation will be required to avoid introducing data races.
The proposed library provides the strand<>
template to synchronise handlers.
A strand ensures that completion handlers never run concurrently, and explicit
synchronisation (such as a mutex) is still not required to protect shared
data. To implement this, we use the one strand for all asynchronous operations
associated with the object.
class connection { tcp::socket socket_; mutable_buffers_1 in_buffer_; mutable_buffers_1 out_buffer_; strand<io_service::executor_type> strand_; // ... void do_read() { socket_.async_read_some(in_buffer_, wrap(strand_, [this](error_code ec, size_t n) { // ... process input data ... if (!ec) do_read(); })); } void do_write() { // ... generate output data ... async_write(socket_, out_buffer_, wrap(strand_, [this](error_code ec, size_t n) { if (!ec) do_write(); })); } };
The wrap
function is used
to associated an executor with an object. In this example, we used wrap
to associated the strand with each
of the lambdas. The wrap
function works with any executor or execution context. For example, here
we associate a thread pool with a lamdba:
async_getline(std::cin, wrap(pool, [](std::string line) { std::cout << "Line: " << line << "\n"; }));
Rather than using the wrap
function, the associated executor may be manually specified by providing
a nested executor_type
typedef
and get_executor()
member function.
class line_printer { public: typedef loop_scheduler::executor_type executor_type; explicit line_printer(loop_scheduler& s) : executor_(s.get_executor()) { } executor_type get_executor() const noexcept { return executor_; } void operator()(std::string line) { std::cout << "Line: " << line << "\n"; } private: loop_scheduler::executor_type executor_; }; // ... async_getline(std::cin, line_printer(scheduler));
For this to work correctly, the async_getline
asynchronous operation must participate in an executor-aware model. To be
executor-aware, an asynchronous operation must:
get_associated_executor
.
executor_work
object for the associated executor. This tells the executor to expect
a function object to be submitted in the future. A thread pool, for example,
will know that it still has work to do and needs to keep running.
Our async_getline
operation
can then be written as follows:
template <class Handler> void async_getline(std::istream& is, Handler handler) {
The make_work
function automatically
obtains the associated executor and creates an executor_work
object for it.
auto work = make_work(handler);
The asynchronous operation itself is posted outside of the associated executor. This is because we want the line reading to be performed asynchronously with respect to the caller.
post([&is, work, handler=std::move(handler)]() mutable { std::string line; std::getline(is, line);
Once the asynchronous work is complete, we execute the completion handler via its associated executor.
// Pass the result to the handler, via the associated executor. dispatch(work.get_executor(), [line=std::move(line), handler=std::move(handler)]() mutable { handler(std::move(line)); }); }); }
When composing asynchronous operations, intermediate operations can simply reuse the associated executor of the final handler.
template <class Handler> void async_getlines(std::istream& is, std::string init, Handler handler) { // Get the final handler's associated executor. auto ex = get_associated_executor(handler); // Use the associated executor for each operation in the composition. async_getline(is, wrap(ex, [&is, lines=std::move(init), handler=std::move(handler)] (std::string line) mutable { if (line.empty()) handler(lines); else async_getlines(is, lines + line + "\n", std::move(handler)); })); }
This ensures that all intermediate completion handlers are correctly executed according to the caller's executor's rules.
A pipeline is a sequence of two or more long-running functions, known as stages, with each stage passing data to the next via a queue. The initial stage acts as a source of data, the intermediate stages act as filters, and the final stage as a sink.
As an example, let us consider a small framework for implementing pipelines. The function used to construct a pipeline is declared as:
template <class F, class... Tail> std::future<void> pipeline(F f, Tail... t);
This function returns a future
that can be used to wait until the pipeline is complete. The initial stage
of the pipeline must be a function with signature:
void initial(queue_front<T0> out);
The intermediate stages have signature:
void intermediate(queue_back<Tn> in, queue_front<Tn+1> out);
The pipeline's final stage has the signature:
void final(queue_back<TN> in);
By default, we want each stage of a pipeline to have its own thread. The
pipeline framework achieves this by calling get_associated_executor
with two arguments:
template <class F, class... Tail> std::future<void> pipeline(F f, Tail... t) { // ... auto ex = get_associated_executor(f, thread_executor()); // ... }
The thread_executor
class
is a custom executor type defined for the example. It starts a new thread
for every function passed to dispatch
,
post
or defer
.
If the function object type F
already has an associated executor then that executor will be used. The
thread_executor
is used for
types that do not specify an associated executor.
So, when we construct and run a pipeline like this:
void reader(queue_front<std::string> out); void filter(queue_back<std::string> in, queue_front<std::string> out); void upper(queue_back<std::string> in, queue_front<std::string> out); void writer(queue_back<std::string> in); // ... thread_pool pool; auto f = pipeline(reader, filter, wrap(pool, upper), writer); f.wait();
we are specifying that the the upper
stage should run on the thread pool, while reader
,
filter
and writer
should use the default behaviour
of launching a new thread.
The Actor model is a model for concurrency where objects, known as actors, communicate only by sending and receiving messages. Each actor's state is accessed only by its own internal thread or strand. This means that actors are inherently thread-safe.
To illustrate how executors may be used to facilitate actors, a tiny actor framework is included with the executors reference implementation. This framework is loosely based on the Theron library[2].
To implement an actor using this framework, we start by deriving a class
from actor
:
class member : public actor { // ...
When constructing an actor, we specify the executor to be used:
explicit member(executor e) : actor(std::move(e)) { // ... }
The polymorphic type executor
is used to allow the selection of an actor's executor to be delayed until
runtime. All of an actor's message handlers are executed according to that
policy. This could be a thread pool executor, but we may equally construct
actors with an executor that knows
about priorities.
The actor's message handlers are member functions, identified by argument type, and may be arbitrarily registered or deregistered:
void init_handler(actor_address next, actor_address from) { // ... register_handler(&member::token_handler); deregister_handler(&member::init_handler); }
Internally, the actor framework uses a per-actor strand
to ensure that the member functions are never called concurrently.
To send a message between actors we use either actor::send()
or actor::tail_send()
. The send()
operation is implemented in terms of the
actor's executor's post()
member function. The tail_send()
function is a distinct operation and conveys
additional information about the caller's intent which may be used to optimise
inter-actor messaging. It is implemented in terms of defer()
.
void token_handler(int token, actor_address /*from*/) { // ... tail_send(msg, to); } // ... };
Executor objects are lightweight and copyable to allow us to encapsulate all sorts of additional information and behaviour on a fine-grained basis. One use case for this is attaching priorities to function objects or tasks.
We begin by defining our priority scheduler class as an execution context. Internally, this class uses a priority queue to store pending function objects.
class priority_scheduler : public execution_context { // ... private: // ... struct item_comp { bool operator()( const std::shared_ptr<item_base>& a, const std::shared_ptr<item_base>& b) { return a->priority_ < b->priority_; } }; std::mutex mutex_; std::condition_variable condition_; std::priority_queue< std::shared_ptr<item_base>, std::vector<std::shared_ptr<item_base>>, item_comp> queue_; bool stopped_ = false; };
The priority_scheduler
class
provides a nested class executor_type
which satisfies the executor type requirements, and a member function get_executor()
to obtain an executor object. On construction, an executor_type
object captures a reference to the priority scheduler, as well as the specified
priority value.
class priority_scheduler : public execution_context { public: class executor_type { public: executor_type(priority_scheduler& ctx, int pri) noexcept : context_(ctx), priority_(pri) { } // ... private: priority_scheduler& context_; int priority_; }; executor_type get_executor(int pri = 0) noexcept { return executor_type(*this, pri); } // ... };
When a function object is submitted, the executor uses its stored priority to insert the function into the correct position in the priority queue:
class priority_scheduler : public execution_context { public: class executor_type { public: // ... template <class Func, class Alloc> void post(Func f, const Alloc& a) { auto p(std::allocate_shared<item<Func>>(a, priority_, std::move(f))); std::lock_guard<std::mutex> lock(context_.mutex_); context_.queue_.push(p); context_.condition_.notify_one(); } // ... }; // ... };
The priority scheduler's executors can then be used like any other:
priority_scheduler sched; auto low = sched.get_executor(0); auto med = sched.get_executor(1); auto high = sched.get_executor(2); // ... dispatch(low, []{ std::cout << "1\n"; }); dispatch(med, []{ std::cout << "2\n"; }); dispatch(high, []{ std::cout << "3\n"; });
Header |
Name |
Description |
---|---|---|
|
Class template |
Transforms a completion token into a completion handler. |
|
Class template |
Determines the result of an asynchronous operation’s initiating function. |
|
Class template |
Helper to simplify implementation of an asynchronous operation. |
|
Class template |
Used to determine a handler’s associated allocator. |
|
Function |
Obtain a handler’s associated allocator. |
|
Class template |
Base class for execution context types. |
|
Class template |
Used to determine a handler’s associated executor. |
|
Function |
Obtain a handler’s associated executor. |
|
Class template |
Associates an executor with an object. |
|
Function |
Associate an executor with an object. |
|
Class template |
Tracks outstanding work against an executor. |
|
Function |
Create work to track an outstanding operation. |
|
Class |
Executor representing all threads in system. |
|
Class |
Polymorphic wrapper for executors. |
|
Functions |
Execute a function object. |
|
Class template |
Executor adapter than runs function objects non-concurrently and in FIFO order. |
|
Functions |
Execute a function object at an absolute time. |
|
Functions |
Execute a function object after a relative time. |
|
Class template |
Completion token to enable futures with asynchronous operations. |
|
Class template specialization of |
Supports use of packaged_task with dispatch, post, defer, and asynchronous operations. |
|
Class templates |
Implements lazy creation of a packaged_task. |
|
Function |
Return a |
|
Class |
A fixed size thread pool. |
|
Class |
A thread pool where threads are explicitly donated by the caller. |
There has been some confusion due to the reuse of the term executor in N3785 and this proposal, but with slightly different meanings. In N3785, an "executor" refers to a heavyweight, non-copyable object, such as a thread pool. In this proposal, an "executor" is a lightweight, copyable policy object. This is distinct from a heavyweight object such as a thread pool, which is known as an "execution context".
N3785's API is superficially similar to Java executors, so it is interesting
to examine the Java prior art in this area. What we find is that N3785 misses
a key concept: the separation of Executor
and ExecutorService
. On the
other hand, it turns out that this proposal's "executor" mirrors
the concept and terminology of Java executors.
Let us start by reviewing a couple of the core interfaces of the Java executor framework: http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/package-summary.html
First, we have interface Executor
.
This interface provides a way of submitting a Runnable
(i.e. the equivalent of a function object) for execution, and it decouples
the submission from the concrete mechanism which runs the function.
Second, we have interface ExecutorService
.
This extends Executor
, i.e.
ExecutorService
is-a
Executor. It adds some additional functionality, such as the ability to request
that it be shut down.
A thread pool is-a ExecutorService
.
A fork/join pool is-a ExecutorService
.
An ExecutorService
represents
a heavyweight entity where Runnable
items are run.
A SerialExecutor
is-a
Executor
. A thread-per-task
executor is-a Executor
.
An executor represents a policy. Where there is a customisation point (as in
the ExecutorCompletionService
class) it is specified as an Executor
.
When we want to create our own policy, we do it by implementing the Executor
interface. Our Executor
policy object can be short lived, or it can be long lived. As we are using
Java, the object is newed and we let the garbage collector take care of it.
In fact, we don't really have a choice.
Java is not C++. Java references are not C++ references, nor are they C++ pointers. We do not have the garbage collector to clean up after us. Yet, and this is especially true of concurrent code, correctly managing object lifetime is critical. How do we address this in C++? The idiomatic approach is to use value semantics.
Thus our Executor
policy object
should use value semantics. We should be able to copy it and move it freely.
Where a particular concrete Executor
uses some allocated resource, the constructors and destructors can manage the
resource lifetime, just as we do in other standard library components.
Of course, we do want to be able to use a heavyweight thread pool as an Executor
. In Java, the thread pool is-a
ExecutorService
which is-a
Executor
, so we are able to
use the heavyweight object in the same way as a lightweight one. Once again,
this is because of Java's reference semantics, where basically all objects
are treated the same, whether light or heavy.
In C++, however, our heavyweight thread pool may be best represented by a non-copyable
type. This presents a challenge: how do we establish a pattern where we can
pass either a noncopyable type or a type with value semantics? That is, how
can we have an interface where we can pass either a heavyweight ExecutorService
or a lightweight Executor
?
The solution is to change the relationship between ExecutorService
and Executor
. Rather than saying
an ExecutorService
is-a
Executor
, we instead say an
ExecutorService
has-a
Executor
. Every ExecutorService
has an associated lightweight
Executor
policy which encapsulates
the submission logic. This lightweight Executor
provides the necessary value semantics.
Thus we can see that this proposal's "executor" is in fact the same
concept as the Java Executor
.
It is just that it is packaged in a way that is more idiomatic C++, i.e. it
uses value semantics. This proposal's "execution context" concept
is the equivalent of Java's ExecutorService
.
The executor is the "how", a policy, and it logically performs the
execution. The execution context or ExecutorService
is the "where", a venue if you like.
As we are using C++, and not Java, we get the same level of abstraction but with the benefits of compile time polymorphism, inlining, control over memory use, and using fewer allocations (i.e. we create less garbage). The reasons we are using C++ in the first place.
As we can see, the separation of executor from execution context clearly exists in the prior art represented by Java. However, this proposal's design is derived from Boost.Asio, and is very much driven by what is required to make asynchronous operations work, but with a desire to have a clean separation of concerns. The Java executors framework did not inform the design, yet it is not surprising that Java represents an example of convergent evolution, once we make a deeper analysis of the library.
Let us take another look at the specification of the Java Executor
class and its execute
method:
http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executor.html
It says:
void execute(Runnable command)
Executes the given command at some time in the future. The command may execute in a new thread, in a pooled thread, or in the calling thread, at the discretion of the Executor implementation.
Note that the wording includes "in the calling thread". In fact,
the specification of the execute method is essentially the same as the dispatch
function of this proposal.
As it is the only available Executor
method, this specification of execute
is a problem. Clearly there are times when we want to guarantee that a function
will not run in the calling thread, such as when we want
launch a long running function. Unfortunately, if we are using an Executor
in a polymorphic context we have
no way of knowing what behaviour we will get. We have to know the concrete
executor type to ensure that it doesn't run in the calling thread.
Thus, we wish to introduce a function with slightly different semantics:
Executes the given command at some time in the future. The command may execute in a new thread, in a pooled thread, at the discretion of the Executor implementation.
This is the post
function of
this proposal, and it lets us as the caller ensure that a function does not
run in the calling thread.
However, this does not obviate the need for the original semantics (i.e. the current Java semantics). There are times when it is to our advantage to allow a function to run in the calling thread. Some examples:
Interestingly, the specification of future::then
(now
removed from the concurrency TS) that took an executor would also suffer from
this extra cost unless it had access to dispatch semantics.
The defer
operation, like
post
, does not allow the function
to run in the calling thread. Where it differs is in the expression of the
intent of the caller. Using defer
states that there is a relationship between the caller and callee, such as
being consecutive links in a chain of function objects. An executor can make
use of this to do smarter, more efficient scheduling.
For example, consider a chain of asynchronous read operations on a socket:
void read_loop(Socket socket, Buffer buffer) { async_read(socket, buffer, [&](error_code, size_t n) { process_data(buffer, n); read_loop(socket, buffer); }); }
where an individual read operation is implemented something like this:
template <class Handler> void async_read(Socket socket, Buffer buffer, Handler handler) { // Perform a speculative read first. error_code ec; size_t n = non_blocking_read(socket, buffer, ec); if (ec != would_block) { // Read completed immediately, post handler. ex = get_associated_executor(handler); post(ex, [=]{ handler(ec, n); }); } else { // Wait for socket to become readable. // ... } }
In certain circumstances the read operation will always complete immediately, such as when the data is already available in the kernel buffers. When this occurs, the sequence of operations is essentially equivalent to:
void read_loop(socket, buffer) { // ... ex.post([&]{ // #1 read_loop(socket, buffer); }); // ... }
Let us assume that our executor ex
uses a thread pool with a mutex-protected queue:
class my_thread_pool { public: class executor_type { public: // ... template <class Func, class Alloc> void post(Func f, const Alloc& a) { auto p(std::allocate_shared<item<Func>>(a, std::move(f))); std::lock_guard<std::mutex> lock(pool_.mutex_); // #2 pool_.queue_.push_back(std::move(p)); // #3 pool_.condition_.notify_one(); // #4 // #5 } // ... }; // ... void run() { for (;;) { std::unique_lock<std::mutex> lock(mutex_); // #6 condition_.wait(lock, [&]{ !queue_.empty(); }); auto p(std::move(queue_.front())); // #7 queue_.pop_front(); lock.unlock(); // #8 p->execute_(p); // #9 } } private: std::mutex mutex_; std::condition_variable condition_; std::deque<std::shared_ptr<item_base>> queue_; };
There are two performance issues at play here. First, each "cycle"
of read_loop
involves two lock/unlock
pairs. Second, a condition variable may be used to wake a sleeping thread when
the queue is non-empty. If we step through the code we will see the following:
On the other hand, with defer
we are telling the executor that the submitted function is a continuation of
the current one. That is, the executor does not have to eagerly schedule the
function because we have told it that one function follows the other.
void read_loop(socket, buffer) { // ... ex.defer([&]{ // #1 read_loop(socket, buffer); }); // ... } class my_thread_pool { public: class executor_type { public: // ... template <class Func, class Alloc> void defer(Func f, const Alloc& a) { if (pool_.thread_local_queue_) { auto p(std::allocate_shared<item<Func>>(a, std::move(f))); pool_.thread_local_queue_->push_back(std::move(p)); // #2 } else post(std::move(f), a); } // ... }; // ... void run() { std::deque<std::shared_ptr<item_base>> local_queue; thread_local_queue_ = &local_queue; for (;;) { std::unique_lock<std::mutex> lock(mutex_); // #3 while (!local_queue.empty()) // #4 { queue_.push(std::move(local_queue.front())); local_queue.pop_front(); } condition_.wait(lock, [&]{ !queue_.empty(); }); auto p(std::move(queue_.front())); // #5 queue_.pop_front(); lock.unlock(); // #6 p->execute_(p); // #7 } } private: std::mutex mutex_; std::condition_variable condition_; std::deque<std::shared_ptr<item_base>> queue_; static thread_local std::deque<std::shared_ptr<item_base>>* thread_local_queue_; };
Now when we step through the code:
we see that we have eliminated one lock/unlock pair, and we also no longer
need to wake another thread. We are able to do this because of the additional
information imparted by defer
.
On recent hardware we can observe an uncontended lock/unlock cost of some 10 to 15 nanoseconds, compared with 1 to 2 nanoseconds for accessing a thread-local queue. There is also a significant (and often larger) benefit in avoiding the unnecessary thread wakeup and the ensuing lock contention, particularly when dealing with bursty traffic profiles. Either way, this is a latency win.
With asynchronous operations, the rules are that if an operation completes immediately it posts the result (rather than dispatch, which may result in unfairness, starvation or stack overflow). If it finishes later, it dispatches the result (to minimise latency).
By default, an individual low-level asynchronous operation, such as async_read
shown above, doesn't know if the
operation represents a continuation of the current function, or a new fork
in the control flow. Either one is possible, so we conservatively assume that
every operation represents a new fork and use post
.
However, once we move to a higher layer of abstraction, like a composed operation to read a message frame, we can start to make certain assertions. We know that within the operation it consists of a single chain of asynchronous reads.
As an example, let us consider a hypothetical composed operation to read a
message frame, implemented in terms of async_read
above. Each message frame consists of a header, a body in several chunks, and
a trailer. In this scenario, the header and body are immediately available
in the kernel buffers, but we have to wait for the trailer to arrive. The sequence
of executor operations used by the asynchronous chain looks like this:
post()
post()
post()
dispatch()
One of the motivating reasons for having lightweight, copyable executors, distinct
from the execution context, is that they let us remap the executor operations
as required. Thus, within the composed operation we can remap post
to defer
.
We can do this with a lightweight wrapper around the composed operation's handler's
associated executor:
template <class Executor> class remap_post_to_defer { ... template <class F, class A> void post(F f, const A& a) { ex_.defer(std::move(f), a); } ... Executor ex_; };
We can then apply this wrapper to optimise the intermediate steps of the chain:
post()
defer()
defer()
dispatch()
If we had a limited vocabulary that only consisted of dispatch:
dispatch()
dispatch()
dispatch()
dispatch()
then traffic bursts can lead to unfairness and starvation. We are susceptible to denial of service attacks.
If our vocabulary only consisted of post:
post()
post()
post()
post()
then every operation in the chain can incur otherwise avoidable synchronisation costs, context switches, and cycles through the scheduler, resulting in higher latency.
If our vocabulary only consisted of defer:
defer()
defer()
defer()
defer()
then we almost get away with it, apart from the additional latency introduced
by defer
at the end of the
operation. However, we are also limiting the opportunities for concurrency.
This may not be an issue in this example with a single chain of operations,
but can be a problem where your asynchronous control flow really does fork,
such as in a typical accept "loop":
void handle_accept() { new_socket->start(); // starts asynchronous reads and writes async_accept(..., &handle_accept); // accepts next connection }
Thus we need all three operations to complete the set:
post
— the default choice,
guaranteeing non-blocking calls and maximising concurrency
dispatch
— for minimising
latency when we are prepared to accept blocking
defer
— to link sequences
of related operations
Note that, when implementing the executor type requirements, it is perfectly
fine to start by implementing dispatch
and defer
in terms of post
. This is in keeping with the specified
semantics. Then, we can optimise the implementation of these functions as we
are able to.
However, are these three operations sufficient? Might there be more things that a user wants to communicate to the executor, about how a function or task should be launched? For example, a priority or a hard real-time deadline.
The proposed library meets these needs by giving a function object or task
an associated executor. As lightweight, copyable objects, executors allow us
to encapsulate all sorts of additional information and behaviour on a fine-grained
basis, such as priority. The associated executor determines how it should be
executed, and the point of association may be distant in time and space from
the point where a function is submitted using dispatch
,
post
and defer
.
This is a pure library proposal. It does not add any new language features, nor does it alter any existing standard library headers. It makes additions to experimental headers that may also be modified by other Technical Specifications.
This library can be implemented using compilers that conform to the C++14 standard. An implementation of this library requires operating system-specific functions that lie outside the C++14 standard.
This proposal builds on the type traits defined in N4045 Library Foundations for Asynchronous Operations. This paper is intended as an alternative proposal to N3785 Executors and schedulers.
A substantial subset of the executors library specified below is a prerequisite for the networking library. For this reason, the networking library's proposed text also incorporates a specification of these facilities.
The type traits introduced in N4045 Library Foundations for Asynchronous Operations define an extensible asynchronous model that can support a variety of composition methods, including:
std::future
but also future classes supplied by other libraries.
The library introduced in this paper applies this asynchronous model, and its design philosophy, to executors. Rather than a design that is restricted to runtime polymorphism, we can allow users to choose the approach that is appropriate to their use case.
The author would like to thank Jamie Allsop, Arash Partow and Dietmar Kühl for providing feedback, corrections and suggestions on both the library implementation and this proposal.
The interface is based on inheritance and polymorphism, rather than on templates, for two reasons. First, executors are often passed as function arguments, often to functions that have no other reason to be templates, so this makes it possible to change executor type without code restructuring. Second, a non-template design makes it possible to pass executors across a binary interface: a precompiled library can export a function one of whose parameters is an executor.
As we will see in this proposal, a template-based design does not preclude the inclusion of a runtime polymorphic wrapper. Such a wrapper still allows users to write non-template code for use with executors, and makes it possible to pass executors across a binary interface. On the other hand, it is not possible to undo the performance impact of a type-erased interface.
The cost of an additional virtual dispatch is almost certainly negligible compared to the other operations involved.
This claim might be true when passing coarse-grained tasks across threads. However, use cases for executors are not limited to this. As outlined in N4045, composition of asynchronous operations may entail multiple layers of abstraction. The ability to leverage function inlining is a key part of delivering a low abstraction penalty, but the compiler is unable to see through a virtual interface.
Most fundamentally, of course, executor is an abstract base class and add() is a virtual member function, and function templates can’t be virtual. Another reason is that a template parameter would complicate the interface without adding any real generality. In the end an executor class is going to need some kind of type erasure to handle all the different kinds of function objects with void() signature, and that’s exactly what std::function already does.
By forcing type erasure at the executor interface, an executor implementer is denied the opportunity to choose a more appropriate form of type erasure. For example, an implementer may wish to store pending work items in a linked list. With a template-based approach, the function object and the “next pointer” can be stored in the same object. This is not possible if type erasure has already occurred[3].
One theoretical advantage of a template-based interface is that the executor might sometimes decide to execute the work item inline, rather than enqueueing it for asynchronous, in which case it could avoid the expense of converting it to a closure. In practice this would be very difficult, however: the executor would somehow have to know which work items would execute quickly enough for this to be worthwhile.
There is a key use case for wanting to execute work items inline: delivering the result of an asynchronous operation, possibly across multiple layers of abstraction. Rather than relying on the executor to “somehow have to know”, we can allow the user to choose. This is the approach taken in this proposal.
Finally, another disadvantage of std::function
is that it prevents the use of move-only function objects.
Of course, as N3785 states, the need for a type-erased function object is itself a consequence of the use of inheritance and polymorphism.
There are several important design decisions involving that time-based functionality. First: how do we handle executors that aren’t able to provide it? The issue is that add_at and add_after involve significant implementation complexity. In Microsoft’s experience it’s important to allow very simple and lightweight executor classes that don’t need such complicated functionality.
N3785 couples timer-based operations to executors via the scheduled_executor
base class. This proposal avoids this coupling by distinguishing between
the executor as a lightweight policy object, and an execution context where
the “implementation complexity” of timers can be housed in a reusable
way. Thus, timer operations are independent of executor types, and can be
used with any executor.
Second, how should we specify time? [...] Some standard functionality, like sleep_until and sleep_for, is templatized to deal with arbitrary duration and time_point specializations. That’s not an option for an executor library that uses virtual functions, however, since virtual member functions can’t be function templates. There are a number of possible options:
1. Redesign the library to make executor a concept rather than an abstract base class. We believe that this would be invention rather than existing practice, and that it would make the library more complicated, and less convenient for users, for little gain.
2. Make executor a class template, parameterized on the clock. As discussed above, we believe that a template-based design would be less convenient than one based on inheritance and runtime polymorphism.
3. Pick a single clock and use its duration and time_point.
We chose the last of those options, largely for simplicity.
Unfortunately, N3785 chooses system_clock
as that single clock. As the system clock is susceptible to clock changes
it may be inappropriate for use cases that require a periodic timer. In those
instances, steady_clock
is
the better choice.
In any case, by decoupling timers from executors, this proposal provides timer operations that are indeed templates, and can work with arbitrary clock types.
A more interesting question is what happens if a user closure throws an exception. The exception will in general be thrown by a different thread than the one that added the closure or the thread that started the executor, and may be far separated from it in time and in code location. As such, unhandled exceptions are considered a program error because they are difficult to signal to the caller. The decision we made is that an exception from a closure is ill-formed and the implementation must call std::terminate.
Rather than apply a blanket rule, this proposal includes exception handling
as part of an executor’s policy. While std::terminate
is likely to be the best choice for unhandled exceptions inside a thread
pool, users may prefer greater control when using something like loop_executor
. For example, the approach
taken by Boost.Asio’s io_service
and this proposal’s loop_scheduler
is to allow exceptions to escape from the “event loop”, where the user
can handle them as appropriate.
[...] Inline executors, which execute inline to the thread which calls add(). This has no queuing and behaves like a normal executor, but always uses the caller’s thread to execute.
Since the executor interface is defined by an abstract base class, code that
calls the add()
function has to assume that the underlying executor may execute the function
object inline. As a consequence, extra care must be taken in situations such
as:
add()
while holding the lock.
add()
for each element, ensuring the added
function object cannot invalidate the iterators.
This proposal instead makes an explicit distinction between operations that can execute inline, and those that cannot. The library user is then able to choose the appropriate operation for their use case.
<experimental/type_traits>
synopsishandler_type
async_result
async_completion
<experimental/memory>
synopsisassociated_allocator
get_associated_allocator
<experimental/executor>
synopsisexecution_context
execution_context::service
is_executor
uses_executor
associated_executor
get_associated_executor
executor_wrapper
wrap
executor_work
make_work
system_executor
bad_executor
executor
dispatch
post
defer
<experimental/strand>
synopsisstrand
<experimental/timer>
synopsisdispatch_at
post_at
defer_at
dispatch_after
post_after
defer_after
<experimental/future>
synopsisuse_future_t
async_result
for packaged_task
packaged_handler
packaged_token
package
<experimental/thread_pool>
synopsisthread_pool
thread_pool::executor_type
<experimental/loop_scheduler>
synopsisloop_scheduler
loop_scheduler::executor_type
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class CompletionToken, class Signature, class = void> struct handler_type; template<class CompletionToken, class Signature> using handler_type_t = typename handler_type<CompletionToken, Signature>::type; template<class Handler> class async_result; template<class CompletionToken, class Signature> struct async_completion; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class CompletionToken, class Signature, class = void> struct handler_type { typedef see below type; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
Template parameter CompletionToken
specifies the model used to obtain the result of the asynchronous operation.
Template parameter Signature
is the call signature (C++ Std, [func.def]) for the handler type invoked
on completion of the asynchronous operation.
A program may specialize this trait if the CompletionToken
template parameter in the specialization is a user-defined type.
Specializations of handler_type
shall define a nested handler type type
that satisfies the MoveConstructible
requirements, and objects of type type
shall be constructible from an lvalue or rvalue of the type specified by
the CompletionToken
template
parameter.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Handler> class async_result { public: typedef void type; explicit async_result(Handler&); async_result(const async_result&) = delete; async_result& operator=(const async_result&) = delete; type get(); }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
Template argument Handler
is a handler type produced by handler_type_t<T,
S>
for some completion token type T
and call signature S
.
A program may specialize this template if the Handler
template parameter in the specialization is a user-defined type.
Specializations of async_result
shall satisfy the Destructible
requirements (C++ Std, [destructible]) in addition to the requirements in
the table below. In this table, R
is a specialization of async_result
for the template parameter Handler
;
r
is a modifiable lvalue
of type R
; and h
is a modifiable lvalue of type Handler
.
Table 1. async_result specialization requirements
Expression |
Return type |
Note |
---|---|---|
|
| |
| ||
|
|
The |
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class CompletionToken, class Signature> struct async_completion { typedef handler_type_t<CompletionToken, Signature> handler_type; explicit async_completion(remove_reference_t<CompletionToken>& t); async_completion(const async_completion&) = delete; async_completion& operator=(const async_completion&) = delete; see below handler; async_result<handler_type> result; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
Template parameter CompletionToken
specifies the model used to obtain the result of the asynchronous operation.
Template parameter Signature
is the call signature (C++ Std, [func.def]) for the handler type invoked
on completion of the asynchronous operation.
explicit async_completion(remove_reference_t<CompletionToken>& t);
Effects: If
CompletionToken
andhandler_type
are the same type, bindshandler
tot
; otherwise, initializeshandler
with the result offorward<CompletionToken>(t)
. Initializesresult
withhandler
.
see below handler;
Type:
handler_type&
ifCompletionToken
andhandler_type
are the same type; otherwise,handler_type
.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T, class Alloc = allocator<void>> struct associated_allocator; template<class T, class Alloc = allocator<void>> using associated_allocator_t = typename associated_allocator<T, Alloc>::type; // get_associated_allocator: template<class T> associated_allocator_t<T> get_associated_allocator(const T& t); template<class T, class Alloc> associated_allocator_t<T, Alloc> get_associated_allocator(const T& t, const Alloc& a); } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T, class Alloc = allocator<void>> struct associated_allocator { typedef see below type; static type get(const T& t, const Alloc& a = Alloc()) noexcept; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
A program may specialize this traits type if the T
template parameter in the specialization is a user-defined type. The template
parameter Alloc
shall be
a type meeting Allocator
requirements (C++ Std, [allocator.requirements]).
Specializations of associated_allocator
shall satisfy the requirements in the table below. In this table, X
is a specialization of associated_allocator
for the template parameter
T
; t
is a const reference to an object of type T
;
and a
is an object of type
Alloc
.
Table 2. associated_allocator specialization requirements
Expression |
Return type |
Note |
---|---|---|
|
A type meeting | |
|
|
Shall not exit via an exception. |
|
|
Shall not exit via an exception. |
template<class T> associated_allocator_t<T> get_associated_allocator(const T& t);
Returns:
associated_allocator<T>::get(t)
.
template<class T, class Alloc> associated_allocator_t<T, Alloc> get_associated_allocator(const T& t, const Alloc& a);
Returns:
associated_allocator<T, Alloc>::get(t, a)
.
namespace std { namespace experimental { inline namespace concurrency_v1 { enum class fork_event { prepare, parent, child }; class execution_context; class service_already_exists; template<class Service> Service& use_service(execution_context& ctx); template<class Service, class... Args> Service& make_service(execution_context& ctx, Args&&... args); template<class Service> bool has_service(execution_context& ctx) noexcept; template<class T> struct is_executor : false_type {}; struct executor_arg_t { }; constexpr executor_arg_t executor_arg = executor_arg_t(); template<class T, class Executor> struct uses_executor; template<class T, class Executor = system_executor> struct associated_executor; template<class T, class Executor = system_executor> using associated_executor_t = typename associated_executor<T, Executor>::type; // get_associated_executor: template<class T> associated_executor_t<T> get_associated_executor(const T& t); template<class T, class Executor> associated_executor_t<T, Executor> get_associated_executor(const T& t, const Executor& ex); template<class T, class ExecutionContext> associated_executor_t<T, typename ExecutionContext::executor_type> get_associated_executor(const T& t, ExecutionContext& ctx); template<class T, class Executor> struct executor_wrapper; template<class T, class Executor, class Signature> struct handler_type<executor_wrapper<T, Executor>, Signature>; template<class T, class Executor> class async_result<executor_wrapper<T, Executor>>; template<class T, class Executor, class Allocator> struct associated_allocator<executor_wrapper<T, Executor>, Allocator>; template<class T, class Executor, class Executor1> struct associated_executor<executor_wrapper<T, Executor>, Executor1>; // wrap: template<class Executor, class T> executor_wrapper<decay_t<T>, Executor> wrap(const Executor& ex, T&& t); template<class ExecutionContext, class T> executor_wrapper<decay_t<T>, typename ExecutionContext::executor_type> wrap(ExecutionContext& ctx, T&& t); template<class T, class Executor> struct executor_work; // make_work: template<class Executor> executor_work<Executor> make_work(const Executor& ex); template<class ExecutionContext> executor_work<typename ExecutionContext::executor_type> make_work(ExecutionContext& ctx); template<class T> executor_work<associated_executor_t<T>> make_work(const T& t); template<class T, class Executor> executor_work<associated_executor_t<T, Executor>> make_work(const T& t, const Executor& ex); template<class T, class ExecutionContext> executor_work<associated_executor_t<T, typename ExecutionContext::executor_type>> make_work(const T& t, ExecutionContext& ctx); class system_executor; bool operator==(const system_executor&, const system_executor&); bool operator!=(const system_executor&, const system_executor&); template<> struct is_executor<system_executor> : true_type {}; class bad_executor; class executor; template <> struct is_executor<executor> : true_type {}; bool operator==(const executor& a, const executor& b) noexcept; bool operator==(const executor& e, nullptr_t) noexcept; bool operator==(nullptr_t, const executor& e) noexcept; bool operator!=(const executor& a, const executor& b) noexcept; bool operator!=(const executor& e, nullptr_t) noexcept; bool operator!=(nullptr_t, const executor& e) noexcept; // dispatch: template<class CompletionToken> auto dispatch(CompletionToken&& token); template<class Executor, class CompletionToken> auto dispatch(const Executor& ex, CompletionToken&& token); template<class ExecutionContext, class CompletionToken> auto dispatch(ExecutionContext& ctx, CompletionToken&& token); // post: template<class CompletionToken> auto post(CompletionToken&& token); template<class Executor, class CompletionToken> auto post(const Executor& ex, CompletionToken&& token); template<class ExecutionContext, class CompletionToken> auto post(ExecutionContext& ctx, CompletionToken&& token); // defer: template<class CompletionToken> auto defer(CompletionToken&& token); template<class Executor, class CompletionToken> auto defer(const Executor& ex, CompletionToken&& token); template<class ExecutionContext, class CompletionToken> auto defer(ExecutionContext& ctx, CompletionToken&& token); } // inline namespace concurrency_v1 } // namespace experimental template<class Alloc> struct uses_allocator<std::experimental::concurrency_v1::executor, Alloc> : true_type {}; } // namespace std
The library describes a standard set of requirements for executors. A type meeting Executor requirements shall embody a set of rules for determining how submitted function objects are to be executed.
An executor type X
shall
satisfy the requirements of CopyConstructible
(C++ Std, [copyconstructible]) types. No constructor, comparison operator,
copy operation, move operation, swap operation, or member functions context
, on_work_started
and on_work_finished
on
these types shall exit via an exception.
The executor copy constructor, comparison operators, and member functions defined in these requirements shall not introduce data races as a result of concurrent calls to those functions from different threads.
In the table below, X
denotes
an executor class, x
denotes
a value of type X&
,
x1
and x2
denote values of type const X&
,
x3
denotes a value of type
X&&
,
f
denotes a MoveConstructible
(C++ Std, [moveconstructible])
function object callable with zero arguments, a
denotes a value of type A
meeting Allocator
requirements
(C++ Std, [allocator.requirements]), t
denotes an object of type T
,
and u
denotes an identifier.
Table 3. Executor requirements
expression |
type |
assertion/note |
---|---|---|
|
Shall not exit via an exception. | |
|
Shall not exit via an exception. | |
|
|
Shall not exit via an exception. |
|
|
Shall not exit via an exception. |
|
|
Shall not exit via an exception. |
|
Shall not exit via an exception. | |
|
Shall not exit via an exception. | |
|
Effects: Calls | |
|
Effects: Calls | |
|
Effects: Calls |
A class is a service if it is publicly derived from another service, or
if it is a class publicly derived from execution_context::service
.
A service may contain a publicly-accessible nested typedef named key_type
. If the nested typedef key_type
exists, the service class shall
be the same type as key_type
,
or otherwise publicly and unambiguously derived from key_type
.
All services define a one-argument constructor that takes a reference to
the execution_context
object
that owns the service. This constructor is explicit,
preventing its participation in automatic conversions.
A service may provide additional constructors with two or more arguments,
where the first argument is a reference to the execution_context
object that owns the service. [Note: These constructors
may be called by the make_service
function. —end note]
[Example:
class my_service : public execution_context::service { public: typedef my_service key_type; explicit my_service(execution_context& ctx); private: virtual void shutdown_service(); ... };
—end example]
A service's shutdown_service
member function must cause all copies of user-defined function objects
that are held by the service to be destroyed.
namespace std { namespace experimental { inline namespace concurrency_v1 { class execution_context { public: class service; // construct / copy / destroy: execution_context(); execution_context(const execution_context&) = delete; execution_context& operator=(const execution_context&) = delete; virtual ~execution_context(); // execution context operations: void notify_fork(fork_event e); protected: // execution context protected operations: void shutdown_context(); void destroy_context(); }; // service access: template<class Service> Service& use_service(execution_context& ctx); template<class Service, class... Args> Service& make_service(execution_context& ctx, Args&&... args); template<class Service> bool has_service(execution_context& ctx) noexcept; class service_already_exists : public logic_error { ... }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
Class execution_context
implements
an extensible, type-safe, polymorphic set of services, indexed by service
type.
Access to the services of an execution_context
is via three function templates, use_service<>
, make_service<>
and has_service<>
.
In a call to use_service<Service>()
, the type argument chooses a service,
making available all members of the named type. If the service is not present
in an execution_context
,
an object of type Service
is created and added to the execution_context
.
A C++ program can check if an execution_context
implements a particular service with the function template has_service<Service>()
.
Service objects may be explicitly added to an execution_context
using the function template make_service<Service>()
. If the service is already present,
the service_already_exists
exception is thrown.
Once a service reference is obtained from an execution_context
object by calling use_service<>
, that reference remains usable until
a call to destroy_context()
.
execution_context();
Effects: Creates an object of class
execution_context
.
~execution_context();
Effects: Destroys an object of class
execution_context
. Performsshutdown_context()
followed bydestroy_context()
.
void notify_fork(fork_event e);
Effects: For each service object
svc
in the set:
— Ife == fork_event::prepare
, performssvc->notify_fork(e)
in reverse order of the beginning of service object lifetime (C++ Std, [basic.life]).
— Otherwise, performssvc->notify_fork(e)
in order of the beginning of service object lifetime.
void shutdown_context();
Effects: For each service object
svc
in theexecution_context
set, in reverse order of the beginning of service object lifetime (C++ Std, [basic.life]), performssvc->shutdown_service()
.
[Note:
shutdown_context
is an idempotent operation. —end note]
void destroy_context();
Effects: Destroys each service object in the
execution_context
set, in reverse order of the beginning of service object lifetime (C++ Std, [basic.life]).
[Note:
destroy_context
is an idempotent operation. —end note]
The functions use_service
,
make_service
and has_service
shall not introduce data
races as a result of concurrent calls to those functions from different
threads.
template<class Service> Service& use_service(execution_context& ctx);
Let
Key
beService::key_type
if the nested typedefService::key_type
exists; otherwise, letKey
beService
.
Requires:
Service
is a service class that is publicly and unambiguously derived fromexecution_context::service
. If the nested typedefService::key_type
exists,Service
is the same type asService::key_type
, orService
is publicly and unambiguously derived fromService::key_type
, andService::key_type
is publicly and unambiguously derived fromexecution_context::service
.
Effects: If an object of type
Key
does not already exist in theexecution_context
set identified byctx
, creates an object of typeService
, initializing it withService(ctx)
, and adds it to the set.
Returns: A reference to the corresponding service of
ctx
.
Notes: The reference returned remains valid until a call to
destroy_context
.
template<class Service, class... Args> Service& make_service(execution_context& ctx, Args&&... args);
Let
Key
beService::key_type
if the nested typedefService::key_type
exists; otherwise, letKey
beService
.
Requires:
Service
is a service class that is publicly and unambiguously derived fromexecution_context::service
. If the nested typedefService::key_type
exists,Service
is the same type asService::key_type
, orService
is publicly and unambiguously derived fromService::key_type
, andService::key_type
is publicly and unambiguously derived fromexecution_context::service
. A service object of typeKey
does not already exist in theexecution_context
set identified byctx
.
Effects: Creates an object of type
Service
, initializing it withService(ctx, forward<Args>(args)...)
, and adds it to theexecution_context
set identified byctx
.
Throws:
service_already_exists
if a corresponding service object of typeKey
is already present in the set.
Notes: The reference returned remains valid until a call to
destroy_context
.
template<class Service> bool has_service(execution_context& ctx) noexcept;
Let
Key
beService::key_type
if the nested typedefService::key_type
exists; otherwise, letKey
beService
.
Requires:
Service
is a service class that is publicly and unambiguously derived fromexecution_context::service
. If the nested typedefService::key_type
exists,Service
is the same type asService::key_type
, orService
is publicly and unambiguously derived fromService::key_type
, andService::key_type
is publicly and unambiguously derived fromexecution_context::service
.
Returns: If an object of type
Key
is present inctx
,true
; otherwise,false
.
namespace std { namespace experimental { inline namespace concurrency_v1 { class execution_context::service { protected: // construct / copy / destroy: service(execution_context& owner); service(const service&) = delete; service& operator=(const service&) = delete; virtual ~service(); // service observers: execution_context& context() noexcept; private: friend class execution_context; // exposition only // service operations: virtual void shutdown_service() = 0; virtual void notify_fork(fork_event e); execution_context& context_; // exposition only }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
service(execution_context& owner);
Postconditions:
&context_ == &owner
.
execution_context& context() noexcept;
Returns:
context_
.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T> struct is_executor : false_type {}; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
is_executor
can be used to
detect executor types satisfying the Executor
type requirements.
Instantiations of the is_executor
template shall meet the UnaryTypeTrait requirements (C++ Std, [meta.rqmts]).
A program may specialize this template for a user-defined type T
to have a BaseCharacteristic of integral_constant<int,
N
>
with N
> 0
to indicate that T
should be treated as an executor type.
namespace std { namespace experimental { inline namespace concurrency_v1 { struct executor_arg_t { }; constexpr executor_arg_t executor_arg = executor_arg_t(); } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
The executor_arg_t
struct
is an empty structure type used as a unique type to disambiguate constructor
and function overloading. Specifically, types may have constructors with
executor_arg_t
as the first
argument, immediately followed by an argument of a type that satisfies the
Executor requirements.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T, class Executor> struct uses_executor; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
Remark: Detects whether T
has a nested executor_type
that is convertible from Executor
.
Meets the BinaryTypeTrait
requirements (C++ Std, [meta.rqmts]). The implementation shall provide
a definition that is derived from false_type
.
A program may specialize this template to derive from true_type
for a user-defined type T
that does not have a nested executor_type
but nonetheless can be constructed with an executor where either:
— the first argument of a constructor has type executor_type
and the second argument has type Executor
;
or
— the last argument of a constructor has type Executor
.
Uses-executor construction with executor Executor
refers to the construction of
an object obj
of type
T
, using constructor arguments
v1,
v2,
..., vN
of types V1,
V2,
..., VN
,
respectively, and an executor ex
of type Executor
, according
to the following rules:
— if uses_executor<T, Executor>::value
is false and is_constructible<T, V1, V2, ..., VN>::value
is true, then obj
is initialized as obj(v1, v2, ..., vN)
;
— otherwise, if uses_executor<T, Executor>::value
is true and is_constructible<T, executor_arg_t, Executor, V1, V2, ..., VN>::value
is true, then obj
is initialized as obj(executor_arg,
ex,
v1,
v2,
..., vN)
;
— otherwise, if uses_executor<T, Executor>::value
is true and is_constructible<T, V1, V2, ..., VN, Executor>::value
is true, then obj
is initialized as obj(v1, v2, ..., vN, ex)
;
— otherwise, the request for uses-executor construction is ill-formed. [Note:
An error will result if uses_executor<T, Executor>::value
is true but the specific constructor does not take an executor. This definition
prevents a silent failure to pass the executor to an element. —end
note]
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T, class Executor = system_executor> struct associated_executor { typedef see below type; static type get(const T& t, const Executor& e = Executor()) noexcept; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
A program may specialize this traits type if the T
template parameter in the specialization is a user-defined type. The template
parameter Executor
shall
be a type meeting Executor requirements.
Specializations of associated_executor
shall satisfy the requirements in the table below. In this table, X
is a specialization of associated_executor
for the template parameter
T
; t
is a const reference to an object of type T
;
and e
is an object of type
Executor
.
Table 4. associated_executor specialization requirements
Expression |
Return type |
Note |
---|---|---|
|
A type meeting Executor requirements. | |
|
|
Shall not exit via an exception. |
|
|
Shall not exit via an exception. |
template<class T> associated_executor_t<T> get_associated_executor(const T& t);
Returns:
associated_executor<T>::get(t)
.
template<class T, class Executor> associated_executor_t<T, Executor> get_associated_executor(const T& t, const Executor& ex);
Returns:
associated_executor<T, Executor>::get(t, ex)
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class T, class ExecutionContext> associated_executor_t<T, typename ExecutionContext::executor_type> get_associated_executor(const T& t, ExecutionContext& ctx);
Returns:
associated_executor<T, typename ExecutionContext::executor_type>::get(t, ctx.get_executor())
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T, class Executor> class executor_wrapper { public: // types: typedef T wrapped_type; typedef Executor executor_type; typedef see below result_type; // not always defined typedef see below argument_type; // not always defined typedef see below first_argument_type; // not always defined typedef see below second_argument_type; // not always defined // construct / copy / destroy: executor_wrapper(T t, const Executor& ex); executor_wrapper(const executor_wrapper& other) = default; executor_wrapper(executor_wrapper&& other) = default; template<class U, class OtherExecutor> executor_wrapper(const executor_wrapper<U, OtherExecutor>& other); template<class U, class OtherExecutor> executor_wrapper(executor_wrapper<U, OtherExecutor>&& other); template<class U, class OtherExecutor> executor_wrapper(executor_arg_t, const Executor& ex, const executor_wrapper<U, OtherExecutor>& other); template<class U, class OtherExecutor> executor_wrapper(executor_arg_t, const Executor& ex, executor_wrapper<U, OtherExecutor>&& other); ~executor_wrapper(); // executor wrapper access: T& unwrap() noexcept; const T& unwrap() const noexcept; executor_type get_executor() const noexcept; // executor wrapper invocation: template<class... Args> result_of_t<cv T&(Args&&...)> operator()(Args&&... args) cv; private: Executor ex_; // exposition only T wrapped_; // exposition only }; template<class T, class Executor, class Signature> struct handler_type<executor_wrapper<T, Executor>, Signature> { typedef executor_wrapper<handler_type_t<T, Signature>, Executor> type; }; template<class T, class Executor> class async_result<executor_wrapper<T, Executor>>; template<class T, class Executor, class Alloc> struct associated_allocator<executor_wrapper<T, Executor>, Alloc>; template<class T, class Executor, class Executor1> struct associated_executor<executor_wrapper<T, Executor>, Executor1>; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
executor_wrapper<T, Executor>
is a wrapper around an object or function
of type T
, and an executor
object of type Executor
satisfying
Executor requirements.
executor_wrapper<T, Executor>
has a weak result type (C++ Std, [func.require]).
If T
is a function type,
result_type
shall be a synonym
for the return type of T
.
The template instantiation executor_wrapper<T,
Executor>
shall define a nested type named argument_type
as a synonym for T1
only
if the type T
is any of the
following:
— a function type or a pointer to function type taking one argument of type
T1
— a pointer to member function R
T0::f
cv
(where cv
represents the member function’s
cv-qualifiers); the type T1
is cv
T0*
— a class type with a member type argument_type
;
the type T1
is T::argument_type
.
The template instantiation executor_wrapper<T,
Executor>
shall define two nested types named first_argument_type
and second_argument_type
as synonyms for T1
and T2
, respectively, only if the type T
is any of the following:
— a function type or a pointer to function type taking two arguments of types
T1
and T2
— a pointer to member function R
T0::f(T2)
cv
(where cv
represents the member function’s
cv-qualifiers); the type T1
is cv
T0*
— a class type with member types first_argument_type
and second_argument_type
;
the type T1
is T::first_argument_type
.
and the type T2
is T::second_argument_type
.
executor_wrapper(T t, const Executor& ex);
Effects: Constructs an object of type
executor_wrapper<T, Executor>
. Initializesex_
with the valueex
. Ifuses_executor<T, Executor>::value
is true, performs uses-executor construction to initializewrapped_
withwrapped_(executor_arg, ex_, std::move(t))
; otherwise, initializeswrapped_
withwrapped_(std::move(t))
.
template<class U, class OtherExecutor> executor_wrapper(const executor_wrapper<U, OtherExecutor>& other);
Requires:
U
isT
or convertible toT
.OtherExecutor
isExecutor
or convertible toExecutor
.
Effects: Constructs an object of type
executor_wrapper<T, Executor>
. Initializesex_
withother.get_executor()
. Ifuses_executor<T, Executor>::value
is true, performs uses-executor construction to initializewrapped_
withwrapped_(executor_arg, ex_, other.unwrap())
; otherwise, initializeswrapped_
withwrapped_(other.unwrap())
.
template<class U, class OtherExecutor> executor_wrapper(executor_wrapper<U, OtherExecutor>&& other);
Requires:
U
isT
or convertible toT
.OtherExecutor
isExecutor
or convertible toExecutor
.
Effects: Constructs an object of type
executor_wrapper<T, Executor>
. Initializesex_
withother.get_executor()
. Ifuses_executor<T, Executor>::value
is true, performs uses-executor construction to initializewrapped_
withwrapped_(executor_arg, ex_, std::move(other.unwrap()))
; otherwise, initializeswrapped_
withwrapped_(std::move(other.unwrap()))
.
template<class U, class OtherExecutor> executor_wrapper(executor_arg_t, const Executor& ex, const executor_wrapper<U, OtherExecutor>& other);
Requires:
U
isT
or convertible toT
.
Effects: Constructs an object of type
executor_wrapper<T, Executor>
. Initializesex_
withex
. Ifuses_executor<T, Executor>::value
is true, performs uses-executor construction to initializewrapped_
withwrapped_(executor_arg, ex_, other.unwrap())
; otherwise, initializeswrapped_
withwrapped_(other.unwrap())
.
template<class U, class OtherExecutor> executor_wrapper(executor_arg_t, const Executor& ex, executor_wrapper<U, OtherExecutor>&& other);
Requires:
U
isT
or convertible toT
.
Effects: Constructs an object of type
executor_wrapper<T, Executor>
. Initializesex_
withex
. Ifuses_executor<T, Executor>::value
is true, performs uses-executor construction to initializewrapped_
withwrapped_(executor_arg, ex_, std::move(other.unwrap()))
; otherwise, initializeswrapped_
withwrapped_(std::move(other.unwrap()))
.
T& unwrap() noexcept; const T& unwrap() const noexcept;
Returns:
wrapped_
.
executor_type get_executor() const noexcept;
Returns:
executor_
.
template<class... Args> result_of_t<cv T&(Args&&...)> operator()(Args&&... args) cv;
Returns:
INVOKE
(unwrap(), forward<Args>(args)...)
(C++ Std, [func.require]).
Remarks:
operator()
is described for exposition only. Implementations are not required to provide an actualexecutor_wrapper::operator()
. Implementations are permitted to supportexecutor_wrapper
function invocation through multiple overloaded operators or through other means.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T, class Executor> class async_result<executor_wrapper<T, Executor>> { public: typedef typename async_result<T>::type type; explicit async_result(executor_wrapper<T, Executor>& wrapper); async_result(const async_result&) = delete; async_result& operator=(const async_result&) = delete; type get(); private: async_result<T> wrapped_; // exposition only }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
The implementation shall provide a specialization of async_result
that meets the async_result
specialization requirements.
explicit async_result(executor_wrapper<T, Executor>& wrapper);
Effects: Initializes
wrapped_
withwrapped_(wrapper.unwrap())
.
type get();
Returns:
wrapped_.get()
.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T, class Executor, class Alloc> struct associated_allocator<executor_wrapper<T, Executor>, Alloc> { typedef associated_allocator_t<T, Alloc> type; static type get(const executor_wrapper<T, Executor>& w, const Alloc& a = Alloc()) noexcept; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
The implementation shall provide a specialization of associated_allocator
that meets the associated_allocator
specialization requirements.
static type get(const executor_wrapper<T, Executor>& w, const Alloc& a = Alloc()) noexcept;
Returns:
associated_allocator<T, Alloc>::get(w.unwrap(), a)
.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class T, class Executor, class Executor1> struct associated_executor<executor_wrapper<T, Executor>, Executor1> { typedef Executor type; static type get(const executor_wrapper<T, Executor>& w, const Executor1& e = Executor1()) noexcept; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
The implementation shall provide a specialization of associated_executor
that meets the associated_executor
specialization requirements.
static type get(const executor_wrapper<T, Executor>& w, const Executor1& e = Executor1()) noexcept;
Returns:
w.get_executor()
.
template<class Executor, class T> executor_wrapper<decay_t<T>, Executor> wrap(const Executor& ex, T&& t);
Returns:
executor_wrapper<decay_t<T>, Executor>(forward<T>(t), ex)
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class ExecutionContext, class CompletionToken> executor_wrapper<decay_t<T>, typename ExecutionContext::executor_type> wrap(ExecutionContext& ctx, T&& t);
Returns:
executor_wrapper<decay_t<T>, typename ExecutionContext::executor_type>(forward<T>(t), ctx.get_executor())
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Executor> class executor_work { public: // types: typedef Executor executor_type; // construct / copy / destroy: explicit executor_work(const executor_type& ex) noexcept; executor_work(const executor_work& other) noexcept; executor_work(executor_work&& other) noexcept; executor_work operator=(const executor_type&) = delete; ~executor_work(); // executor work observers: executor_type get_executor() const noexcept; bool owns_work() const noexcept; // executor work modifiers: void reset() noexcept; private: Executor ex_; // exposition only bool owns_; // exposition only }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
explicit executor_work(const executor_type& ex) noexcept;
Effects: Constructs an object of class
executor_work
, initializingex_
withex
, and then performingex_.on_work_started()
.
Postconditions:
ex == ex_
andowns_ == true
.
executor_work(const executor_work& other) noexcept;
Effects: Constructs an object of class
executor_work
, initializingex_
withother.ex
. Ifother.owns_ == true
, performsex_.on_work_started()
.
Postconditions:
ex == other.ex_
andowns_ == other.owns_
.
executor_work(executor_work&& other) noexcept;
Effects: Constructs an object of class
executor_work
, initializingex_
withother.ex
andowns_
withother.owns_
.
Postconditions:
ex
is equal to the prior value ofother.ex_
,owns_
is equal to the prior value ofother.owns_
, andother.owns_ == false
.
executor_type get_executor() const noexcept;
Returns:
ex_
.
bool owns_work() const noexcept;
Returns:
owns_
.
template<class Executor> executor_work<Executor> make_work(const Executor& ex);
Returns:
executor_work<Executor>(ex)
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class ExecutionContext> executor_work<typename ExecutionContext::executor_type> make_work(ExecutionContext& ctx);
Returns: An object of type
executor_work<typename ExecutionContext::executor_type>
initialized with the result ofctx.get_executor()
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
template<class T> executor_work<associated_executor_t<T>> make_work(const T& t);
Returns: An object of type
executor_work<associated_executor_t<T>>
initialized with the result ofassociated_executor<T>::get(t)
.
Remarks: This function shall not participate in overload resolution unless
is_executor<T>::value
is false andis_convertible<T&, execution_context&>::value
is false.
template<class T, class Executor> executor_work<associated_executor_t<T, Executor>> make_work(const T& t, const Executor& ex);
Returns: An object of type
executor_work<associated_executor_t<T, Executor>>
initialized with the result ofassociated_executor<T, Executor>::get(t, ex)
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class T, class ExecutionContext> executor_work<associated_executor_t<T, typename ExecutionContext::executor_type>> make_work(const T& t, ExecutionContext& ctx);
Returns: An object of type
executor_work<associated_executor_t<T, typename ExecutionContext::executor_type>>
initialized with the result ofassociated_executor<T, typename ExecutionContext::executor_type>::get(t, ctx.get_executor())
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
namespace std { namespace experimental { inline namespace concurrency_v1 { class system_executor { public: // executor operations: execution_context& context() noexcept; void on_work_started() noexcept; void on_work_finished() noexcept; template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a); template<class Func, class Alloc> void post(Func&& f, const Alloc& a); template<class Func, class Alloc> void defer(Func&& f, const Alloc& a); }; bool operator==(const system_executor&, const system_executor&) noexcept; bool operator!=(const system_executor&, const system_executor&) noexcept; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
Class system_executor
is
a DefaultConstructible
type
(C++ Std, [defaultconstructible]) satisfying Executor
requirements. It represents a set of rules where function objects
are permitted to execute on any thread.
To satisfy the executor requirements for the post
and defer
member functions,
the system executor may allocate thread
objects to run the submitted function objects. If std::exit
is called, and there remain unexecuted functions objects that have been submitted
using post
or defer
, the implementation shall discard
these function objects without calling them.
execution_context& context() noexcept;
Returns: A reference to a static-duration object of a type derived from
execution_context
.
void on_work_started() noexcept;
Effects: Does nothing.
void on_work_finished() noexcept;
Effects: Does nothing.
template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a);
Effects: Calls
DECAY_COPY
(forward<Func>(f))()
.
template<class Func, class Alloc> void post(Func&& f, const Alloc& a);
Effects: Calls
DECAY_COPY
(forward<Func>(f))()
as if in a thread of execution represented by athread
object, with the call toDECAY_COPY()
being evaluated in the thread that calledpost
. Any exception propagated from the execution ofDECAY_COPY
(forward<Func>(f))()
shall result in a call tostd::terminate
.
template<class Func, class Alloc> void defer(Func&& f, const Alloc& a);
Effects: Calls
DECAY_COPY
(forward<Func>(f))()
as if in a thread of execution represented by athread
object, with the call toDECAY_COPY()
being evaluated in the thread that calleddefer
. Any exception propagated from the execution ofDECAY_COPY
(forward<Func>(f))()
shall result in a call tostd::terminate
.
namespace std { namespace experimental { inline namespace concurrency_v1 { class bad_executor : public exception { public: // constructor: bad_executor() noexcept; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
An exception of type bad_executor
is thrown by executor
member
functions dispatch
, post
and defer
when the executor object has no target.
namespace std { namespace experimental { inline namespace concurrency_v1 { class executor { public: // construct / copy / destroy: executor() noexcept; executor(nullptr_t) noexcept; executor(const executor& e) noexcept; executor(executor&& e) noexcept; template<class Executor> executor(Executor e); template<class Executor, class Alloc> executor(allocator_arg_t, const Alloc& a, Executor e); executor& operator=(const executor& e) noexcept; executor& operator=(executor&& e) noexcept; executor& operator=(nullptr_t) noexcept; template<class Executor> executor& operator=(Executor e); ~executor(); // executor modifiers: void swap(executor& other) noexcept; template<class Executor, class Alloc> void assign(Executor e, const Alloc& e); // executor operations: execution_context& context() noexcept; void on_work_started() noexcept; void on_work_finished() noexcept; template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a); template<class Func, class Alloc> void post(Func&& f, const Alloc& a); template<class Func, class Alloc> void defer(Func&& f, const Alloc& a); // executor capacity: explicit operator bool() const noexcept; // executor target access: const type_info& target_type() const noexcept; template<class Executor> Executor* target() noexcept; template<class Executor> const Executor* target() const noexcept; }; template<> struct is_executor<executor> : true_type {}; // executor comparisons: bool operator==(const executor& a, const executor& b) noexcept; bool operator==(const executor& e, nullptr_t) noexcept; bool operator==(nullptr_t, const executor& e) noexcept; bool operator!=(const executor& a, const executor& b) noexcept; bool operator!=(const executor& e, nullptr_t) noexcept; bool operator!=(nullptr_t, const executor& e) noexcept; // executor specialized algorithms: void swap(executor& a, executor& b) noexcept; } // inline namespace concurrency_v1 } // namespace experimental template<class Alloc> struct uses_allocator<std::experimental::concurrency_v1::executor, Alloc> : true_type {}; } // namespace std
The executor
class provides
a polymorphic wrapper for types that satisfy the Executor
requirements. The target object is the executor
object that is held by the wrapper. The executor
type itself meets the requirements for an Executor.
[Note: To meet the noexcept
requirements for executor copy constructors and move constructors, implementations
may share a target between two or more executor
objects. —end note]
executor() noexcept;
Postconditions:
!*this
.
executor(nullptr_t) noexcept;
Postconditions:
!*this
.
executor(const executor& e) noexcept;
Postconditions:
!*this
if!e
; otherwise,*this
targetse.target()
or a copy ofe.target()
.
executor(executor&& e) noexcept;
Effects: If
!e
,*this
has no target; otherwise, movese.target()
or move-constructs the target ofe
into the target of*this
, leavinge
in a valid state with an unspecified value.
template<class Executor> executor(Executor e);
Requires:
Executor
shall satisfy the Executor requirements.
Effects:
*this
targets a copy ofe
initialized withstd::move(e)
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class Executor, class Alloc> executor(allocator_arg_t, const Alloc& a, Executor e);
Requires:
Executor
shall satisfy the Executor requirements.Allocator
conforms to theAllocator
requirements (C++ Std, [allocator.requirements]).
Effects:
*this
targets a copy ofe
initialized withstd::move(e)
.
A copy of the allocator argument is used to allocate memory, if necessary, for the internal data structures of the constructed
executor
object.
executor& operator=(const executor& e) noexcept;
Effects:
executor(e).swap(*this)
.
Returns:
*this
.
executor& operator=(executor&& e) noexcept;
Effects: Replaces the target of
*this
with the target ofe
, leavinge
in a valid state with an unspecified value.
Returns:
*this
.
executor& operator=(nullptr_t) noexcept;
Effects:
executor(nullptr).swap(*this)
.
Returns:
*this
.
template<class Executor> executor& operator=(Executor e);
Effects:
executor(std::move(e)).swap(*this)
.
Returns:
*this
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
~executor();
Effects: If
*this != nullptr
, releases shared ownership of, or destroys, the target of*this
.
void swap(executor& other) noexcept;
Effects: Interchanges the targets of
*this
andother
.
template<class Executor, class Alloc> void assign(Executor e, const Alloc& e);
Effects:
executor(allocator_arg, a, std::move(e)).swap(*this)
.
execution_context& context() noexcept;
Returns:
e.context()
, wheree
is the target object of*this
.
void on_work_started() noexcept;
Effects:
e.on_work_started()
, wheree
is the target object of*this
.
void on_work_finished() noexcept;
Effects:
e.on_work_finished()
, wheree
is the target object of*this
.
template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a);
Effects:
e.dispatch(g, a)
, wheree
is the target object of*this
, andg
is a function object of unspecified type that, when called asg()
, performsDECAY_COPY(f)()
.
template<class Func, class Alloc> void post(Func&& f, const Alloc& a);
Effects:
e.post(g, a)
, wheree
is the target object of*this
, andg
is a function object of unspecified type that, when called asg()
, performsDECAY_COPY(f)()
.
template<class Func, class Alloc> void defer(Func&& f, const Alloc& a);
Effects:
e.defer(g, a)
, wheree
is the target object of*this
, andg
is a function object of unspecified type that, when called asg()
, performsDECAY_COPY(f)()
.
explicit operator bool() const noexcept;
Returns:
true
if*this
has a target, otherwisefalse
,
const type_info& target_type() const noexcept;
Returns: If
*this
has a target of typeT
,typeid(T)
; otherwise,typeid(void)
.
template<class Executor> Executor* target() noexcept; template<class Executor> const Executor* target() const noexcept;
Requires:
Executor
shall satisfy the Executor requirements.
Returns: If
target_type() == typeid(Executor)
a pointer to the stored executor target; otherwise a null pointer.
bool operator==(const executor& a, const executor& b) noexcept;
Returns:
—true
if!a
and!b
;
—true
ifa
andb
share a target;
—true
ife
andf
are the same type ande == f
, wheree
is the target object ofa
andf
is the target object ofb
;
— otherwisefalse
.
bool operator==(const executor& e, nullptr_t) noexcept; bool operator==(nullptr_t, const executor& e) noexcept;
Returns:
!e
.
bool operator!=(const executor& a, const executor& b) noexcept;
Returns:
!(a == b)
.
bool operator!=(const executor& e, nullptr_t) noexcept; bool operator!=(nullptr_t, const executor& e) noexcept;
Returns:
(bool) e
.
template<class CompletionToken> auto dispatch(CompletionToken&& token);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex
by performingget_associated_executor(handler)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Performsex.dispatch(std::move(handler), alloc)
.
Returns:
result.get()
.
template<class Executor, class CompletionToken> auto dispatch(const Executor& ex, CompletionToken&& token);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex1
by performingget_associated_executor(handler)
.
— Creates a work objectw
by performingmake_work(ex1)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Constructs a function objectf
with a function call operator that performsex1.dispatch(std::move(handler), alloc)
followed byw.reset()
.
— Performsex.dispatch(std::move(f), alloc)
.
Returns:
result.get()
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class ExecutionContext, class CompletionToken> auto dispatch(ExecutionContext& ctx, CompletionToken&& token);
Returns:
std::experimental::concurrency_v1::dispatch(ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
template<class CompletionToken> auto post(CompletionToken&& token);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex
by performingget_associated_executor(handler)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Performsex.post(std::move(handler), alloc)
.
Returns:
result.get()
.
template<class Executor, class CompletionToken> auto post(const Executor& ex, CompletionToken&& token);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex1
by performingget_associated_executor(handler)
.
— Creates a work objectw
by performingmake_work(ex1)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Constructs a function objectf
with a function call operator that performsex1.dispatch(std::move(handler), alloc)
followed byw.reset()
.
— Performsex.post(std::move(f), alloc)
.
Returns:
result.get()
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class ExecutionContext, class CompletionToken> auto post(ExecutionContext& ctx, CompletionToken&& token);
Returns:
std::experimental::concurrency_v1::post(ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
template<class CompletionToken> auto defer(CompletionToken&& token);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex
by performingget_associated_executor(handler)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Performsex.defer(std::move(handler), alloc)
.
Returns:
result.get()
.
template<class Executor, class CompletionToken> auto defer(const Executor& ex, CompletionToken&& token);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex1
by performingget_associated_executor(handler)
.
— Creates a work objectw
by performingmake_work(ex1)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Constructs a function objectf
with a function call operator that performsex1.dispatch(std::move(handler), alloc)
followed byw.reset()
.
— Performsex.defer(std::move(f), alloc)
.
Returns:
result.get()
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class ExecutionContext, class CompletionToken> auto defer(ExecutionContext& ctx, CompletionToken&& token);
Returns:
std::experimental::concurrency_v1::defer(ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Executor> class strand; template<class Executor> bool operator==(const strand<Executor>& a, const strand<Executor>& b); template<class Executor> bool operator!=(const strand<Executor>& a, const strand<Executor>& b); template<class Executor> struct is_executor<strand<Executor>> : true_type {}; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Executor> class strand { public: // types: typedef Executor inner_executor_type; // construct / copy / destroy: strand(); explicit strand(Executor ex); strand(const strand& other); strand(strand&& other); template<class OtherExecutor> strand(const strand<OtherExecutor>& other); template<class OtherExecutor> strand(strand<OtherExecutor>&& other); strand& operator=(const strand& other); strand& operator=(strand&& other); template<class OtherExecutor> strand& operator=(const strand<OtherExecutor>& other); template<class OtherExecutor> strand& operator=(strand<OtherExecutor>&& other); ~strand(); // strand operations: inner_executor_type get_inner_executor() const noexcept; bool running_in_this_thread() const noexcept; execution_context& context() noexcept; void on_work_started() noexcept; void on_work_finished() noexcept; template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a); template<class Func, class Alloc> void post(Func&& f, const Alloc& a); template<class Func, class Alloc> void defer(Func&& f, const Alloc& a); private: Executor inner_ex_; // exposition only }; bool operator==(const strand<Executor>& a, const strand<Executor>& b); bool operator!=(const strand<Executor>& a, const strand<Executor>& b); } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
strand<Executor>
is a wrapper around an object of type Executor
satisfying Executor requirements.
strand<Executor>
satisfies the Executor requirements.
A strand provides guarantees of ordering and non-concurrency. Given:
— strand objects s1
and s2
such that s1
== s2
— a function object f1
added
to the strand s1
using post
or defer
,
or using dispatch
when s1.running_in_this_thread() == false
— a function object f2
added
to the strand s2
using post
or defer
,
or using dispatch
when s2.running_in_this_thread() == false
then the implementation shall invoke f1
and f2
such that:
— the invocation of f1
is not
concurrent with the invocation of f2
— the invocation of f1
synchronizes
with the invocation of f2
.
Furthermore, if the addition of f1
happens before the addition of f2
,
then the invocation of f1
happens before the invocation of f2
.
The strand copy constructors, comparison operators, and member functions shall not introduce data races as a result of concurrent calls to those functions from different threads.
If any function f
executed
by the strand throws an exception, the subsequent strand state shall be as
if f
had exited without throwing
an exception.
strand();
Effects: Constructs an object of class
strand<Executor>
that represents a unique ordered, non-concurrent state. Initializesinner_ex_
withinner_ex_()
.
Remarks: This overload shall not participate in overload resolution unless
Executor
satisfies theDefaultConstructible
requirements (C++ Std, [defaultconstructible]).
explicit strand(Executor ex);
Effects: Constructs an object of class
strand<Executor>
that represents a unique ordered, non-concurrent state. Initializesinner_ex_
withinner_ex_(ex)
.
strand(const strand& other);
Effects: Constructs an object of class
strand<Executor>
. Initalizesinner_ex_
withinner_ex_(other.inner_ex_)
.
Postconditions:
—*this == other
—get_inner_executor() == other.get_inner_executor()
strand(strand&& other);
Effects: Constructs an object of class
strand<Executor>
. Initalizesinner_ex_
withinner_ex_(std::move(other.inner_ex_))
.
Postconditions:
—*this
is equal to the prior value ofother
—get_inner_executor() == other.get_inner_executor()
template<class OtherExecutor> strand(const strand<OtherExecutor>& other);
Requires:
OtherExecutor
is convertible toExecutor
.
Effects: Constructs an object of class
strand<Executor>
. Initalizesinner_ex_
withinner_ex_(other.inner_ex_)
.
Postconditions:
*this == other
.
template<class OtherExecutor> strand(strand<OtherExecutor>&& other);
Requires:
OtherExecutor
is convertible toExecutor
.
Effects: Constructs an object of class
strand<Executor>
. Initalizesinner_ex_
withinner_ex_(other.inner_ex_)
.
Postconditions:
*this
is equal to the prior value ofother
.
strand& operator=(const strand& other);
Requires:
Executor
isAssignable
(C++ Std [assignable]).
Postconditions:
—*this == other
—get_inner_executor() == other.get_inner_executor()
Returns:
*this
.
strand& operator=(strand&& other);
Requires:
Executor
isAssignable
(C++ Std [assignable]).
Postconditions:
—*this
is equal to the prior value ofother
—get_inner_executor() == other.get_inner_executor()
Returns:
*this
.
template<class OtherExecutor> strand& operator=(const strand<OtherExecutor>& other);
Requires:
OtherExecutor
is convertible toExecutor
.Executor
isAssignable
(C++ Std [assignable]).
Effects: Equivalent to
strand<Executor>::operator=(Executor(other))
.
Returns:
*this
.
template<class OtherExecutor> strand& operator=(strand<OtherExecutor>&& other);
Requires:
OtherExecutor
is convertible toExecutor
.Executor
isAssignable
(C++ Std [assignable]).
Effects: Equivalent to
strand<Executor>::operator=(Executor(std::move(other)))
.
Returns:
*this
.
~strand();
Effects: Destroys an object of class
strand<Executor>
. Function objects that were added to the strand but have not yet been executed shall still be executed in a way that meets the guarantees of ordering and non-concurrency.
inner_executor_type get_inner_executor() const noexcept;
Returns:
inner_ex_
.
bool running_in_this_thread() const noexcept;
Returns:
true
if the current thread of execution is invoking a function that was submitted to the strand, or to any other strand objects
such thats == *this
, usingdispatch
,post
ordefer
; otherwisefalse
.
execution_context& context() noexcept;
Returns:
inner_ex_.context()
.
void on_work_started() noexcept;
Effects: Calls
inner_ex_.on_work_started()
.
void on_work_finished() noexcept;
Effects: Calls
inner_ex_.on_work_finished()
.
template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a);
Effects: If
running_in_this_thread()
is true, callsDECAY_COPY
(forward<Func>(f))()
. Otherwise, requests invocation off
, as if by forwarding the function objectf
and allocatora
to the executorinner_ex_
, such that the guarantees of ordering and non-concurrency are met.
If
f
exits via an exception, and the execution off
is performed in the current thread and beforedispatch
returns, the exception shall propagate to the caller ofdispatch()
template<class Func, class Alloc> void post(Func&& f, const Alloc& a);
Effects: Requests invocation of
f
, as if by forwarding the function objectf
and allocatora
to the executorinner_ex_
, such that the guarantees of ordering and non-concurrency are met.
template<class Func, class Alloc> void defer(Func&& f, const Alloc& a);
Effects: Requests invocation of
f
, as if by forwarding the function objectf
and allocatora
to the executorinner_ex_
, such that the guarantees of ordering and non-concurrency are met.
#include <chrono> namespace std { namespace experimental { inline namespace concurrency_v1 { // dispatch_at: template<class Clock, class Duration, class... CompletionTokens> auto dispatch_at(const chrono::time_point<Clock, Duration>& abs_time, CompletionTokens&&... tokens); template<class Clock, class Duration, class Executor, class... CompletionTokens> auto dispatch_at(const chrono::time_point<Clock, Duration>& abs_time, const Executor& ex, CompletionTokens&&... tokens); template<class Clock, class Duration, class ExecutionContext, class... CompletionTokens> auto dispatch_at(const chrono::time_point<Clock, Duration>& abs_time, ExecutionContext& ctx, CompletionTokens&&... tokens); // post_at: template<class Clock, class Duration, class... CompletionTokens> auto post_at(const chrono::time_point<Clock, Duration>& abs_time, CompletionTokens&&... tokens); template<class Clock, class Duration, class Executor, class... CompletionTokens> auto post_at(const chrono::time_point<Clock, Duration>& abs_time, const Executor& ex, CompletionTokens&&... tokens); template<class Clock, class Duration, class ExecutionContext, class... CompletionTokens> auto post_at(const chrono::time_point<Clock, Duration>& abs_time, ExecutionContext& ctx, CompletionTokens&&... tokens); // defer_at: template<class Clock, class Duration, class... CompletionTokens> auto defer_at(const chrono::time_point<Clock, Duration>& abs_time, CompletionTokens&&... tokens); template<class Clock, class Duration, class Executor, class... CompletionTokens> auto defer_at(const chrono::time_point<Clock, Duration>& abs_time, const Executor& ex, CompletionTokens&&... tokens); template<class Clock, class Duration, class ExecutionContext, class... CompletionTokens> auto defer_at(const chrono::time_point<Clock, Duration>& abs_time, ExecutionContext& ctx, CompletionTokens&&... tokens); // dispatch_after: template<class Rep, class Period, class... CompletionTokens> auto dispatch_after(const chrono::duration<Rep, Period>& rel_time, CompletionTokens&&... token); template<class Rep, class Period, class Executor, class... CompletionTokens> auto dispatch_after(const chrono::duration<Rep, Period>& rel_time, const Executor& ex, CompletionTokens&&... tokens); template<class Rep, class Period, class ExecutionContext, class... CompletionTokens> auto dispatch_after(const chrono::duration<Rep, Period>& rel_time, ExecutionContext& ctx, CompletionTokens&&... tokens); // post_after: template<class Rep, class Period, class... CompletionTokens> auto post_after(const chrono::duration<Rep, Period>& rel_time, CompletionTokens&&... token); template<class Rep, class Period, class Executor, class... CompletionTokens> auto post_after(const chrono::duration<Rep, Period>& rel_time, const Executor& ex, CompletionTokens&&... tokens); template<class Rep, class Period, class ExecutionContext, class... CompletionTokens> auto post_after(const chrono::duration<Rep, Period>& rel_time, ExecutionContext& ctx, CompletionTokens&&... tokens); // defer_after: template<class Rep, class Period, class... CompletionTokens> auto defer_after(const chrono::duration<Rep, Period>& rel_time, CompletionTokens&&... token); template<class Rep, class Period, class Executor, class... CompletionTokens> auto defer_after(const chrono::duration<Rep, Period>& rel_time, const Executor& ex, CompletionTokens&&... tokens); template<class Rep, class Period, class ExecutionContext, class... CompletionTokens> auto defer_after(const chrono::duration<Rep, Period>& rel_time, ExecutionContext& ctx, CompletionTokens&&... tokens); } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
template<class Clock, class Duration, class... CompletionTokens> auto dispatch_at(const chrono::time_point<Clock, Duration>& abs_time, CompletionTokens&&... tokens);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex
by performingget_associated_executor(handler)
.
— Creates a work objectw
by performingmake_work(ex)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Without blocking the current thread of execution, on expiration of the absolute timeout specified byabs_time
performsex.dispatch(std::move(handler), alloc)
followed byw.reset()
.
Returns:
result.get()
.
template<class Clock, class Duration, class Executor, class... CompletionTokens> auto dispatch_at(const chrono::time_point<Clock, Duration>& abs_time, const Executor& ex, CompletionTokens&&... tokens);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Creates a work objectw
by performingmake_work(ex)
.
— Obtains the handler's associated executor objectex1
by performingget_associated_executor(handler)
.
— Creates a work objectw1
by performingmake_work(ex1)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Constructs a function objectf
with a function call operator that performsex1.dispatch(std::move(handler), alloc)
followed byw1.reset()
.
— Without blocking the current thread of execution, on expiration of the absolute timeout specified byabs_time
performsex.dispatch(std::move(f), alloc)
followed byw.reset()
.
Returns:
result.get()
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class Clock, class Duration, class ExecutionContext, class... CompletionTokens> auto dispatch_at(const chrono::time_point<Clock, Duration>& abs_time, ExecutionContext& ctx, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::dispatch_at(ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
template<class Clock, class Duration, class... CompletionTokens> auto post_at(const chrono::time_point<Clock, Duration>& abs_time, CompletionTokens&&... tokens);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex
by performingget_associated_executor(handler)
.
— Creates a work objectw
by performingmake_work(ex)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Without blocking the current thread of execution, on expiration of the absolute timeout specified byabs_time
performsex.post(std::move(handler), alloc)
followed byw.reset()
.
Returns:
result.get()
.
template<class Clock, class Duration, class Executor, class... CompletionTokens> auto post_at(const chrono::time_point<Clock, Duration>& abs_time, const Executor& ex, CompletionTokens&&... tokens);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Creates a work objectw
by performingmake_work(ex)
.
— Obtains the handler's associated executor objectex1
by performingget_associated_executor(handler)
.
— Creates a work objectw1
by performingmake_work(ex1)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Constructs a function objectf
with a function call operator that performsex1.dispatch(std::move(handler), alloc)
followed byw1.reset()
.
— Without blocking the current thread of execution, on expiration of the absolute timeout specified byabs_time
performsex.post(std::move(f), alloc)
followed byw.reset()
.
Returns:
result.get()
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class Clock, class Duration, class ExecutionContext, class... CompletionTokens> auto post_at(const chrono::time_point<Clock, Duration>& abs_time, ExecutionContext& ctx, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::post_at(ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
template<class Clock, class Duration, class... CompletionTokens> auto defer_at(const chrono::time_point<Clock, Duration>& abs_time, CompletionTokens&&... tokens);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Obtains the handler's associated executor objectex
by performingget_associated_executor(handler)
.
— Creates a work objectw
by performingmake_work(ex)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Without blocking the current thread of execution, on expiration of the absolute timeout specified byabs_time
performsex.defer(std::move(handler), alloc)
followed byw.reset()
.
Returns:
result.get()
.
template<class Clock, class Duration, class Executor, class... CompletionTokens> auto defer_at(const chrono::time_point<Clock, Duration>& abs_time, const Executor& ex, CompletionTokens&&... tokens);
Let the type
Handler
be the handler function object type determined by performinghandler_type_t<CompletionToken, void()>
.
Requires: The type
Handler
must satisfy theMoveConstructible
requirements (C++ Std, [moveconstructible]) and be callable with zero arguments.
Effects:
— Constructs a function objecthandler
of typeHandler
, initialized withhandler(forward<CompletionToken>(token))
.
— Constructs an objectresult
of typeasync_result<Handler>
, initializing the object asresult(handler)
.
— Creates a work objectw
by performingmake_work(ex)
.
— Obtains the handler's associated executor objectex1
by performingget_associated_executor(handler)
.
— Creates a work objectw1
by performingmake_work(ex1)
.
— Obtains the handler's associated allocator objectalloc
by performingget_associated_allocator(handler)
.
— Constructs a function objectf
with a function call operator that performsex1.dispatch(std::move(handler), alloc)
followed byw1.reset()
.
— Without blocking the current thread of execution, on expiration of the absolute timeout specified byabs_time
performsex.defer(std::move(f), alloc)
followed byw.reset()
.
Returns:
result.get()
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class Clock, class Duration, class ExecutionContext, class... CompletionTokens> auto defer_at(const chrono::time_point<Clock, Duration>& abs_time, ExecutionContext& ctx, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::defer_at(ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
template<class Rep, class Period, class... CompletionTokens> auto dispatch_after(const chrono::duration<Rep, Period>& rel_time, CompletionTokens&&... token);
Returns:
std::experimental::concurrency_v1::dispatch_at(chrono::steady_clock::now() + rel_time, forward<CompletionToken>(token))
.
template<class Rep, class Period, class Executor, class... CompletionTokens> auto dispatch_after(const chrono::duration<Rep, Period>& rel_time, const Executor& e, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::dispatch_at(chrono::steady_clock::now() + rel_time, e, forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class Rep, class Period, class ExecutionContext, class... CompletionTokens> auto dispatch_after(const chrono::duration<Rep, Period>& rel_time, ExecutionContext& c, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::dispatch_at(chrono::steady_clock::now() + rel_time, ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
template<class Rep, class Period, class... CompletionTokens> auto post_after(const chrono::duration<Rep, Period>& rel_time, CompletionTokens&&... token);
Returns:
std::experimental::concurrency_v1::post_at(chrono::steady_clock::now() + rel_time, forward<CompletionToken>(token))
.
template<class Rep, class Period, class Executor, class... CompletionTokens> auto post_after(const chrono::duration<Rep, Period>& rel_time, const Executor& e, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::post_at(chrono::steady_clock::now() + rel_time, e, forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class Rep, class Period, class ExecutionContext, class... CompletionTokens> auto post_after(const chrono::duration<Rep, Period>& rel_time, ExecutionContext& c, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::post_at(chrono::steady_clock::now() + rel_time, ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
template<class Rep, class Period, class... CompletionTokens> auto defer_after(const chrono::duration<Rep, Period>& rel_time, CompletionTokens&&... token);
Returns:
std::experimental::concurrency_v1::defer_at(chrono::steady_clock::now() + rel_time, forward<CompletionToken>(token))
.
template<class Rep, class Period, class Executor, class... CompletionTokens> auto defer_after(const chrono::duration<Rep, Period>& rel_time, const Executor& e, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::defer_at(chrono::steady_clock::now() + rel_time, e, forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_executor<Executor>::value
is true.
template<class Rep, class Period, class ExecutionContext, class... CompletionTokens> auto defer_after(const chrono::duration<Rep, Period>& rel_time, ExecutionContext& c, CompletionTokens&&... tokens);
Returns:
std::experimental::concurrency_v1::defer_at(chrono::steady_clock::now() + rel_time, ctx.get_executor(), forward<CompletionToken>(token))
.
Remarks: This function shall not participate in overload resolution unless
is_convertible<ExecutionContext&, execution_context&>::value
is true.
#include <future> namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Allocator = allocator<void>> class use_future_t; constexpr use_future_t<> use_future = use_future_t<>(); template<class Allocator, class R, class... Args> struct handler_type<use_future_t<Allocator>, R(Args...)>; template<class R, class... Args> class async_result<packaged_task<R(Args...)>>; template<class Signature, class Alloc> class packaged_handler; template<class Signature, class Alloc> class async_result<packaged_handler<Signature, Alloc>>; template<class Func, class Alloc = allocator<void>> class packaged_token; template<class Func, class Alloc, class R, class... Args> struct handler_type<packaged_token<Func, Alloc>, R(Args...)>; template<class Func, class Alloc = allocator<void>> packaged_token<decay_t<Func>, Alloc> package(Func&& f, const Alloc& a = Alloc()); } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Allocator = allocator<void>> class use_future_t { public: // use_future_t types: typedef Allocator allocator_type; // use_future_t members: constexpr use_future_t() noexcept; explicit use_future_t(const Allocator& a) noexcept; template<class OtherAllocator> use_future_t<OtherAllocator> operator[](const OtherAllocator& a) const noexcept; allocator_type get_allocator() const noexcept; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
The class template use_future_t
defines a set of completion token types
for use with asynchronous operations.
constexpr use_future_t() noexcept;
Effects: Constructs a
use_future_t
with default-constructed allocator.
explicit use_future_t(const Allocator& a) noexcept;
Effects: Constructs an object of type
use_future_t
with post-conditionget_allocator() == a
.
template<class OtherAllocator> use_future_t<OtherAllocator> operator[](const OtherAllocator& a) const noexcept;
Returns: A
use_future_t
object whereget_allocator() == a
.
allocator_type get_allocator() const noexcept;
Returns: The associated allocator object.
template<class Allocator, class R, class... Args> struct handler_type<use_future_t<Allocator>, R(Args...)> { typedef see below type; };
An object t1
of the nested
function object type type
is an asynchronous provider with an associated shared state (C++Std, [futures.state]).
The type type
provides
type::operator()
such that the expression t1(declval<Args>()...)
is well formed.
The implementation shall specialize associated_executor
for type
. For function
objects executed using the associated executor's dispatch()
, post()
or defer()
functions, any exception thrown is caught
by the executor and stored in the associated shared state.
The implementation shall specialize async_result
for type
such that, when
an async_result
object
r1
is constructed from
t1
, the expression r1.get()
returns a future with the same shared
state as t1
.
The semantics of async_result::type
and type::operator()
are defined in the table below. In this table, N
is
the value of sizeof...(Args)
;
let i
be in the range [0
..N
)
and let Ti
be the i
th
type in Args
; let Ui
be decay<Ti>::type
for each
type Ti
in Args
;
and let ai
be the i
th
argument to type::operator()
.
Table 5. handler_type<use_future_t<Allocator>, R(Args...)>::type semantics
|
|
|
|
---|---|---|---|
0 |
|
Makes the shared state ready. | |
1 |
|
|
If |
1 |
|
|
If |
1 |
all other types |
|
Atomically stores |
2 |
|
|
If |
2 |
|
|
If |
2 |
all other types |
|
Atomically stores the result of |
>2 |
|
|
If |
>2 |
|
|
If |
>2 |
all other types |
|
Atomically stores the result of |
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class R, class... Args> class async_result<packaged_task<R(Args...)>> { public: typedef future<R> type; async_result(packaged_task<R(Args...)>& t); async_result(const async_result&) = delete; async_result& operator=(const async_result&) = delete; type get(); private: type future_; // exposition only }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
The implementation shall provide a specialization of async_result
that meets the async_result
specialization requirements.
async_result(packaged_task<R(Args...)>& t);
Effects: Initializes
future_
witht.get_future()
.
type get();
Returns:
std::move(future_)
.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Signature, class Alloc> class packaged_handler : public packaged_task<Signature> { public: // packaged_handler types: typedef Alloc allocator_type; // packaged_handler constructors: template<class Func> explicit packaged_handler(packaged_token<Func, Alloc>&& token); // packaged_handler operations: allocator_type get_allocator() const noexcept; private: Alloc allocator_; // exposition only }; template<class Signature, class Alloc> class async_result<packaged_handler<Signature, Alloc>>; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
template<class Func> explicit packaged_handler(packaged_token<Func, Alloc>&& token);
Effects: Constructs an object of class
packaged_handler<Signature, Alloc>
, initializing the base class withpackaged_task<Signature>(std::move(token.f_))
and initializingallocator_
withtoken.allocator_
.
allocator_type get_allocator() const noexcept;
Returns:
allocator_
.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Signature, class Alloc> class async_result<packaged_handler<Signature, Alloc>> : public async_result<packaged_task<Signature>> { public: explicit async_result(packaged_handler<Signature, Alloc>& h); }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
The implementation shall provide a specialization of async_result
that meets the async_result
specialization requirements.
explicit async_result(packaged_handler<Signature, Alloc>& h);
Effects: Initializes the base class with
h
.
namespace std { namespace experimental { inline namespace concurrency_v1 { template<class Func, class Alloc = allocator<void>> class packaged_token { public: // packaged_token types: typedef Alloc allocator_type; // packaged_token constructors: explicit packaged_token(Func f); packaged_token(Func f, const Alloc& a); // packaged_token operations: allocator_type get_allocator() const noexcept; private: Func f_; // exposition only Alloc allocator_; // exposition only }; template<class Func, class Alloc, class R, class... Args> struct handler_type<packaged_token<Func, Alloc>, R(Args...)> { typedef packaged_handler<result_of_t<Func(Args...)>(Args...), Alloc> type; }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
explicit packaged_token(Func f);
Effects: Constructs an object of class
packaged_token<Func, Alloc>
, initializingf_
withstd::move(f)
and default constructingallocator_
.
packaged_token(Func f, const Alloc& a);
Effects: Constructs an object of class
packaged_token<Func, Alloc>
, initializingf_
withstd::move(f)
andallocator_
witha
.
template<class Func, class Alloc = allocator<void>> packaged_token<decay_t<Func>, Alloc> package(Func&& f, const Alloc& a = Alloc());
Returns:
packaged_token<decay_t<Func>, Alloc>(forward<Func>(f), a)
.
namespace std { namespace experimental { inline namespace concurrency_v1 { class thread_pool; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
namespace std { namespace experimental { inline namespace concurrency_v1 { class thread_pool : public execution_context { public: // types: class executor_type; // construct / copy / destroy: thread_pool(); explicit thread_pool(std::size_t num_threads); thread_pool(const thread_pool&) = delete; thread_pool& operator=(const thread_pool&) = delete; ~thread_pool(); // thread_pool operations: executor_type get_executor() noexcept; void stop(); void join(); }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
Class thread_pool
implements
a fixed-size pool of threads.
An object of type thread_pool
has an associated executor object, meeting the Executor
type requirements, of type thread_pool::executor_type
and obtainable via the thread_pool
object's get_executor
member
function.
For an object of type thread_pool
,
outstanding work is defined as the sum of:
— the total number of calls to the thread_pool
executor's on_work_started
function, less the total number of calls to the on_work_finished
function;
— the number of function objects that have been added to the thread_pool
via the thread_pool
executor, but not yet executed; and
— the number of function objects that are currently being executed by the
thread_pool
.
The thread_pool
member functions
get_executor
, stop
, and join
,
and the thread_pool::executor_type
copy constructors, member
functions and comparison operators, shall not introduce data races as a result
of concurrent calls to those functions from different threads.
thread_pool(); explicit thread_pool(std::size_t num_threads);
Effects: Creates an object of class
thread_pool
containing a number of threads of execution, each represented by athread
object. If specified, the number of threads in the pool isnum_threads
. Otherwise, the number of threads in the pool is implementation-defined. [Note: A suggested value for the implementation-defined number of threads isstd::thread::hardware_concurrency() * 2
. —end note]
~thread_pool();
Effects: Destroys an object of class
thread_pool
. Performsstop()
followed byjoin()
.
executor_type get_executor() noexcept;
Returns: An executor that may be used for submitting function objects to the
thread_pool
.
void stop();
Effects: Signals the threads in the pool to complete as soon as possible. If a thread is currently executing a function object, the thread will exit only after completion of that function object. The call to
stop()
returns without waiting for the threads to complete.
void join();
Effects: If not already stopped, signals the threads in the pool to exit once the outstanding work is
0
. Blocks until all threads in the pool have completed.
Synchronization: The completion of each thread in the pool synchronizes with (C++ Std, [intro.multithread]) the corresponding successful
join()
return.
Postconditions: The threads in the pool represented by
*this
have completed.
namespace std { namespace experimental { inline namespace concurrency_v1 { class thread_pool::executor_type { public: // construct / copy / destroy: executor_type(const executor_type& other) noexcept; executor_type(executor_type&& other) noexcept; executor_type& operator=(const executor_type& other) noexcept; executor_type& operator=(executor_type&& other) noexcept; ~executor_type(); // executor operations: bool running_in_this_thread() const noexcept; thread_pool& context() noexcept; void on_work_started() noexcept; void on_work_finished() noexcept; template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a); template<class Func, class Alloc> void post(Func&& f, const Alloc& a); template<class Func, class Alloc> void defer(Func&& f, const Alloc& a); }; bool operator==(const thread_pool::executor_type& a, const thread_pool::executor_type& b) noexcept; bool operator!=(const thread_pool::executor_type& a, const thread_pool::executor_type& b) noexcept; template<> struct is_executor<thread_pool::executor_type> : true_type {}; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
thread_pool::executor_type
is a type satisfying Executor requirements. Objects of
type thread_pool::executor_type
are associated with a thread_pool
, and function objects submitted
using the dispatch
, post
or defer
member functions will be executed by the thread_pool
.
executor_type(const executor_type& other) noexcept;
Effects: Constructs an object of class
thread_pool::executor_type
.
Postconditions:
*this == other
.
executor_type(executor_type&& other) noexcept;
Effects: Constructs an object of class
thread_pool::executor_type
.
Postconditions:
*this
is equal to the prior value ofother
.
executor_type& operator=(const executor_type& other) noexcept;
Postconditions:
*this == other
.
Returns:
*this
.
executor_type& operator=(executor_type&& other) noexcept;
Postconditions:
*this
is equal to the prior value ofother
.
Returns:
*this
.
bool running_in_this_thread() const noexcept;
Returns:
true
if the current thread of execution is a member of the pool; otherwisefalse
.
thread_pool& context() noexcept;
Returns: A reference to the associated
thread_pool
object.
void on_work_started() noexcept;
Effects: Increases the count of outstanding work associated with the
thread_pool
.
void on_work_finished() noexcept;
Effects: Decreases the count of outstanding work associated with the
thread_pool
.
template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a);
Effects: If
running_in_this_thread()
is true, callsDECAY_COPY
(forward<Func>(f))()
. Otherwise, requests execution off
.
If
f
exits via an exception, and the execution off
is performed in the current thread and beforedispatch
returns, the exception shall propagate to the caller ofdispatch
.
template<class Func, class Alloc> void post(Func&& f, const Alloc& a);
Effects: Requests execution of
f
.
template<class Func, class Alloc> void defer(Func&& f, const Alloc& a);
Effects: Requests execution of
f
.
bool operator==(const thread_pool::executor_type& a, const thread_pool::executor_type& b) noexcept;
Returns:
&a.context() == &b.context()
.
bool operator!=(const thread_pool::executor_type& a, const thread_pool::executor_type& b) noexcept;
Returns:
!(a == b)
.
namespace std { namespace experimental { inline namespace concurrency_v1 { class loop_scheduler; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
namespace std { namespace experimental { inline namespace concurrency_v1 { class loop_scheduler : public execution_context { public: // types: class executor_type; // construct / copy / destroy: loop_scheduler(); explicit loop_scheduler(std::size_t concurrency_hint); loop_scheduler(const loop_scheduler&) = delete; loop_scheduler& operator=(const loop_scheduler&) = delete; ~loop_scheduler(); // loop_scheduler operations: executor_type get_executor() noexcept; size_t run(); template<class Rep, class Period> size_t run_for(const chrono::duration<Rep, Period>& rel_time); template<class Clock, class Duration> size_t run_until(const chrono::time_point<Clock, Duration>& abs_time); size_t run_one(); template<class Rep, class Period> size_t run_one_for(const chrono::duration<Rep, Period>& rel_time); template<class Clock, class Duration> size_t run_one_until(const chrono::time_point<Clock, Duration>& abs_time); size_t poll(); size_t poll_one(); void stop(); bool stopped() const; void restart(); }; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
An object of type loop_scheduler
has an associated executor object, meeting the Executor
type requirements, of type loop_scheduler::executor_type
and obtainable via the loop_scheduler
object's get_executor
member
function.
For an object of type loop_scheduler
,
outstanding work is defined as the sum of:
— the total number of calls to the loop_scheduler
executor's on_work_started
function, less the total number of calls to the on_work_finished
function;
— the number of function objects that have been added to the loop_scheduler
via the loop_scheduler
executor, but not yet executed; and
— the number of function objects that are currently being executed by the
loop_scheduler
.
If at any time the outstanding work falls to 0
,
the loop_scheduler
is stopped
as if by stop()
.
The loop_scheduler
member
functions get_executor
,
run
, run_for
,
run_until
, run_one
, run_one_for
,
run_one_until
, poll
, poll_one
,
stop
, and stopped
,
and the loop_scheduler::executor_type
copy constructors, member
functions and comparison operators, shall not introduce data races as a result
of concurrent calls to those functions from different threads. [Note:
The restart
member function
is excluded from these thread safety requirements. —end note]
loop_scheduler(); explicit loop_scheduler(std::size_t concurrency_hint);
Effects: Creates an object of class
loop_scheduler
.
Remarks: The
concurrency_hint
parameter is a suggestion to the implementation on the number of threads that should process asynchronous operations and execute function objects.
~loop_scheduler();
Effects: Destroys an object of class
loop_scheduler
.
executor_type get_executor() noexcept;
Returns: An executor that may be used for submitting function objects to the
loop_scheduler
.
size_t run();
Requires: Must not be called from a thread that is currently calling one of
run
,run_for
,run_until
,run_one
,run_one_for
,run_one_until
,poll
, orpoll_one
.
Effects: Equivalent to:
size_t n = 0; while (run_one()) if (n != numeric_limits<size_t>::max()) ++n;
Returns:
n
.
template<class Rep, class Period> size_t run_for(const chrono::duration<Rep, Period>& rel_time);
Effects: Equivalent to:
return run_until(chrono::steady_clock::now() + rel_time);
template<class Clock, class Duration> size_t run_until(const chrono::time_point<Clock, Duration>& abs_time);
Effects: Equivalent to:
size_t n = 0; while (run_one_until(abs_time)) if (n != numeric_limits<size_t>::max()) ++n;
Returns:
n
.
size_t run_one();
Requires: Must not be called from a thread that is currently calling one of
run
,run_for
,run_until
,run_one
,run_one_for
,run_one_until
,poll
, orpoll_one
.
Effects: If the
loop_scheduler
object has no oustanding work, performsstop()
. Otherwise, blocks while the loop_scheduler has outstanding work, or until theloop_scheduler
is stopped, or until one function object has been executed.
If an executed function object throws an exception, the exception shall be allowed to propagate to the caller of
run_one()
. Theloop_scheduler
state shall be as if the function object had returned normally.
Returns:
1
if a function object was executed, otherwise0
.
Notes: This function may invoke additional handlers through nested calls to the
loop_scheduler
executor'sdispatch
member function. These do not count towards the return value.
template<class Rep, class Period> size_t run_one_for(const chrono::duration<Rep, Period>& rel_time);
Effects: Equivalent to:
return run_until(chrono::steady_clock::now() + rel_time);
Returns:
1
if a function object was executed, otherwise0
.
template<class Clock, class Duration> size_t run_one_until(const chrono::time_point<Clock, Duration>& abs_time);
Effects: If the
loop_scheduler
object has no oustanding work, performsstop()
. Otherwise, blocks while theloop_scheduler
has outstanding work, or until the expiration of the absolute timeout (C++ Std, [thread.req.timing]) specified byabs_time
, or until theloop_scheduler
is stopped, or until one function object has been executed.
If an executed function object throws an exception, the exception shall be allowed to propagate to the caller of
run_one()
. Theloop_scheduler
state shall be as if the function object had returned normally.
Returns:
1
if a function object was executed, otherwise0
.
Notes: This function may invoke additional handlers through nested calls to the
loop_scheduler
executor'sdispatch
member function. These do not count towards the return value.
size_t poll();
Effects: Equivalent to:
size_t n = 0; while (poll_one()) if (n != numeric_limits<size_t>::max()) ++n;
Returns:
n
.
size_t poll_one();
Effects: If the
loop_scheduler
object has no oustanding work, performsstop()
. Otherwise, if there is a function object ready for immediate execution, executes it.
If an executed function object throws an exception, the exception shall be allowed to propagate to the caller of
poll_one()
. Theloop_scheduler
state shall be as if the function object had returned normally.
Returns:
1
if a handler was executed, otherwise0
.
Notes: This function may invoke additional handlers through nested calls to the
loop_scheduler
executor'sdispatch
member function. These do not count towards the return value.
void stop();
Effects: Stops the
loop_scheduler
. Concurrent calls torun
,run_for
,run_until
,run_one
,run_one_for
,run_one_until
,poll
orpoll_one
will end as soon as possible. If a call torun
,run_for
,run_until
,run_one
,run_one_for
,run_one_until
,poll
orpoll_one
is currently executing a function object, the call will end only after completion of that function object. The call tostop()
returns without waiting for concurrent calls torun
,run_for
,run_until
,run_one
,run_one_for
,run_one_until
,poll
orpoll_one
to complete.
Postconditions:
stopped() == true
.
[Note: When
stopped() == true
, subsequent calls torun
,run_for
,run_until
,run_one
,run_one_for
,run_one_until
,poll
orpoll_one
will exit immediately with a return value of0
, without executing any function objects. Aloop_scheduler
remains in the stopped state until a call torestart()
. —end note]
void restart();
Postconditions:
stopped() == false
.
namespace std { namespace experimental { inline namespace concurrency_v1 { class loop_scheduler::executor_type { public: // construct / copy / destroy: executor_type(const executor_type& other) noexcept; executor_type(executor_type&& other) noexcept; executor_type& operator=(const executor_type& other) noexcept; executor_type& operator=(executor_type&& other) noexcept; ~executor_type(); // executor operations: bool running_in_this_thread() const noexcept; loop_scheduler& context() noexcept; void on_work_started() noexcept; void on_work_finished() noexcept; template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a); template<class Func, class Alloc> void post(Func&& f, const Alloc& a); template<class Func, class Alloc> void defer(Func&& f, const Alloc& a); }; bool operator==(const loop_scheduler::executor_type& a, const loop_scheduler::executor_type& b) noexcept; bool operator!=(const loop_scheduler::executor_type& a, const loop_scheduler::executor_type& b) noexcept; template<> struct is_executor<loop_scheduler::executor_type> : true_type {}; } // inline namespace concurrency_v1 } // namespace experimental } // namespace std
loop_scheduler::executor_type
is a type satisfying Executor requirements. Objects of
type loop_scheduler::executor_type
are associated with a loop_scheduler
, and function objects submitted
using the dispatch
, post
or defer
member functions will be executed by the loop_scheduler
from within the run
, run_for
, run_until
,
run_one
, run_one_for
,
run_one_until
, poll
or poll_one
functions.
executor_type(const executor_type& other) noexcept;
Effects: Constructs an object of class
loop_scheduler::executor_type
.
Postconditions:
*this == other
.
executor_type(executor_type&& other) noexcept;
Effects: Constructs an object of class
loop_scheduler::executor_type
.
Postconditions:
*this
is equal to the prior value ofother
.
executor_type& operator=(const executor_type& other) noexcept;
Postconditions:
*this == other
.
Returns:
*this
.
executor_type& operator=(executor_type&& other) noexcept;
Postconditions:
*this
is equal to the prior value ofother
.
Returns:
*this
.
bool running_in_this_thread() const noexcept;
Returns:
true
if the current thread of execution is invoking therun
,run_for
,run_until
,run_one
,run_one_for
,run_one_until
,poll
orpoll_one
function of the associatedloop_scheduler
object.
loop_scheduler& context() noexcept;
Returns: A reference to the associated
loop_scheduler
object.
void on_work_started() noexcept;
Effects: Increases the count of outstanding work associated with the
loop_scheduler
.
void on_work_finished() noexcept;
Effects: Decreases the count of outstanding work associated with the
loop_scheduler
.
template<class Func, class Alloc> void dispatch(Func&& f, const Alloc& a);
Effects: If
running_in_this_thread()
is true, callsDECAY_COPY
(forward<Func>(f))()
. Otherwise, requests execution off
.
If
f
exits via an exception, and the execution off
is performed in the current thread and beforedispatch
returns, the exception shall propagate to the caller ofdispatch
.
template<class Func, class Alloc> void post(Func&& f, const Alloc& a);
Effects: Requests execution of
f
.
template<class Func, class Alloc> void defer(Func&& f, const Alloc& a);
Effects: Requests execution of
f
.
bool operator==(const loop_scheduler::executor_type& a, const loop_scheduler::executor_type& b) noexcept;
Returns:
&a.context() == &b.context()
.
bool operator!=(const loop_scheduler::executor_type& a, const loop_scheduler::executor_type& b) noexcept;
Returns:
!(a == b)
.
[1] The concept itself is not new, however the term is taken from Holgate, Len, Activatable Object, ACCU Overload Journal #122, August 2014
[3]
Unless a small-object optimisation is employed by std::function
,
but this is not guaranteed.