...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
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()
.
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); ... };
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; }
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.)
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.