Interruption


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 fiber_interrupted exception using try-catch blocks. If you do this, interruption_caught() will return false.

However, catching the fiber_interrupted exception is not enough to disable interruptions and perform operations abstracted with suspending functions. As soon as you call the suspending function within the catch block, another fiber_interrupted exception will be throw (operation will not even start for what it is worth).

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

Example

When you do operations involving buffers, it’s a common requirement that you keep the buffer accessible until the operation completes.

void start(fiber::this_fiber this_fiber)
{
  // ...

  boost::system::error_code ec;
  BufferType buf;
  boost::asio::async_read(socket, buf, this_fiber[ec]);
  // ...
}

If you rely on the default (no) interrupter (as in the example), then you have a problem. buf might be destroyed due to stack unwinding on fiber_interrupted while the async_read() operation is in progress.

The solution is easy. Teach IOFiber how to interrupt the operation so the fiber wakeup (to throw fiber_interrupted) synchronizes with the cancellation of the underlying operation:

void start(fiber::this_fiber this_fiber)
{
  // ...

  // Must handle `operation_aborted` explicitly later
  this_fiber.interrupter = [&socket]() { socket.cancel(); };

  boost::system::error_code ec;
  BufferType buf;
  boost::asio::async_read(socket, buf, this_fiber[ec]);
  // ...
}

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

mutex::lock() is a suspension point, but it is not an interruption point. This is by-design. The primary problem solved by interruption requests is freeing fibers stalled in operations that might never finish, not “recovering” fibers on a deadlock. Code your programs correctly.

On the other hand, condition_variable::wait() might be associated with external events so it is an interruption point. Refer to its documentation for more details.

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 this_fiber.yield() is semantically closer to POSIX’s sched_yield(), you can use it to achieve the effects of POSIX’s pthread_testcancel().

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 pthread_cleanup_push().

Important
fiber::interrupt() is guaranteed to NOT be an interruption point.

1. Check background(7) for a more comprehensive list.