...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
As noted in the Scheduling section, by default
Boost.Fiber uses its own round_robin
scheduler
for each thread. To control the way Boost.Fiber
schedules ready fibers on a particular thread, in general you must follow several
steps. This section discusses those steps, whereas Scheduling
serves as a reference for the classes involved.
The library's fiber manager keeps track of suspended (blocked) fibers. Only when a fiber becomes ready to run is it passed to the scheduler. Of course, if there are fewer than two ready fibers, the scheduler's job is trivial. Only when there are two or more ready fibers does the particular scheduler implementation start to influence the overall sequence of fiber execution.
In this section we illustrate a simple custom scheduler that honors an integer fiber priority. We will implement it such that a fiber with higher priority is preferred over a fiber with lower priority. Any fibers with equal priority values are serviced on a round-robin basis.
The full source code for the examples below is found in priority.cpp.
The first essential point is that we must associate an integer priority with each fiber.[11]
One might suggest deriving a custom fiber
subclass to store such
properties. There are a couple of reasons for the present mechanism.
fibers::async()
.) Higher-level
libraries might introduce additional such wrapper functions. A custom scheduler
must associate its custom properties with every fiber
in the thread, not only the ones explicitly launched by instantiating a
custom fiber
subclass.
fiber
subclass, we would have to hunt down and modify every place that launches
a fiber on that thread.
fiber
class is actually just a handle to internal context
data.
A subclass of fiber
would
not add data to context
.
The present mechanism allows you to “drop in” a custom scheduler with its attendant custom properties without altering the rest of your application.
Instead of deriving a custom scheduler fiber properties subclass from fiber
,
you must instead derive it from fiber_properties
.
class priority_props : public boost::fibers::fiber_properties { public: priority_props( boost::fibers::context * ctx): fiber_properties( ctx), priority_( 0) { } int get_priority() const { return priority_; } // Call this method to alter priority, because we must notify // priority_scheduler of any change. void set_priority( int p) { // Of course, it's only worth reshuffling the queue and all if we're // actually changing the priority. if ( p != priority_) { priority_ = p; notify(); } } // The fiber name of course is solely for purposes of this example // program; it has nothing to do with implementing scheduler priority. // This is a public data member -- not requiring set/get access methods -- // because we need not inform the scheduler of any change. std::string name; private: int priority_; };
Your subclass constructor must accept a |
|
Provide read access methods at your own discretion. |
|
It's important to call |
|
A property that does not affect the scheduler does not need access methods. |
Now we can derive a custom scheduler from algorithm_with_properties<>
,
specifying our custom property class priority_props
as the template parameter.
class priority_scheduler : public boost::fibers::algo::algorithm_with_properties< priority_props > { private: typedef boost::fibers::scheduler::ready_queue_type rqueue_t; rqueue_t rqueue_; std::mutex mtx_{}; std::condition_variable cnd_{}; bool flag_{ false }; public: priority_scheduler() : rqueue_() { } // For a subclass of algorithm_with_properties<>, it's important to // override the correct awakened() overload. virtual void awakened( boost::fibers::context * ctx, priority_props & props) noexcept { int ctx_priority = props.get_priority(); // With this scheduler, fibers with higher priority values are // preferred over fibers with lower priority values. But fibers with // equal priority values are processed in round-robin fashion. So when // we're handed a new context*, put it at the end of the fibers // with that same priority. In other words: search for the first fiber // in the queue with LOWER priority, and insert before that one. rqueue_t::iterator i( std::find_if( rqueue_.begin(), rqueue_.end(), [ctx_priority,this]( boost::fibers::context & c) { return properties( &c ).get_priority() < ctx_priority; })); // Now, whether or not we found a fiber with lower priority, // insert this new fiber here. rqueue_.insert( i, * ctx); } virtual boost::fibers::context * pick_next() noexcept { // if ready queue is empty, just tell caller if ( rqueue_.empty() ) { return nullptr; } boost::fibers::context * ctx( & rqueue_.front() ); rqueue_.pop_front(); return ctx; } virtual bool has_ready_fibers() const noexcept { return ! rqueue_.empty(); } virtual void property_change( boost::fibers::context * ctx, priority_props & props) noexcept { // Although our priority_props class defines multiple properties, only // one of them (priority) actually calls notify() when changed. The // point of a property_change() override is to reshuffle the ready // queue according to the updated priority value. // 'ctx' might not be in our queue at all, if caller is changing the // priority of (say) the running fiber. If it's not there, no need to // move it: we'll handle it next time it hits awakened(). if ( ! ctx->ready_is_linked()) { return; } // Found ctx: unlink it ctx->ready_unlink(); // Here we know that ctx was in our ready queue, but we've unlinked // it. We happen to have a method that will (re-)add a context* to the // right place in the ready queue. awakened( ctx, props); } void suspend_until( std::chrono::steady_clock::time_point const& time_point) noexcept { if ( (std::chrono::steady_clock::time_point::max)() == time_point) { std::unique_lock< std::mutex > lk( mtx_); cnd_.wait( lk, [this](){ return flag_; }); flag_ = false; } else { std::unique_lock< std::mutex > lk( mtx_); cnd_.wait_until( lk, time_point, [this](){ return flag_; }); flag_ = false; } } void notify() noexcept { std::unique_lock< std::mutex > lk( mtx_); flag_ = true; lk.unlock(); cnd_.notify_all(); } };
See ready_queue_t. |
|
You must override the |
|
|
|
You must override the |
|
You must override |
|
Overriding |
|
Your |
Our example priority_scheduler
doesn't override algorithm_with_properties::new_properties()
:
we're content with allocating priority_props
instances on the heap.
You must call use_scheduling_algorithm()
at the start
of each thread on which you want Boost.Fiber
to use your custom scheduler rather than its own default round_robin
.
Specifically, you must call use_scheduling_algorithm()
before performing any other Boost.Fiber
operations on that thread.
int main( int argc, char *argv[]) { // make sure we use our priority_scheduler rather than default round_robin boost::fibers::use_scheduling_algorithm< priority_scheduler >(); ... }
The running fiber can access its own fiber_properties
subclass
instance by calling this_fiber::properties()
. Although
properties<>()
is a nullary function, you must pass, as a template parameter, the fiber_properties
subclass.
boost::this_fiber::properties< priority_props >().name = "main";
Given a fiber
instance still connected with a running fiber (that
is, not fiber::detach()
ed), you may access that fiber's properties
using fiber::properties()
. As with boost::this_fiber::properties<>()
, you must pass your fiber_properties
subclass as the template
parameter.
template< typename Fn > boost::fibers::fiber launch( Fn && func, std::string const& name, int priority) { boost::fibers::fiber fiber( func); priority_props & props( fiber.properties< priority_props >() ); props.name = name; props.set_priority( priority); return fiber; }
Launching a new fiber schedules that fiber as ready, but does not
immediately enter its fiber-function. The current fiber
retains control until it blocks (or yields, or terminates) for some other reason.
As shown in the launch()
function above, it is reasonable to launch a fiber and immediately set relevant
properties -- such as, for instance, its priority. Your custom scheduler can
then make use of this information next time the fiber manager calls algorithm_with_properties::pick_next()
.
[11] A previous version of the Fiber library implicitly tracked an int priority for each fiber, even though the default scheduler ignored it. This has been dropped, since the library now supports arbitrary scheduler-specific fiber properties.