![]() |
Home | Libraries | People | FAQ | More |
Closure
Executor
work
executor
executor_adaptor
generic_executor_ref
scheduler
at_executor
scheduler_executor_wrapper
resubmit_at_executor
serial_executor
generic_serial_executor
inline_executor
basic_thread_pool
thread_executor
loop_executor
![]() |
Warning |
---|---|
These features are experimental and subject to change in future versions. There are not too much tests yet, so it is possible that you can find out some trivial bugs :( |
![]() |
Note |
---|---|
These features are based on the N3785 - Executors and Schedulers revision 3 C++1y proposal from Chris Mysen, Niklas Gustafsson, Matt Austern, Jeffrey Yasskin. The text that follows has been adapted from this paper to show the differences. |
Executors are objects that can execute units of work packaged as function objects. Boost.Thread differs from N3785 mainly in the an Executor doesn't needs to inherit from an abstract class Executor. Static polymorphism is used instead and type erasure is used internally.
Multithreaded programs often involve discrete (sometimes small) units of work that are executed asynchronously. This often involves passing work units to some component that manages execution. We already have boost::async, which potentially executes a function asynchronously and eventually returns its result in a future. (“As if” by launching a new thread.)
If there is a regular stream of small work items then we almost certainly don’t want to launch a new thread for each, and it’s likely that we want at least some control over which thread(s) execute which items. It is often convenient to represent that control as multiple executor objects. This allows programs to start executors when necessary, switch from one executor to another to control execution policy, and use multiple executors to prevent interference and thread exhaustion. Several possible implementations exist of the executor class and in practice there are a number of main groups of executors which have been found to be useful in real-world code (more implementations exist, this is simply a high level classification of them). These differ along a couple main dimensions, how many execution contexts will be used, how they are selected, and how they are prioritized.
A question arises of which of these executors (or others) be included in
this library. There are use cases for these and many other executors. Often
it is useful to have more than one implemented executor (e.g. the thread
pool) to have more precise control of where the work is executed due to
the existence of a GUI thread, or for testing purposes. A few core executors
are frequently useful and these have been outlined here as the core of
what should be in this library, if common use cases arise for alternative
executor implementations, they can be added in the future. The current
set provided here are: a basic thread pool basic_thread_pool
,
a serial executor serial_executor
,
a loop executor loop_executor
,
an inline executor inline_executor
and a thread-spawning executor thread_executor
.
#include <boost/thread/executors/basic_thread_pool.hpp> #include <boost/thread/future.hpp> #include <numeric> #include <algorithm> #include <functional> #include <iostream> #include <list> template<typename T> struct sorter { boost::basic_thread_pool pool; typedef std::list<T> return_type; std::list<T> do_sort(std::list<T> chunk_data) { if(chunk_data.empty()) { return chunk_data; } std::list<T> result; result.splice(result.begin(),chunk_data, chunk_data.begin()); T const& partition_val=*result.begin(); typename std::list<T>::iterator divide_point = std::partition(chunk_data.begin(), chunk_data.end(), [&](T const& val){return val<partition_val;}); std::list<T> new_lower_chunk; new_lower_chunk.splice(new_lower_chunk.end(), chunk_data, chunk_data.begin(), divide_point); boost::future<std::list<T> > new_lower = boost::async(pool, &sorter::do_sort, this, std::move(new_lower_chunk)); std::list<T> new_higher(do_sort(chunk_data)); result.splice(result.end(),new_higher); while(!new_lower.is_ready()) { pool.schedule_one_or_yield(); } result.splice(result.begin(),new_lower.get()); return result; } }; template<typename T> std::list<T> parallel_quick_sort(std::list<T>& input) { if(input.empty()) { return input; } sorter<T> s; return s.do_sort(input); }
The authors of Boost.Thread have taken a different approach respect to N3785. Instead of basing all the design on an abstract executor class we make executor concepts. We believe that this is the good direction as a static polymorphic executor can be seen as a dynamic polymorphic executor using a simple adaptor. We believe also that it would make the library more usable, and more convenient for users.
The major design decisions concern deciding what a unit of work is, how to manage with units of work and time related functions in a polymorphic way.
An Executor is an object that schedules the closures that have been submitted to it, usually asynchronously. There could be multiple models of the Executor class. Some specific design notes:
One important question is just what a closure is. This library has a very
simple answer: a closure is a Callable
with no parameters and returning void
.
N3785 choose the more specific std::function<void()>
as it provides only dynamic polymorphism
and states that in practice the implementation of a template based approach
or another approach is impractical. The authors of this library think that
the template based approach is compatible with a dynamic based approach.
They give some arguments:
The first one is that a virtual function can not be a template. This is
true but it is also true that the executor interface can provide the template
functions that call to the virtual public functions. Another reason they
give 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". We think that it is up to the executor
to manage with this implementation details, not to the user.
We share all the argument they give related to the void()
interface of the work unit. A work unit
is a closure that takes no arguments and returns no value. This is indeed
a limitation on user code, but combined with boost::async
taking executors as parameters the user has all what she needs.
The third one is related to performance. They assert that "any mechanism
for storing closures on an executor’s queue will have to use some form
of type erasure. There’s no reason to believe that a custom closure mechanism,
written just for std::executor and used nowhere else within the standard
library, would be better in that respect than std::function<void()>
". We believe that the implementation
can do better that storing the closure on a std::function<void()>
. e.g. the implementation can use
intrusive data to store the closure and the pointers to other nodes needed
to store the closures in a given order.
In addition std::function<void()>
can not be constructed by moving the closure, so e.g. std::packaged_task
could not be a Closure.
The approach of this library respect to scheduled work of the N3785 proposal
is quite different. Instead of adding the scheduled operations to a specific
scheduled_executor polymorphic interface, we opt by adding a specific
scheduler
class that is
not an executor and knows how to manage with the scheduling of timed tasks
submit_at
/submit_after
.
scheduler
provides executor
factories at
/after
given a specific time_point
or a duration
.
The built executors wrap a reference to this scheduler and the time at
which the submitted task will be executed.
If we want to schedule these operations on an existing executor (as serial_executor
does), these classes
provide a on
factory taking
another executor as parameter and wraps both instance on the returned executor.
sch.on(tp).after(seconds(i)).submit(boost::bind(fn,i));
This has several advantages:
time_point
and duration
respectively
as we are not working with virtual functions.
In order to manage with all the clocks, this library propose generic solution.
scheduler<Clock>
know how to manage with the submit_at
/submit_after
Clock::time_point
/Clock::duration
tasks. Note that the durations
on different clocks differ.
As in N3785 and based on the same design decision than std
/boost::thread
if a user closure throws an exception,
the executor must call the std::terminate
function. Note that when we combine boost::async
and Executors
, the exception
will be caught by the closure associated to the returned future, so that
the exception is stored on the returned future, as for the other async
overloads.
It is common idiom to set some thread local variable at the beginning of a thread. As Executors could instantiate threads internally these Executors shall have the ability to call a user specific function at thread entry on the executor constructor.
For executors that don't instantiate any thread and that would use the
current thread this function shall be called only for the thread calling
the at_thread_entry
member
function.
The library does not provision yet for the ability to cancel/interrupt work, though this is a commonly requested feature.
This could be managed externally by an additional cancelation object that can be shared between the creator of the unit of work and the unit of work.
We can think also of a cancelable closure that could be used in a more transparent way.
An alternative is to make async return a cancelable_task but this will need also a cancelable closure.
The library does not provision for the ability to get the current executor, though having access to it could simplify a lot the user code.
The reason is that the user can always use a thread_local variable and
reset it using the at_thread_entry
member function.
thread_local current_executor_state_type current_executor_state; executor* current_executor() { return current_executor_state.current_executor(); } basic_thread_pool pool( // at_thread_entry [](basic_thread_pool& pool) { current_executor_state.set_current_executor(pool); } );
[
The library authors share some of the concerns of the C++ standard committee (introduction of a new single shared resource, a singleton, could make it difficult to make it portable to all the environments) and that this library doesn't need to provide a default executor for the time been.
The user can always define his default executor himself.
boost::generic_executor_ref default_executor() { static boost::basic_thread_pool tp(4); return generic_executor_ref(tp); }
Closure
Executor
work
executor
executor_adaptor
generic_executor_ref
scheduler
at_executor
scheduler_executor_wrapper
resubmit_at_executor
serial_executor
generic_serial_executor
inline_executor
basic_thread_pool
thread_executor
loop_executor
A type E
meets the Closure
requirements if is a model
of Callable(void())
and a model of CopyConstructible
/MoveConstructible
.
The Executor
concept
models the common operations of all the executors.
A type E
meets the Executor
requirements if the following
expressions are well-formed and have the specified semantics
e.submit(lc);
e.submit(rc);
e.close();
b =
e.closed();
e.try_executing_one();
e.reschedule_until(p);
where
e
denotes a value
of type E
,
lc
denotes a lvalue
reference of type Closure
,
rc
denotes a rvalue
reference of type Closure
p
denotes a value
of type Predicate
The specified closure will be scheduled for execution at some point in the future. If invoked closure throws an exception the executor will call std::terminate, as is the case with threads.
completion of closure on a particular thread happens before destruction of thread's thread local variables.
void
.
sync_queue_is_closed if the thread pool is closed. Whatever exception that can be throw while storing the closure.
If an exception is thrown then the executor state is unmodified.
The specified closure will be scheduled for execution at some point in the future. If invoked closure throws an exception the executor will call std::terminate, as is the case with threads.
completion of closure on a particular thread happens before destruction of thread's thread local variables.
void
.
sync_queue_is_closed if the thread pool is closed. Whatever exception that can be throw while storing the closure.
If an exception is thrown then the executor state is unmodified.
close the executor e
for submissions.
The worker threads will work until there is no more closures to run.
void
.
Whatever exception that can be thrown while ensuring the thread safety.
If an exception is thrown then the executor state is unmodified.
bool
.
whether the executor is closed for submissions.
Whatever exception that can be throw while ensuring the thread safety.
try to execute one work.
whether a work has been executed.
bool
.
Whether a work has been executed.
whatever the current work constructor throws or the work()
throws.
This must be called from a scheduled work
reschedule works until p()
.
bool
.
Whether a work has been executed.
whatever the current work constructor throws or the work()
throws.
#include <boost/thread/work.hpp> namespace boost { typedef 'implementation_defined' work; }
work is a model of 'Closure'
Executor abstract base class.
#include <boost/thread/executor.hpp> namespace boost { class executor { public: typedef boost::work work; executor(executor const&) = delete; executor& operator=(executor const&) = delete; executor(); virtual ~executor() {}; virtual void close() = 0; virtual bool closed() = 0; virtual void submit(work&& closure) = 0; virtual void submit(work& closure) = 0; template <typename Closure> void submit(Closure&& closure); virtual bool try_executing_one() = 0; template <typename Pred> bool reschedule_until(Pred const& pred); }; }
virtual ~executor();
Destroys the executor.
The completion of all the closures happen before the completion of the executor destructor.
Polymorphic adaptor of a model of Executor to an executor.
#include <boost/thread/executor.hpp> namespace boost { template <typename Executor> class executor_adaptor : public executor { Executor ex; // for exposition only public: typedef executor::work work; executor_adaptor(executor_adaptor const&) = delete; executor_adaptor& operator=(executor_adaptor const&) = delete; template <typename ...Args> executor_adaptor(Args&& ... args); Executor& underlying_executor() noexcept; void close(); bool closed(); void submit(work&& closure); void submit(work& closure); bool try_executing_one(); }; }
template <typename ...Args> executor_adaptor(Args&& ... args);
Constructs an executor_adaptor.
Nothing.
virtual ~executor_adaptor();
Destroys the executor_adaptor.
The completion of all the closures happen before the completion of the executor destructor.
Executor& underlying_executor() noexcept;
The underlying executor instance.
Executor abstract base class.
#include <boost/thread/generic_executor_ref.hpp> namespace boost { class generic_executor_ref { public: generic_executor_ref(generic_executor_ref const&); generic_executor_ref& operator=(generic_executor_ref const&); template <class Executor> generic_executor_ref(Executor& ex); generic_executor_ref() {}; void close() = 0; bool closed() = 0; template <typename Closure> void submit(Closure&& closure); virtual bool try_executing_one() = 0; template <typename Pred> bool reschedule_until(Pred const& pred); }; }
Scheduler providing time related functions. Note that scheduler
is not an Executor.
#include <boost/thread/executors/scheduler.hpp> namespace boost { template <class Clock=steady_clock> class scheduler { public: using work = boost::function<void()> ; using clock = Clock; scheduler(scheduler const&) = delete; scheduler& operator=(scheduler const&) = delete; scheduler(); ~scheduler(); void close(); bool closed(); template <class Duration, typename Closure> void submit_at(chrono::time_point<clock,Duration> abs_time, Closure&& closure); template <class Rep, class Period, typename Closure> void submit_after(chrono::duration<Rep,Period> rel_time, Closure&& closure); template <class Duration> at_executor<scheduler> submit_at(chrono::time_point<clock,Duration> abs_time); template <class Rep, class Period> at_executor<scheduler> submit_after(chrono::duration<Rep,Period> rel_time); template <class Executor> scheduler_executor_wrapper<scheduler, Executor> on(Executor& ex); }; }
~scheduler();
Destroys the scheduler.
The completion of all the closures happen before the completion of the executor destructor.
template <class Clock, class Duration, typename Closure> void submit_at(chrono::time_point<Clock,Duration> abs_time, Closure&& closure);
Schedule a closure
to be executed at abs_time
.
Nothing.
template <class Rep, class Period, typename Closure> void submit_after(chrono::duration<Rep,Period> rel_time, Closure&& closure);
Schedule a closure
to be executed after rel_time
.
Nothing.
#include <boost/thread/executors/scheduler.hpp> namespace boost { template <class Scheduler> class at_executor { public: using work = Scheduler::work; using clock = Scheduler::clock; at_executor(at_executor const&) = default; at_executor(at_executor &&) = default; at_executor& operator=(at_executor const&) = default; at_executor& operator=(at_executor &&) = default; at_executor(Scheduler& sch, clock::time_point const& tp); ~at_executor(); void close(); bool closed(); Scheduler& underlying_scheduler(); template <class Closure> void submit(Closure&& closure); template <class Duration, typename Work> void submit_at(chrono::time_point<clock,Duration> abs_time, Closure&& closure); template <class Rep, class Period, typename Work> void submit_after(chrono::duration<Rep,Period> rel_time, Closure&& closure); template <class Executor> resubmit_at_executor<Scheduler, Executor> on(Executor& ex); }; }
at_executor(Scheduler& sch, clock::time_point const& tp);
Constructs a at_executor
.
Nothing.
~at_executor();
Destroys the at_executor
.
The completion of all the closures happen before the completion of the executor destructor.
Scheduler& underlying_scheduler() noexcept;
The underlying scheduler instance.
template <typename Closure> void submit(Closure&& closure);
Schedule the closure
to be executed at the abs_time
given at construction time.
Nothing.
template <class Clock, class Duration, typename Closure> void submit_at(chrono::time_point<Clock,Duration> abs_time, Closure&& closure);
Schedule a closure
to be executed at abs_time
.
Nothing.
template <class Rep, class Period, typename Closure> void submit_after(chrono::duration<Rep,Period> rel_time, Closure&& closure);
Schedule a closure
to be executed after rel_time
.
Nothing.
#include <boost/thread/executors/scheduler.hpp> namespace boost { template <class Scheduler, class Executor> class scheduler_executor_wrapper { public: using work = Scheduler::work; using clock = Scheduler::clock; scheduler_executor_wrapper(scheduler_executor_wrapper const&) = default; scheduler_executor_wrapper(scheduler_executor_wrapper &&) = default; scheduler_executor_wrapper& operator=(scheduler_executor_wrapper const&) = default; scheduler_executor_wrapper& operator=(scheduler_executor_wrapper &&) = default; scheduler_executor_wrapper(Scheduler& sch, Executor& ex); ~scheduler_executor_wrapper(); void close(); bool closed(); Executor& underlying_executor(); Scheduler& underlying_scheduler(); template <class Closure> void submit(Closure&& closure); template <class Duration, typename Work> void submit_at(chrono::time_point<clock,Duration> abs_time, Closure&& closure); template <class Rep, class Period, typename Work> void submit_after(chrono::duration<Rep,Period> rel_time, Closure&& closure); template <class Duration> resubmit_at_executor<Scheduler, Executor> at(chrono::time_point<clock,Duration> abs_time); template <class Rep, class Period> resubmit_at_executor<Scheduler, Executor> after(chrono::duration<Rep,Period> rel_time); }; }
scheduler_executor_wrapper(Scheduler& sch, Executor& ex);
Constructs a scheduler_executor_wrapper
.
Nothing.
~scheduler_executor_wrapper();
Destroys the scheduler_executor_wrapper
.
The completion of all the closures happen before the completion of the executor destructor.
Scheduler& underlying_scheduler() noexcept;
The underlying scheduler instance.
Executor& underlying_executor() noexcept;
The underlying executor instance.
template <typename Closure> void submit(Closure&& closure);
Submit the closure
on the underlying executor.
Nothing.
template <class Clock, class Duration, typename Closure> void submit_at(chrono::time_point<Clock,Duration> abs_time, Closure&& closure);
Resubmit the closure
to be executed on the underlying executor at abs_time
.
Nothing.
template <class Rep, class Period, typename Closure> void submit_after(chrono::duration<Rep,Period> rel_time, Closure&& closure);
Resubmit the closure
to be executed on the underlying executor after rel_time
.
Nothing.
resubmit_at_executor(Scheduler&, Executor&, clock::time_point<Duration>)
~resubmit_at_executor()
underlying_executor()
underlying_scheduler()
submit()
submit_at()
submit_after()
Executor
wrapping an
Scheduler
, an Executor
and a time_point
providing an Executor
interface.
#include <boost/thread/executors/scheduler.hpp> namespace boost { template <class Scheduler, class Executor> class resubmit_at_executor { public: using work = Scheduler::work; using clock = Scheduler::clock; resubmit_at_executor(resubmit_at_executor const&) = default; resubmit_at_executor(resubmit_at_executor &&) = default; resubmit_at_executor& operator=(resubmit_at_executor const&) = default; resubmit_at_executor& operator=(resubmit_at_executor &&) = default; template <class Duration> resubmit_at_executor(Scheduler& sch, Executor& ex, clock::time_point<Duration> const& tp); ~resubmit_at_executor(); void close(); bool closed(); Executor& underlying_executor(); Scheduler& underlying_scheduler(); template <class Closure> void submit(Closure&& closure); template <class Duration, typename Work> void submit_at(chrono::time_point<clock,Duration> abs_time, Closure&& closure); template <class Rep, class Period, typename Work> void submit_after(chrono::duration<Rep,Period> rel_time, Closure&& closure); }; }
template <class Duration> resubmit_at_executor(Scheduler& sch, Executor& ex, clock::time_point<Duration> const& tp);
Constructs a resubmit_at_executor
.
Nothing.
~resubmit_at_executor();
Destroys the executor_adaptor.
The completion of all the closures happen before the completion of the executor destructor.
Executor& underlying_executor() noexcept;
The underlying executor instance.
Scheduler& underlying_scheduler() noexcept;
The underlying scheduler instance.
template <typename Closure> void submit(Closure&& closure);
Resubmit the closure
to be executed on the underlying executor at the abs_time
given at construction
time.
Nothing.
template <class Clock, class Duration, typename Closure> void submit_at(chrono::time_point<Clock,Duration> abs_time, Closure&& closure);
Resubmit the closure
to be executed on the underlying executor at abs_time
.
Nothing.
template <class Rep, class Period, typename Closure> void submit_after(chrono::duration<Rep,Period> rel_time, Closure&& closure);
Resubmit the closure
to be executed on the underlying executor after rel_time
.
Nothing.
A serial executor ensuring that there are no two work units that executes concurrently.
#include <boost/thread/serial_executor.hpp> namespace boost { template <class Executor> class serial_executor { public: serial_executor(serial_executor const&) = delete; serial_executor& operator=(serial_executor const&) = delete; template <class Executor> serial_executor(Executor& ex); Executor& underlying_executor() noexcept; void close(); bool closed(); template <typename Closure> void submit(Closure&& closure); bool try_executing_one(); template <typename Pred> bool reschedule_until(Pred const& pred); }; }
template <class Executor> serial_executor(Executor& ex);
Constructs a serial_executor.
Nothing.
~serial_executor();
Destroys the serial_executor.
The completion of all the closures happen before the completion of the executor destructor.
generic_executor_ref& underlying_executor() noexcept;
The underlying executor instance.
Nothing.
A serial executor ensuring that there are no two work units that executes concurrently.
#include <boost/thread/generic_serial_executor.hpp> namespace boost { class generic_serial_executor { public: generic_serial_executor(generic_serial_executor const&) = delete; generic_serial_executor& operator=(generic_serial_executor const&) = delete; template <class Executor> generic_serial_executor(Executor& ex); generic_executor_ref& underlying_executor() noexcept; void close(); bool closed(); template <typename Closure> void submit(Closure&& closure); bool try_executing_one(); template <typename Pred> bool reschedule_until(Pred const& pred); }; }
template <class Executor> generic_serial_executor(Executor& ex);
Constructs a serial_executor.
Nothing.
~generic_serial_executor();
Destroys the serial_executor.
The completion of all the closures happen before the completion of the executor destructor.
Executor& underlying_executor() noexcept;
The underlying executor instance.
A serial executor ensuring that there are no two work units that executes concurrently.
#include <boost/thread/inline_executor.hpp> namespace boost { class inline_executor { public: inline_executor(inline_executor const&) = delete; inline_executor& operator=(inline_executor const&) = delete; inline_executor(); void close(); bool closed(); template <typename Closure> void submit(Closure&& closure); bool try_executing_one(); template <typename Pred> bool reschedule_until(Pred const& pred); }; }
inline_executor();
Constructs an inline_executor.
Nothing.
~inline_executor();
Destroys the inline_executor.
The completion of all the closures happen before the completion of the executor destructor.
A thread pool with up to a fixed number of threads.
#include <boost/thread/executors/basic_thread_pool.hpp> namespace boost { class basic_thread_pool { public: basic_thread_pool(basic_thread_pool const&) = delete; basic_thread_pool& operator=(basic_thread_pool const&) = delete; basic_thread_pool(unsigned const thread_count = thread::hardware_concurrency()); template <class AtThreadEntry> basic_thread_pool( unsigned const thread_count, AtThreadEntry at_thread_entry); ~basic_thread_pool(); void close(); bool closed(); template <typename Closure> void submit(Closure&& closure); bool try_executing_one(); template <typename Pred> bool reschedule_until(Pred const& pred); }; }
creates a thread pool that runs closures on thread_count
threads.
Whatever exception is thrown while initializing the needed resources.
~basic_thread_pool();
Destroys the thread pool.
The completion of all the closures happen before the completion of the executor destructor.
A thread_executor with a threads for each task.
#include <boost/thread/executors/thread_executor.hpp> namespace boost { class thread_executor { public: thread_executor(thread_executor const&) = delete; thread_executor& operator=(thread_executor const&) = delete; thread_executor(); template <class AtThreadEntry> basic_thread_pool( unsigned const thread_count, AtThreadEntry at_thread_entry); ~thread_executor(); void close(); bool closed(); template <typename Closure> void submit(Closure&& closure); }; }
creates a thread_executor.
Whatever exception is thrown while initializing the needed resources.
~thread_executor();
Waits for closures (if any) to complete, then joins and destroys the threads.
The completion of all the closures happen before the completion of the executor destructor.
A user scheduled executor.
#include <boost/thread/loop_executor.hpp> namespace boost { class loop_executor { public: loop_executor(loop_executor const&) = delete; loop_executor& operator=(loop_executor const&) = delete; loop_executor(); ~loop_executor(); void close(); bool closed(); template <typename Closure> void submit(Closure&& closure); bool try_executing_one(); template <typename Pred> bool reschedule_until(Pred const& pred); void loop(); void run_queued_closures(); }; }
loop_executor();
creates an executor that runs closures using one of its closure-executing methods.
Whatever exception is thrown while initializing the needed resources.
virtual ~loop_executor();
Destroys the executor.
The completion of all the closures happen before the completion of the executor destructor.
void loop();
reschedule works until closed()
or empty.
whatever the current work constructor throws or the work()
throws.
void run_queued_closures();
reschedule the enqueued works.
whatever the current work constructor throws or the work()
throws.