Ritu Singh
Problem:
I believe we all would agree that sync over async is not a good idea. The TAP model greatly simplifies the asynchronous calls with async/await, and that's the way to go.
Unfortunately, for anyone stuck with some old technologies, this is not an option. For example, web services (asmx) in .NET Framework, have no support for TAP. In large legacy solutions, maintaining duplicate call chains (sync and async) can be very intimidating and tiresome. Moreover, you may be using "sync over async" approaches even if you're not aware. A lot of libraries that provide sync APIs, in essence internally are just wrappers over their async APIs. Examples Rebus, RestSharp, and others.
That's the main purpose of this post/question. Even though I have a fair understanding of async/await, I won't pretend I know all the corner cases (and there are a lot). So, I'd like to know the opinion of the experts on this topic. What are the possible drawbacks of this implementation?
>https://github.com/restsharp/RestSharp/blob/dev/src/RestSharp/AsyncHelpers.cs
Unlike many "sync over async" implementations, where usually they just spin up a new thread to avoid possible deadlock Task.Run(async () => {await ...}).Result; this implementation uses custom synchronization context to avoid deadlocks while remaining on the same thread. This offers the ability to even access HttpContext.Current (for NETFX apps).
I've tried this helper in various scenarios and it works fairly well. But, I'd like further opinion where it may go wrong.
So, how wrong is this? What are the pitfalls?
Solution:
I do have an >article discussing several possible approaches of sync-over-async along with a short discussion of the drawbacks of each.
For example, web services (asmx) in .NET Framework, have no support for TAP.
No, but ASMX does support APM, and you can write a >short interop layer to expose your core TAP logic to ASMX and remain async-all-the-way. There's some >examples on SO on how to do that and some >TAP-to-APM helpers in Nito.AsyncEx that make the interop layer very clean.
There are very few scenarios that require sync-over-async; it's usually a pragmatic decision to leave some of the code sync that really should be async, when converting the code isn't a business priority.
100% agree.
My preferred solution is a more modern version of the "boolean argument hack" in the >Brownfield Async article, which was originally shown to me by Stephen Toub during a technical review of that article. It's a technique that keeps the code either async-all-the-way or sync-all-the-way, but without requiring any duplication of logic.
More recently, Stephen Toub showed off the more modern version of the "boolean argument hack" in his >article on .NET 7 performance improvements (it's a huge article; search for "One final change related to reading and writing performance" to find the relevant section). I pulled out that gem and wrote a >more specific blog post about it. The code looks weird at first but it's a really powerful technique and that's what I recommend for all modern libraries.
This implementation installs a custom SynchronizationContext that contains a queue of work, queues the initial work item, and then synchronously waits for its queue of work to be complete. It's similar to AsyncContext from my AsyncEx library. I believe that implementation is originally sourced from >this old forum post, which I have seen copied around a few places, usually with a comment like "I have no idea how this works", which to be honest is kind of terrifying to me. I take code from SO and other places, but only after fully understanding it.
You could say it's a variant of the "nested message loop hack" from >the Brownfield Async article. AsyncHelpers.RunSync takes control of the current thread and turns it into a message pump, processing its own queue of work. It installs its own SynchronizationContext to capture async continuations (>which by default resume on the captured SynchronizationContext or TaskScheduler as I describe on my blog).
So, the corner cases you're going to run into all have to do with that SynchronizatonContext swap. An exhaustive list may not be possible, but here's some concerns off the top of my head:
Some components require a specific SynchronizationContext. One example is in the pre-Core ASP.NET days, some ASP.NET APIs will just hang if SynchronizationContext.Current is not an AspNetSynchronizationContext. I don't really know why they do that, but it is behavior I've observed when attempting this hack many years ago. As another example, some UI components will verify they're on the correct synchronization context (others verify they're on the correct thread, which still works fine if the SyncCtx is swapped).
Some components capture a SynchronizationContext for later, but the SynchronizationContext used here has a limited lifetime; once the tasks complete, the whole SyncCtx is torn down. So anything like Progress<T> or an Rx observable observing on that SyncCtx must not be used after the SyncCtx is torn down.
This solution installs a single-threaded context and then synchronously blocks on it. So, it solves a class of sync-over-async issues, but if anything it calls does a blocking-style sync-over-async, it will deadlock for sure.
The final one is one of my biggest concerns, but the hardest to explain. In my article I kind of hand-wave it as "unexpected reentrancy". This kind of approach may have surprising results if run on a UI thread (or more specifically, an STA thread). CBrumme had some great blog posts about how the .NET runtime would do some STA pumping when blocking; those posts were sadly taken down a few years ago when MS changed their blog URI schemes. Essentially, it means that some Windows messages may be processed by the UI thread even if it's "blocked" from a managed perspective; this may cause parts of your code to run that you're not expecting to (in this case, running as part of RunSync instead of the window's main message processing loop). Now, these posts were taken down, and the >modern .NET Core AutoResetEvent.WaitOne ends up at WaitForMultipleObjectsIgnoringSyncContext, which from the name sounds like it might not be partially pumping, so maybe that's just not true anymore. But for myself, I'd be very wary of doing something like this on an STA/GUI thread.
In summary, it's not an approach I recommend. I recommend using the generic-value-type-constrained-interface approach instead. But, if you have a strong understanding of all the code that will ever be run synchronously and are sure you won't run into the situations above, then it would be acceptable.
Suggested blogs:
>How to merge cells with HTML, CSS, and PHP?
>How to Migrate your apps to Android 13
>How to Read a csv file with php using cURL
>How to read frame from ffmpeg subprocess in Python?
>How to register a schedule with the controller?
>How to resolve the Composer dependency conflicts (Symfony)?
>How to route between different components in React.js?
>How to save python yaml and load a nested class?-Python
>How to send multiple HTML form fields to PHP arrays via Ajax?