0

I know, that the question of async/await is old as hills, but nevertheless, it would be great if someone help me with the following:

Preconditions: .NetFramework. Legacy.

  • API controller with synchronous action methods.
    • The action methods call some private methods, which create objects of ISomething and,
    • using a public void Run(ISomething input) method from another class (the instance of the class is created in the constructor of the controller),
    • send ISomething objects to another part of the application via an asynchronous library like _lib.Send(ISomething input).

Since public void Run(ISomething input) is synchronous and the methods of the library are asynchronous, there is an adapter under the hood of public void Run(ISomething input). The whole picture looks like this:

// _lib.Send(ISomething input) returns Task here and performs IO operation

public void Run(ISomething input)
{
   RunAsync(() => _lib.Send(ISomething input));
}

private void RunAsync(Func<Task> input)
{
   var result = Task.Run(() =>
            {
                input.Invoke();
            }).ConfigureAwait(false);            
   result.GetAwaiter().GetResult();
}

My questions are:

  1. should async/await be used within Task.Run() (please explain, if possible)?
  2. Is there a better way to wrap _lib.Send(ISomething input) under the above mentioned conditions? // the signature is: public Task Send(ISomething input);
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Alex
  • 1
  • 5
    The thread that calls the synchronous method still has to wait until the whole operation finishes. This makes it a little worse by using yet another thread. The point of async is that while a long running non-CPU operation completes the application uses not two threads or even one, but zero threads. https://stackoverflow.com/questions/21406973/wrapping-synchronous-code-into-asynchronous-call – Scott Hannen Apr 18 '21 at 23:25
  • `Task.GetAwaiter().GetResult()` is a very good way to create Deadlocks. If this is run on the UI thread for instance, and the `_lib.Send` method updates an `IProgress`, you will have broken your application. – Aron Apr 19 '21 at 09:38

1 Answers1

1

How to join synchronous and asynchronous?

The best answer is "you don't". In this case, since Run depends on the asynchronous _lib.Send, then Run should be changed to be asynchronous. Then the controller actions that call Run should be changed to be asynchronous. Then everything is asynchronous and you no longer have the sync-over-async antipattern.

However, sometimes that answer isn't realistic, because it costs developer time to update legacy code, and generally server time is cheaper than developer time. The ideal answer of "go async all the way" is great advice for new code, but may not be realistic for legacy code. So there are a few hacks you can use to support both synchronous and asynchronous code in legacy apps. The hack being used here is the "thread pool hack".

Is there a better way to wrap _lib.Send(ISomething input)

One of the problems with the hacks is that they don't work for all scenarios. There is no perfect all-purpose solution for calling asynchronous code from synchronous code, and furthermore there cannot be a perfect all-purpose solution. If you must keep the legacy sync-over-async antipattern, then you just have to accept the limitations of the hack you need to use.

In this case, the thread pool hack has the limitation that the code being wrapped (_lib.Send) must be callable from a bare thread pool thread. Since this is a legacy ASP.NET app, this means that code cannot depend on the ASP.NET context (including HttpContext.Current). Based on the name and description of _lib.Send, I expect it doesn't depend on the context and can be called from an arbitrary thread, so I expect the thread pool hack should work for that code.

Again, this is with the understanding that it would be better to have a Task RunAsync(ISomething input) => _lib.Send(input);, and that would avoid the Task.Run wrapper completely. You could even keep the old void Run implementation, and convert the rest of the code to use RunAsync gradually if you want. That is what I recommend.

should async/await be used within Task.Run() (please explain, if possible)?

It's not necessary in this case. The keywords should be used in any non-trivial code, but since the delegate is just calling a single method, it's not necessary IMO.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810