StephenCleary / AsyncEx

A helper library for async/await.
MIT License
3.49k stars 358 forks source link

AsyncContext.Run vs TaskUtil.Await #237

Closed Timovzl closed 3 years ago

Timovzl commented 3 years ago

Although this question does not concern only AsyncEx, I thought this would be the most reliable place to ask.

Where AsyncEx provides AsyncContext.Run, the well-known MassTransit package happens to provide TaskUtil.Await. The intent seems to be largely the same.

Both methods seem to succeed in avoiding most deadlocks. If I'm not mistaken, both also help avoid thread pool starvation (by letting only one thread pick up the continuations?).

Still, the implementations are quite different. (For one thing, TaskUtil.Await uses Thread.Sleep in a loop.) I'm trying to understand the consequences of the differences, but I'm not familiar enough with the concepts.

I'm curious what practical consequences I should expected when using either of the methods.

StephenCleary commented 3 years ago

From reading the code, it looks like TaskUtil.Await begins executing its work on the current thread, but with a SynchronizationContext that passes any continuations off to a thread pool thread. The original thread is then blocked (Thread.Sleep in a loop) until the work completes running on thread pool thread(s).

AsyncContext.Run also takes over the current thread until the work is complete, but it works by scheduling all continuations onto that same thread, not on thread pool threads.

AFAICT that's the main difference.

Timovzl commented 3 years ago

Your reponse is much appreciated, @StephenCleary!

Do you expect any performance difference between Thread.Sleep(3) in a loop vs. GetAwaiter().GetResult()?

P.S. MassTransit's implementation seems to use a single thread as well, judging by its SingleThreadSynchronizationContext.

StephenCleary commented 3 years ago

If you need to block, then blocking is generally more performant than busy waiting. I suspect (but am not sure) that they may be doing busy waiting in case the original thread is a UI thread, in which case busy waiting enables STA pumping. IIRC pumping might be done by the Thread.Sleep call.

SingleThreadSynchronizationContext uses a ChannelExecutor bounded to a single item at a time. ChannelExecutor runs its consumer task on a thread pool thread - and the consumer is asynchronous, so it may switch threads at any await. Finally, the actual continuations themselves are also wrapped in a thread pool thread.

So, I haven't actually run the code, but I'm quite sure that SingleThreadExecutionContext only ensures that one continuation at a time may be run, and allows them to run on any available thread pool thread.