cppcoro icon indicating copy to clipboard operation
cppcoro copied to clipboard

Are coroutines cancellable?

Open Matthias247 opened this issue 5 years ago • 1 comments

Hi @lewissbaker, this is more of a question, since you seem to be well aware about how C++ coroutines work.

We recently talked about cancellation of coroutines for Rust at https://trio.discourse.group/t/structured-concurrency-in-rust/73. Thereby we came to the conclusion that the requirements for a synchronous destructor (which allows for immediate cancellation) makes it hard to implement some abstractions, e.g. over IOCP operation in windows. Most other coroutine frameworks (e.g. async functions in Javascript, C# or Kotlin) are not cancellable, and could wrap those operations. Now I was wondering how C++ behaves here.

As far as I e.g. saw in the source code for https://github.com/lewissbaker/cppcoro/blob/master/lib/async_manual_reset_event.cpp the coroutines here are not cancellable. Otherwise if a waiter would be cancelled, the setter would deal with a dangling pointer. Is that generally the case, that the coroutines - either in C++ in general or this repository - have run-to-completion semantics?

I have seen that the C++ specification specifies a destroy() method on the coroutine frame, which can be called when the coroutine is suspended as far as I understand. This would also imply some kind of synchronous destruction for me. Is the intention here that there are different classes of coroutines? Ones like task, which are not cancellable, and ones like generator which are cancellable?

Matthias247 avatar Mar 11 '19 06:03 Matthias247

Within cppcoro there are two flavours of cancellation that are used with C++ coroutines.

The first is the generator model. This is where a producer suspends after producing a value and resumes the consumer which then has the decision of whether to ask for the next value, resuming the producer, or to cancel the generator. When the generator is cancelled it calls the .destroy() method on the handle which synchronously calls the destructors of all in-scope variables, cleaning up any resources. In this model the consumer is active and the producer is suspended.

The second model occurs when the consumer is currently suspended waiting for the producer to produce a value. In this model the consumer cannot actively perform an action to interrupt/cancel the producer. Instead, the producer passes a cancellation_token into the operation which can be used to communicate a request to cancel/stop the operation. The producer can register a callback with the cancellation_token to allow it to actively interrupt the operation if cancellation is requested. With this model, as the consumer is suspended waiting for the producer, there needs to be some other concurrent operation that is able to actively request cancellation. Once cancellation is requested, the producer is interrupted but will still run to completion, hopefully in a timely manner. Once the producer completes the consumer is resumed, possibly with kind of result that indicates it was cancelled.

While the second model allows the producer to perform some asynchronous cleanup, the use of .destroy() to implement the first model means that it cannot perform asynchronous cleanup operations.

I have been exploring some alternatives that would allow an async_generator to perform async cleanup but it involves introducing another asynchronous operation that can be used to wait until cleanup completes after the destructor of the generator signals a request to cancel. See https://github.com/lewissbaker/cppcoro/blob/async_streams/test/async_stream_tests.cpp for an example.

I've also been exploring some ideas for extending the C++ coroutines language feature to incorporate async cleanup / async RAII patterns. There is some early ideas here https://github.com/lewissbaker/papers/issues/4 but I've also been exploring some other ideas as well.

lewissbaker avatar Mar 27 '19 06:03 lewissbaker