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

Integrating Fibers with Nonblocking I/O

Overview

Nonblocking I/O is distinct from asynchronous I/O. A true async I/O operation promises to initiate the operation and notify the caller on completion, usually via some sort of callback (as described in Integrating Fibers with Asynchronous Callbacks).

In contrast, a nonblocking I/O operation refuses to start at all if it would be necessary to block, returning an error code such as EWOULDBLOCK. The operation is performed only when it can complete immediately. In effect, the caller must repeatedly retry the operation until it stops returning EWOULDBLOCK.

In a classic event-driven program, it can be something of a headache to use nonblocking I/O. At the point where the nonblocking I/O is attempted, a return value of EWOULDBLOCK requires the caller to pass control back to the main event loop, arranging to retry again on the next iteration.

Worse, a nonblocking I/O operation might partially succeed. That means that the relevant business logic must continue receiving control on every main loop iteration until all required data have been processed: a doubly-nested loop, implemented as a callback-driven state machine.

Boost.Fiber can simplify this problem immensely. Once you have integrated with the application's main loop as described in Sharing a Thread with Another Main Loop, waiting for the next main-loop iteration is as simple as calling this_fiber::yield().

Example Nonblocking API

For purposes of illustration, consider this API:

class NonblockingAPI {
public:
    NonblockingAPI();

    // nonblocking operation: may return EWOULDBLOCK
    int read( std::string & data, std::size_t desired);

    ...
};

Polling for Completion

We can build a low-level wrapper around NonblockingAPI::read() that shields its caller from ever having to deal with EWOULDBLOCK:

// guaranteed not to return EWOULDBLOCK
int read_chunk( NonblockingAPI & api, std::string & data, std::size_t desired) {
    int error;
    while ( EWOULDBLOCK == ( error = api.read( data, desired) ) ) {
        // not ready yet -- try again on the next iteration of the
        // application's main loop
        boost::this_fiber::yield();
    }
    return error;
}

Filling All Desired Data

Given read_chunk(), we can straightforwardly iterate until we have all desired data:

// keep reading until desired length, EOF or error
// may return both partial data and nonzero error
int read_desired( NonblockingAPI & api, std::string & data, std::size_t desired) {
    // we're going to accumulate results into 'data'
    data.clear();
    std::string chunk;
    int error = 0;
    while ( data.length() < desired &&
           ( error = read_chunk( api, chunk, desired - data.length() ) ) == 0) {
        data.append( chunk);
    }
    return error;
}

(Of course there are more efficient ways to accumulate string data. That's not the point of this example.)

Wrapping it Up

Finally, we can define a relevant exception:

// exception class augmented with both partially-read data and errorcode
class IncompleteRead : public std::runtime_error {
public:
    IncompleteRead( std::string const& what, std::string const& partial, int ec) :
        std::runtime_error( what),
        partial_( partial),
        ec_( ec) {
    }

    std::string get_partial() const {
        return partial_;
    }

    int get_errorcode() const {
        return ec_;
    }

private:
    std::string partial_;
    int         ec_;
};

and write a simple read() function that either returns all desired data or throws IncompleteRead:

// read all desired data or throw IncompleteRead
std::string read( NonblockingAPI & api, std::size_t desired) {
    std::string data;
    int ec( read_desired( api, data, desired) );

    // for present purposes, EOF isn't a failure
    if ( 0 == ec || EOF == ec) {
        return data;
    }

    // oh oh, partial read
    std::ostringstream msg;
    msg << "NonblockingAPI::read() error " << ec << " after "
        << data.length() << " of " << desired << " characters";
    throw IncompleteRead( msg.str(), data, ec);
}

Once we can transparently wait for the next main-loop iteration using this_fiber::yield(), ordinary encapsulation Just Works.

The source code above is found in adapt_nonblocking.cpp.


PrevUpHomeNext