AsyncLock icon indicating copy to clipboard operation
AsyncLock copied to clipboard

Don't marshal async continuations to the captured context

Open NetherGranite opened this issue 1 year ago • 2 comments
trafficstars

Currently, async/await continuations and Task.ContinueWith(...) continuations are marshalled back to the captured context (TaskScheduler or SynchronizationContext) as is the default behavior with these tools. I believe there is no reason for this in this library. An example of this is if I use this library in an ASP.NET Core web project currently, this library will post its continuations to the web framework's single-threaded UI synchronization context, resulting in worse performance and even deadlocks if asynchronous code is blocked on.

This resolves the issue by 1) using Task.ConfigureAwait(false) in async/await code to instruct the async state machines to schedule continuations onto the default thread pool and 2) passing TaskScheduler.Default to Task.ContinueWith(...) calls to ensure the continuations are run on the default thread pool.

Notably, Task.Run(...) schedules work onto the default thread pool by default, so no changes are needed there. Only continuations are subject to this "marshalling onto the captured context" behavior by default.

Resolves #17.

NetherGranite avatar Jul 01 '24 18:07 NetherGranite

Thanks for this PR. I'll take a look at this later and see how it works under the stress test.

Note that ASP.NET Core does not have the behavior you are talking about, the synchronization context is in desktop .NET GUI apps and legacy ASP.NET (not Core).

mqudsi avatar Jul 01 '24 19:07 mqudsi

Of course!

I'll take a look at this later and see how it works under the stress test.

It's very possible this will somehow perform worse under stress test as I didn't test it, but these changes instruct async state machines and the TPL to simply use TaskScheduler.Default rather than spending the extra time to discover what the currently captured task scheduler (or synchronization context in the case of async state machines) is and using that. This can be seen in ConfiguredTaskAwaitable, which executes a Task's continuation with a booleean parameter that skips the "current context" logic and goes straight to the thread pool, and in Task.ContinueWith(Action<Task>), which simply defers to the overload that takes a task scheduler and passes TaskScheduler.Current.

Note that ASP.NET Core does not have the behavior you are talking about

Sorry, I was referring to ASP.NET Core web projects with a UI aspect. For example, Blazor has a synchronization context, which is my current use case. Most if not all UI frameworks have one if I understand correctly.

NetherGranite avatar Jul 02 '24 12:07 NetherGranite

Sorry, I didn't merge this PR because I was worried about (non-existent) complications between AsyncLocal<T> and ConfigureAwait(false) then I forgot about it and added the calls to ConfigureAwait(false) myself in a commit earlier tonight.

I didn't use ConfigureAwait(false) in synchronous methods (where we use an async method as a hack) and I neglected to update the continuations with TaskScheduler.Default as you did here. If you would like to rebase on top of the latest code and update the PR to handle the continuations, I'll be happy to merge it so you get the credit for that. If you don't have the time for it any longer, no worries!

Apologies for the confusion.

mqudsi avatar Feb 23 '25 05:02 mqudsi

No worries at all! I take it you resolved this yourself 🙂

NetherGranite avatar Feb 25 '25 21:02 NetherGranite