runtime: System.Threading.Tasks.Task.WhenAny(IEnumerable) inconsistently does not use current scheduler for tasks
Description
There are 6 overloads for Task.WhenAny
:
WhenAny(IEnumerable<Task>)
WhenAny(Task[])
WhenAny(Task, Task)
WhenAny<TResult>(IEnumerable<Task<TResult>>)
WhenAny<TResult>(Task<TResult>[])
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)
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.
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. 😃