runtime: System.Threading.Tasks.Task.WhenAny(IEnumerable) inconsistently does not use current scheduler for tasks

Description

There are 6 overloads for Task.WhenAny:

  1. WhenAny(IEnumerable<Task>)
  2. WhenAny(Task[])
  3. WhenAny(Task, Task)
  4. WhenAny<TResult>(IEnumerable<Task<TResult>>)
  5. WhenAny<TResult>(Task<TResult>[])
  6. WhenAny<TResult>(Task<TResult>, Task<TResult>)

It appears that 1, 2, 3, and 6 properly use the current task scheduler only and never the default scheduler, but that 4 and 5 use ContinueWith on the default scheduler to schedule the casting. To make this more confusing, if you call 5 with an array of 2 it uses a different scheduler than calling 5 with an array of 3.

For my use case, I have to prevent things from getting on the default scheduler for determinism (and write an analyzer for this). This is easy to know when it’s ContinueWith or Run or Start or whatever which all have predictable scheduler choice (be it default or current). But for WhenAny it’s not easily predictable.

See https://github.com/dotnet/runtime/blob/90c5f0550545f8d7cdf058377571e7b02d45f005/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L6561-L6562 and https://github.com/dotnet/runtime/blob/90c5f0550545f8d7cdf058377571e7b02d45f005/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L6612-L6613.

Reproduction Steps

With a custom scheduler as current, call await Task.WhenAny(new[] { Task.FromResult("a"), Task.FromResult("b") }) and confirm the default scheduler is not used for continuation but await Task.WhenAny(new[] { Task.FromResult("a"), Task.FromResult("b"), Task.FromResult("c") }) does.

I caught this via my TplEventSource listener.

Expected behavior

Any .NET task (or task factory) calls should clearly use one scheduler or another, not mix them. And the documentation should say as much. Maybe the continue-with can use the first task’s scheduler?

(or I am wrong, maybe this is the expected behavior, and I should never expect to know that default scheduler will not be used internally)

Actual behavior

Depending on which WhenAny call you make, you may end up running something on the default scheduler.

Regression?

No response

Known Workarounds

I could stop using event source listeners to catch people putting tasks on the wrong scheduler, but it leaves users open to accidents.

Configuration

No response

Other information

No response

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 15 (8 by maintainers)

Most upvoted comments

I was just hoping that this one inconsistency in this one method in this one situation could be fixed to use the current scheduler the way the rest of this method uses the current scheduler.

Can you think of any way I can tell developers not to use the thread pool but still use the task framework?

Nope. Effectively every async method anywhere in the core libraries might end up using the thread pool. It’s an inherent part of the runtime. It’d be like trying to avoid ever touching the GC. 😃