Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

PrevUpHomeNext

Customization

Overview

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.

Custom Property Class

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.

  1. Boost.Fiber provides a number of different ways to launch a fiber. (Consider 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.
  2. Consider a large existing program that launches fibers in many different places in the code. We discover a need to introduce a custom scheduler for a particular thread. If supporting that scheduler's custom properties required a particular fiber subclass, we would have to hunt down and modify every place that launches a fiber on that thread.
  3. The 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), 1
        priority_( 0) {
    }

    int get_priority() const {
        return priority_; 2
    }

    // Call this method to alter priority, because we must notify
    // priority_scheduler of any change.
    void set_priority( int p) { 3
        // 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; 4
private:
    int priority_;
};

1

Your subclass constructor must accept a context* and pass it to the fiber_properties constructor.

2

Provide read access methods at your own discretion.

3

It's important to call notify() on any change in a property that can affect the scheduler's behavior. Therefore, such modifications should only be performed through an access method.

4

A property that does not affect the scheduler does not need access methods.

Custom Scheduler Class

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_type1   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.
    2virtual void awakened( boost::fibers::context * ctx, priority_props & props) noexcept {
        int ctx_priority = props.get_priority(); 3
        // 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);
    }

    4virtual 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;
    }

    5virtual bool has_ready_fibers() const noexcept {
        return ! rqueue_.empty();
    }

    6virtual 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()) { 7
            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();
    }
};

1

See ready_queue_t.

2

You must override the algorithm_with_properties::awakened() method. This is how your scheduler receives notification of a fiber that has become ready to run.

3

props is the instance of priority_props associated with the passed fiber ctx.

4

You must override the algorithm_with_properties::pick_next() method. This is how your scheduler actually advises the fiber manager of the next fiber to run.

5

You must override algorithm_with_properties::has_ready_fibers() to inform the fiber manager of the state of your ready queue.

6

Overriding algorithm_with_properties::property_change() is optional. This override handles the case in which the running fiber changes the priority of another ready fiber: a fiber already in our queue. In that case, move the updated fiber within the queue.

7

Your property_change() override must be able to handle the case in which the passed ctx is not in your ready queue. It might be running, or it might be blocked.

Our example priority_scheduler doesn't override algorithm_with_properties::new_properties(): we're content with allocating priority_props instances on the heap.

Replace Default Scheduler

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 >();
    ...
}

Use Properties

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 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.


PrevUpHomeNext