Skip to content

Instantly share code, notes, and snippets.

@kirkshoop
Created December 3, 2021 21:14
Show Gist options
  • Save kirkshoop/874e27158024ef2dfc5084f824a0f1f2 to your computer and use it in GitHub Desktop.
Save kirkshoop/874e27158024ef2dfc5084f824a0f1f2 to your computer and use it in GitHub Desktop.
async_scope proposal
// async_scope has a single concern - it provides a scope in which senders
// can be spawned without waiting for each sender to complete.
// async_scope is intended to compose inside of non-blocking operations
// (none of it's methods are allowed to block).
//
// Requirements:
//
// - async_scope must be empty when the destructor runs.
// - senders passed to spawn must not require any CPOs on the receiver
// connected to them.
// - senders passed to spawn must be never_blocking.
// -
//
template<typename AsyncStorageProvider>
struct async_scope {
~async_scope(); // UB if outstanding schedulers are used after destruction. Terminates if there is outstanding work.
async_scope();
explicit async_scope(AsyncStorageProvider); // use specified storage provider
// This is the lazy version that completes the storage_sender<>
// with void when storage has been secured and the operation has
// been started. This allows bounded calls to spawn() to apply
// back-pressure. For example: spawn(..).repeat_effect_until(..)
// will only spawn the next sender once the previous sender has
// been started. storage_sender<> supports cancellation. only
// this spawned sender will be cancelled, not the async_scope.
spawn(sender) const&->storage_sender<>;
// This is the fire and forget version that starts the async
// storage request and returns (potentially before the sender
// has been started).
spawn_now(sender) const&->void;
// This is the lazy version that completes the storage_sender<>
// with a future_sender<> when storage has been secured and the
// operation has been started. This allows bounded calls to spawn()
// to apply back-pressure.
// For example: spawn_future(..).repeat_effect_until(..) will only
// spawn the next sender once the previous sender has been started.
// storage_sender<> and future_sender<> support cancellation. only
// this spawned sender will be cancelled, not the async_scope.
// future_sender<> start() is in a race with the completion of the
// sender that has already been started. The race will be resolved
// by the future_sender<> state. Storage for the future_sender<>
// state can be reserved in the original storage.
spawn_future(sender) const&->storage_sender<future_sender<Values…>>;
// empty_sender completes with void when all spawned senders have
// completed. An async_scope can be empty more than once. The
// intended usage is to spawn all the senders and then start the
// empty_sender to know when all spawned senders have completed.
// Another supported usage is to use request_stop on async_scope
// to prevent further senders from being spawned, and then start
// the empty_sender.
empty() const&->empty_sender;
// cancellation scope for this instance, cancelling this does not
// cancel the spawned senders.
get_stop_source() & ->stop_source; // would expect inline_stop_source
get_stop_token() const& ->stop_token; // would expect inline_stop_token
request_stop() & ->void; // prevents further calls to spawn()
};
@LeeHowes
Copy link

LeeHowes commented Dec 3, 2021

request_stop does or does not cancel? Does it only stop calls to spawn, and you need to use the stop source to cancel the work?

Should we be able to cancel the empty_sender to help make sure we both drawn and stop spawning in a unified way?

@LeeHowes
Copy link

LeeHowes commented Dec 3, 2021

It isn't clear in the comment, but I think spawn_now() doesn't guarantee to start() the sender, but does guarantee to have completed the allocation. Here it says it starts the storage request but presumably it has to wait for it too.

What happens if that storage request fails, does this function throw?

@kirkshoop
Copy link
Author

request_stop does or does not cancel?

request_stop does not cancel, unless its token is composed into a sender that is passed to spawn.
with_query_value(sender, unifex::get_stop_token, unifex::get_stop_token(scope))

Should we be able to cancel the empty_sender

yes, that makes sense to me.

I think spawn_now() doesn't guarantee to start() the sender, but does guarantee to have completed the allocation

The wording was carefully crafted, but must need improvement.
spawn_now() does not guarantee to complete the allocation before it returns.
The storage_sender<> is created and started internally and then spawn returns.
When the storage_sender<> completes, the sender is started - all this is potentially async WRT the call to spawn.

What happens if that storage request fails, does this function throw?

If the storage fails inside of spawn, then yes, spawn will throw.
If the storage fails asynchronously then the only place to deliver that error is to the empty_sender<> once it is started. I am not sure that is the correct result. If the error is delivered to the empty_sender, then I think the error should also request_stop on the whole async_scope, thus preventing any further spawn calls, and empty() must be repeated until it succeeds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment