-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Add awaitable ThreadPool.SwitchTo() #20025
Comments
SwitchTo? |
Was keeping in line with |
I agree this is a pretty reasonable thing to add. In the original async/await CTP, we actually had methods for doing this, IIRC as extensions on a TaskScheduler and SynchronizationContext, e.g. await TaskScheduler.Default.SwitchTo(); // continuation will be queued to the thread pool We removed these because you couldn't use an await inside of a catch/finally, which meant it was difficult to return to the original context if you were trying to do something structured like: var origScheduler = TaskScheduler.Current;
await TaskScheduler.Default.SwitchTo();
try
{
... // runs on thread pool
}
finally { await origScheduler; } and as a result you could end up in situations where error related logic would run on the "wrong" context; in a sense we were concerned we were adding APIs that would lead to dangerous situations. However, C# has since gained the ability to have awaits in catch/finally blocks, making that largely a moot argument. I would personally also err on the side of using a name more like SwitchTo. The Yield naming was intended to suggest yielding and coming back to the current context, so using it to mean "go to that other context" might be a little confusing. |
SwitchTo sounds fine to me.
If we ever get async disposable maybe that pattern could be more natural. await using (TaskScheduler.Current.Preserve())
{
await TaskScheduler.Default.SwitchTo();
// Do thread pool stuff
} // Revert to the original scheduler |
Should it check if already on threadpool/scheduler and immediately complete if so? |
It would depend on your scenario. For example, if you were using it to ensure that a caller of your async API wasn't blocked while doing some long computation, then you'd want it to always queue. But if you were using it to ensure you were running on a context that owned done resources only accessible to that context, you wouldn't care and for perf might want it to nop. |
@davidfowl, now we have
But I feel it is no different from:
Is there any subtle difference? |
Potentially perf, but functionally I'd expect them to be the same. That said, the former doesn't exist, so it's hard to say for sure. |
Wouldn't it be possible to have the A use case is where you have a limited access to a resource like a database connection or external API service. Together with the async disposable, you can create something like the following: private TaskScheduler _limitedResource = new LimitedTaskScheduler(10);
await using (Task.SwitchTo(_limitedResource))
{
// Use resource with limited concurrent access
} Placing the An overload to this method can accept a boolean indicating whether you must always switch or only if you are running on a different context than the provided one. |
Could the API take a cancellation token? Making the SwitchTo implementation aware of cancellation could avoid queuing work on the thread pool when already canceled. It would be a single call, replacing: cancellationToken.ThrowIfCancellationRequested();
await TaskScheduler.Default.SwitchTo();
cancellationToken.ThrowIfCancellationRequested(); Usage: public static async IAsyncEnumerable<string> EnumerateFilesAsync(
string path,
string searchPattern,
SearchOption searchOption,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
await TaskScheduler.Default.SwitchTo(cancellationToken);
foreach (var file in Directory.EnumerateFiles(path, searchPattern, searchOption))
{
yield return file;
await TaskScheduler.Default.SwitchTo(cancellationToken);
}
} Maybe not relevant to how .NET Core would implement it, but this implementation actually passes the cancellation token to TaskFactory.StartNew when switching to a non-default task scheduler: https://gist.github.com/jnm2/46a642d2c9f2794ece0b095ba3d96270 |
await using (TaskScheduler.Current.Preserve())
{
await TaskScheduler.Default.SwitchTo();
// Do thread pool stuff
} // Revert to the original scheduler It could be simplified to await using (await TaskScheduler.Default.SwitchTo())
{
} |
I don't like having |
It must implement it via async disposable pattern, with |
The IDE will (should!) still warn when a type follows the async disposable pattern and is not disposed, and I wouldn't like that for this API. Maybe |
Is It'd be really useful where we deal we a 3rd party code that we don't want to execute on any custom synchronization context. // switch to the thread pool explicitly for the rest of the async method
await TaskScheduler.Default.SwitchTo();
await RunOneWorkflowAsync();
await RunAnotherWorkflowAsync(); // execute RunOneWorkflowAsync on the thread pool
// and stay there for the rest of the async method
await Task.Run(RunOneWorkflowAsync).ConfigureAwait(false);
await RunAnotherWorkflowAsync(); await Task.Run(async () =>
{
// start on the thread pool
await RunOneWorkflowAsync();
await RunAnotherWorkflowAsync();
}).ConfigureAwait(false);
// continue on the thread pool for the rest of the async method // start on whatever the current synchronization context is
await RunOneWorkflowAsync().ConfigureAwait(false);
// continue on the thread pool for the rest of the async method,
// unless everything inside `RunOneWorkflowAsync` has completed synchronously
await RunAnotherWorkflowAsync(); I have an experimental implementation of |
@noseratio the syntax you want already works if you reference the Microsoft.VisualStudio.Threading Nuget package. |
@AArnott thanks for the pointer, didn't know that had also been open-sourced! 👍 It's good to see the I'm curious to know if this |
I don't know why I'd want to suppress ExecutionContext flow. But I'm open to learning. The whole distinction between safe and unsafe awaiters is one I'm not particularly strong on. We only explicitly call |
This explained things for me: https://devblogs.microsoft.com/pfxteam/whats-new-for-parallelism-in-net-4-5-beta/ |
I don't know why you'd want to suppress the execution context flow as part of this either. Seems entirely unrelated here. Maybe there's some common scenario I'm missing? |
I think we could do a better job of suppressing ExecutionFlow in some of our awaiters: microsoft/vs-threading#689 |
@davidfowl and @AArnott I might be wrong, but I think it might be a sensible optimization for This is not a concern for |
That makes sense. I usually use |
See microsoft/vs-threading#689 (comment). Someone could do the appropriate measurements, but my guess is that for .NET Core this would actually be a net negative rather than positive. |
That comment is a great insight, thank you. I suppose it explains why there so few uses of |
Yes, it's use now isn't about throughout or allocation, but entirely about object lifetime, i.e. ensuring that ExecutionContext isn't captured unnecessarily into something that may live for a long time, that doesn't need access to the context (and won't be calling unknown code that might), and thus shouldn't forcibly extend the lifetime of values stored into async locals captured by the EC. |
I've seen occasionally the await Task.Yield().ConfigureAwait(false); It would have identical functionality with the proposed here: await TaskScheduler.Default.SwitchTo(alwaysYield: true); The first line is more concise and, to me, more familiar. But to be honest both are lacking the critical component: |
In .NET 8.0 we can do: await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); But its pretty verbose. A short and concise helper method like this would be nice to have: await Task.YieldNoContext(); |
I would just add Maybe only the old-school boolean overload though, Or maybe just |
TBH I think we should have both. Both With vs-threading, you can switch to any |
Task.Run
breaks the execution flow by forcing the code into a delegate, it's basically callback hell (since it's the first callback in the chain). Instead, for the situations where we want to stay in the await pattern,ThreadPool.Yield()
would be a nice to have API. It would turn code that looks like this:/cc @stephentoub
The text was updated successfully, but these errors were encountered: