If a fiber is stalled waiting for some IO request that might never complete you still can use the interruption API to stop the fiber. The fiber interruption API was inspired by the POSIX thread cancellation API (and Boost.Thread)[1].
Interrupting a fiber
If you want to interrupt a fiber, call fiber::interrupt()
. An interruption
request will be queued for near delivery to interrupt the fiber.
The fiber will react to the interruption request when it next calls a function that is a suspension point (or if it is currently suspended whilst executing one). If the target fiber has disabled interruption, then the interruption request remains queued until the fiber enables interruption and reaches a new suspension point.
Note
|
The semantics are the same of a POSIX’s PTHREAD_CANCEL_DEFERRED thread.
|
By default, the interruption request is delivered by throwing a
fiber_interrupted
exception originating at the suspension point. Unless this
exception is caught inside the interrupted fiber’s start-function, the stack
unwinding process (as with any other exception) causes the destructors with
automatic storage duration to be executed. Unlike other exceptions, when
fiber_interrupted
is propagated out of fiber’s start-function, this does not
cause the call to std::terminate
; the effect is as though the fiber’s
start-function has returned normally.
To avoid accidental trap of the fiber_interrupted
exception, this class
doesn’t inherit std::exception
.
You can check whether the fiber finished by normal means or by letting the
fiber_interrupted
exception escape its stack by using
fiber::interruption_caught()
(after joining it). If the fiber_interrupted
exception escaped the stack, then fiber::interruption_caught()
will return
true
.
Note
|
fiber::interruption_caught() exposes the information that POSIX
otherwise offers through PTHREAD_CANCELED .
|
Reacting to interruption requests
When a fiber is interrupted and reaches a suspension point, a
fiber_interrupted
exception will be throw and you’ll be unable to perform any
operation that would otherwise suspend the fiber.
Note
|
It is safe to catch and swallow the However, catching the |
Disabling interruptions
If you wish to temporarily disable interruptions and stay capable of using
suspending functions, just instantiate an object of the class
fiber::this_fiber::disable_interruption
. Objects of this class disable
interruption for the fiber that created them on construction, and restore the
interruption state to whatever it was before on destruction:
void f(fiber::this_fiber this_fiber)
{
// interruption enabled here
{
fiber::this_fiber::disable_interruption di{this_fiber};
// interruption disabled
{
fiber::this_fiber::disable_interruption di2{this_fiber};
// interruption still disabled
} // di2 destroyed, interruption state restored
// interruption still disabled
} // di destroyed, interruption state restored
// interruption now enabled
}
The effects of an instance of fiber::this_fiber::disable_interruption
can be
temporarily reversed by constructing an instance of
fiber::this_fiber::restore_interruption
, passing in the
fiber::this_fiber::disable_interruption
object in question. This will restore
the interruption state to what it was when the
fiber::this_fiber::disable_interruption
object was constructed, and then
disable interruption again when the fiber::this_fiber::restore_interruption
object is destroyed.
void g(fiber::this_fiber this_fiber)
{
// interruption enabled here
{
fiber::this_fiber::disable_interruption di{this_fiber};
// interruption disabled
{
fiber::this_fiber::restore_interruption ri{di};
// interruption now enabled
} // ri destroyed, interruption disable again
} // di destroyed, interruption state restored
// interruption now enabled
}
Note
|
This provides the functionality that POSIX otherwise offers through
pthread_setcancelstate() .
|
async_*
functions
When a fiber is interrupted at an async_*
function, the underlying IO request
isn’t cancelled immediately. Usually, this is not a problem because in code
pieces like this (which should be most of the code out there)…
void start(fiber::this_fiber this_fiber)
{
boost::asio::steady_timer timer{this_fiber.get_executor().context()};
timer.expires_after(std::chrono::seconds(1));
boost::system::error_code ec;
timer.async_wait(this_fiber[ec]);
// ...
}
…if the fiber is interrupted, stack unwinding will play on and destruct the underlying IO objects causing the operations to be aborted.
However, this behaviour is less than ideal and for this reason you can customize
the interrupter by filling the this_fiber.interrupter
variable:
void start(fiber::this_fiber this_fiber)
{
boost::asio::steady_timer timer{this_fiber.get_executor().context()};
timer.expires_after(std::chrono::seconds(1));
this_fiber.interrupter = [&timer]() { timer.cancel(); };
boost::system::error_code ec;
timer.async_wait(this_fiber[ec]);
// ...
}
Unfortunately you still have to translate the
boost::asio::error::operation_aborted
error to a fiber_interrupted
exception
in this example on the suspension-site if you want to
fiber::interruption_caught()
to report exit reason correctly. To simplify
matters further, the following syntax sugar is provided:
#include <trial/iofiber/interrupter/asio/basic_waitable_timer.hpp>
void start(fiber::this_fiber this_fiber)
{
boost::asio::steady_timer timer{this_fiber.get_executor().context()};
timer.expires_after(std::chrono::seconds(1));
boost::system::error_code ec;
timer.async_wait(this_fiber[ec].with_intr(timer));
// ...
}
Note
|
ExampleWhen you do operations involving buffers, it’s a common requirement that you keep the buffer accessible until the operation completes.
If you rely on the default (no) interrupter (as in the example), then you have a
problem. The solution is easy. Teach IOFiber how to interrupt the operation so the fiber
wakeup (to throw
|
with_intr()
will set this_fiber.interrupter
and return a new token for the
async_*
operation. The interrupter is selected based on a trait mechanism —
interrupter_for
. This is the specialization for boost::asio::steady_timer
:
template<class Clock, class WaitTraits, class Executor>
struct interrupter_for<
boost::asio::basic_waitable_timer<Clock, WaitTraits, Executor>>
{
// Called within `with_intr()` before operation begins
template<class T>
static void assign(
T this_fiber,
boost::asio::basic_waitable_timer<Clock, WaitTraits, Executor>& timer)
{
this_fiber.interrupter = [&timer]() { timer.cancel(); };
}
// Called after result is ready (just before control goes back to the user)
template<class T, class... Args>
static void on_result(T this_fiber, boost::system::error_code& ec,
Args&...)
{
if (ec == boost::asio::error::operation_aborted)
throw trial::iofiber::fiber_interrupted();
}
};
on_result()
will be called on the fiber stack just before the result is
converted back to user-consumption (e.g. error codes will be converted to
exceptions if user has not used the this_fiber[ec]
syntax). Arguments are
passed to the trait by non-const ref and you can mutate them before delivery
to the user (e.g. if you clear the error code object, no exception will be
thrown).
You may specialize this trait for any object (or sequence of objects). They will
be forwarded from the call to with_intr()
to assign()
. It is expected that
library providers specialize it for their IO objects so you don’t have to worry.
Separate headers in this library will specialize this trait for Boost/ASIO IO
objects.
Important
|
If you want to force the use of this safer with_intr() syntax and
forbid plain this_fiber to work as a completion token, just define
TRIAL_IOFIBER_DISABLE_DEFAULT_INTERRUPTER .
|
Note
|
This token works only with asynchronous operations that make use of the
async_initiate() helper function. async_completion is not supported.
|
Suspension points
Fibers are scheduled cooperatively which means that CPU time is only transferred from one fiber to the next at defined suspension points. The suspension points for fibers are:
-
Boost.Asio’s
async_*
functions. -
this_fiber.yield()
. -
fiber::join(this_fiber)
.
Any functions that are compositions of these functions will also be functions
that serve as suspension points and might throw fiber_interrupted
.
Important
|
On the other hand, It follows POSIX design. |
Use this_fiber.yield()
if you want to create a suspension point so that a
fiber that is otherwise executing code that contains no suspension points will
respond to an interruption request.
Note
|
Although It diverges from POSIX because the C++ programmer already has to deal with
RAII and “unexpected” exceptions won’t fail to clean resources as it’d happen to
the C user who “forgot” to manage resources using |
Important
|
fiber::interrupt() is guaranteed to NOT be an interruption point.
|