Open pentp opened 4 years ago
Tagging subscribers to this area: @tarekgh Notify danmosemsft if you want to be subscribed.
Not entirely, it's a separate proposal specifically for WhenAny
Came here to report this. My use case: I am repeatedly subscribing to a bunch of operations that represent events. Most of these subscriptions are unsubscribed fairly quickly.
If the WhenAny
task cannot be canceled then continuations will keep piling up indefinitely. That's why I'd like to pass in a token to cancel the WhenAny
task and drop the continuations.
Updated proposal with some minor fixes and switched to null Task result instead of canceled Tasks (moved to alternatives section).
Null task results could lead people to await directly without checking for null in codebases that aren't on C# 8 with NRTs enabled. Given the signature, I'd have expected a canceled outer task rather than a succeeded task with a result of null.
Background and Motivation
It's a relatively common pattern to await on tasks which don't support cancellation (yet). One workaround is to pass
new Task(() => {}, cancellationToken)
as one of the tasks toTask.WhenAny
, but I've also seen a variant where the token registration sets a TCS result and the TCS.Task is passed to WhenAny and even more complicated variants, but all of them are pretty inefficient and make the code unwieldy. There are somewhat similarWaitAny
overloads for sync-over-async code already.Proposed API
Usage Examples
Alternative Designs
Instead of setting a null result when canceled, could instead mark the
WhenAny
task as canceled, but this is inefficient since it throws cancellation exceptions in the common case whereWhenAny
is directly awaited. Also this has no benefits when Unwrap is used as it behaves almost the same for null task results and cancelled tasks (the only difference is cancellationToken value in the task state).Alternative #27722: The same effect could be achieved with a general purpose WithCancellation API on tasks, which would support the previous example like this:
return Task.WhenAny(task1.WithCancellation(ct), task2.WithCancellation(ct)).Unwrap();
This could be more efficient actually, especially when the token isn't cancellable or the result is synchronously available already.Risks
Aside from the API count increase, I see the main risk that if new CancellationTokenSource instances are created and used with these APIs, it would be quite easy to end up not disposing them properly (or disposing them too early). To avoid such pitfalls for the most common cases, a general purpose
WithTimeout(int/TimeSpan)
API on Task would be very useful, but it would cover only timeouts instead of all cancellations...