![]() |
Home | Libraries | People | FAQ | More |
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.