Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Add Task.WhenAny overloads with CancellationToken #37505

Open
pentp opened this issue Jun 5, 2020 · 6 comments
Open

Proposal: Add Task.WhenAny overloads with CancellationToken #37505

pentp opened this issue Jun 5, 2020 · 6 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Threading.Tasks
Milestone

Comments

@pentp
Copy link
Contributor

pentp commented Jun 5, 2020

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 to Task.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 similar WaitAny overloads for sync-over-async code already.

Proposed API

namespace System.Threading.Tasks {
public class Task {
  // omitted "public static" for clarity

  Task<Task> WhenAny(IEnumerable<Task> tasks);
+ Task<Task?> WhenAny(IEnumerable<Task> tasks, CancellationToken cancellationToken);
  Task<Task> WhenAny(Task task1, Task task2);
+ Task<Task?> WhenAny(Task task1, Task task2, CancellationToken cancellationToken);
  Task<Task> WhenAny(params Task[] tasks);
+ Task<Task?> WhenAny(params Task[] tasks, CancellationToken cancellationToken);

  Task<Task<TResult>> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks);
+ Task<Task<TResult>?> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks, CancellationToken cancellationToken);
  Task<Task<TResult>> WhenAny<TResult>(Task<TResult> task1, Task<TResult> task2);
+ Task<Task<TResult>?> WhenAny<TResult>(Task<TResult> task1, Task<TResult> task2, CancellationToken cancellationToken);
  Task<Task<TResult>> WhenAny<TResult>(params Task<TResult>[] tasks);
+ Task<Task<TResult>?> WhenAny<TResult>(Task<TResult>[] tasks, CancellationToken cancellationToken);

// single task variants that don't have a counterpart without the CT parameter
+ Task<Task?> WhenAny(Task task, CancellationToken cancellationToken);
+ Task<Task<TResult>?> WhenAny<TResult>(Task<TResult> task, CancellationToken cancellationToken);
}}

Usage Examples

Task<int> TryGetData(CancellationToken ct)
{
    Task<int> task1 = service.GetData();
    Task<int> task2 = cache.GetData();

    return Task.WhenAny(task1, task2, ct).Unwrap();
}

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 where WhenAny 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...

@pentp pentp added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Jun 5, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Threading.Tasks untriaged New issue has not been triaged by the area owner labels Jun 5, 2020
@ghost
Copy link

ghost commented Jun 5, 2020

Tagging subscribers to this area: @tarekgh
Notify danmosemsft if you want to be subscribed.

@stephentoub
Copy link
Member

Dup of #27722?

@pentp
Copy link
Contributor Author

pentp commented Jun 5, 2020

Not entirely, it's a separate proposal specifically for WhenAny

@tarekgh tarekgh removed the untriaged New issue has not been triaged by the area owner label Jun 5, 2020
@tarekgh tarekgh added this to the Future milestone Jun 5, 2020
@GSPP
Copy link

GSPP commented Jun 15, 2020

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.

@pentp
Copy link
Contributor Author

pentp commented Oct 1, 2020

Updated proposal with some minor fixes and switched to null Task result instead of canceled Tasks (moved to alternatives section).

@jnm2
Copy link
Contributor

jnm2 commented Oct 1, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Threading.Tasks
Projects
None yet
Development

No branches or pull requests

6 participants