Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid violating the DRY principle when you have to have both async and sync versions of code?

I'm working on a project that needs to support both async and sync version of a same logic/method. So for example I need to have:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

The async and sync logic for these methods are identical in every respect except that one is async and another one is not. Is there a legitimate way to avoid violating the DRY principle in this kind of a scenario? I saw that people say you could use GetAwaiter().GetResult() on an async method and invoke it from your sync method? Is that thread safe in all scenarios? Is there another, better way to do this or am I forced to duplicate the logic?

like image 475
Marko Avatar asked Jan 07 '20 22:01

Marko


People also ask

How does asynchronous code affect performance?

Asynchronous code does introduce a small amount of overhead at run time, but for low traffic situations the performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.

What is sync over async?

Async is multi-thread, which means operations or programs can run in parallel. Sync is single-thread, so only one operation or program will run at a time. Async is non-blocking, which means it will send multiple requests to a server.

Can we use async method in synchronous?

async Main is now part of C# 7.2 and can be enabled in the projects advanced build settings. If you have a simple asynchronous method that doesn't need to synchronize back to its context, then you can use Task. WaitAndUnwrapException: var task = MyAsyncMethod();

Should every method be async?

If a method has no async operations inside it there's no benefit in making it async . You should only have async methods where you have an async operation (I/O, DB, etc.). If your application has a lot of these I/O methods and they spread throughout your code base, that's not a bad thing.


2 Answers

You asked several questions in your question. I will break them down slightly differently than you did. But first let me directly answer the question.

We all want a camera that is lightweight, high quality, and cheap, but like the saying goes, you can only get at most two out of those three. You are in the same situation here. You want a solution that is efficient, safe, and shares code between the synchronous and asynchronous paths. You're only going to get two of those.

Let me break down why that is. We'll start with this question:


I saw that people say you could use GetAwaiter().GetResult() on an async method and invoke it from your sync method? Is that thread safe in all scenarios?

The point of this question is "can I share the synchronous and asynchronous paths by making the synchronous path simply do a synchronous wait on the asynchronous version?"

Let me be super clear on this point because it is important:

YOU SHOULD IMMEDIATELY STOP TAKING ANY ADVICE FROM THOSE PEOPLE.

That is extremely bad advice. It is very dangerous to synchronously fetch a result from an asynchronous task unless you have evidence that the task has completed normally or abnormally.

The reason this is extremely bad advice is, well, consider this scenario. You want to mow the lawn, but your lawn mower blade is broken. You decide to follow this workflow:

  • Order a new blade from a web site. This is a high-latency, asynchronous operation.
  • Synchronously wait -- that is, sleep until you have the blade in hand.
  • Periodically check the mailbox to see if the blade has arrived.
  • Remove the blade from the box. Now you have it in hand.
  • Install the blade in the mower.
  • Mow the lawn.

What happens? You sleep forever because the operation of checking the mail is now gated on something that happens after the mail arrives.

It is extremely easy to get into this situation when you synchronously wait on an arbitrary task. That task might have work scheduled in the future of the thread that is now waiting, and now that future will never arrive because you are waiting for it.

If you do an asynchronous wait then everything is fine! You periodically check the mail, and while you are waiting, you make a sandwich or do your taxes or whatever; you keep getting work done while you are waiting.

Never synchronously wait. If the task is done, it is unnecessary. If the task is not done but scheduled to run off the current thread, it is inefficient because the current thread could be servicing other work instead of waiting. If the task is not done and schedule run on the current thread, it is hanging to synchronously wait. There is no good reason to synchronously wait, again, unless you already know that the task is complete.

For further reading on this topic, see

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Stephen explains the real-world scenario much better than I can.


Now let's consider the "other direction". Can we share code by making the asynchronous version simply do the synchronous version on a worker thread?

That is possibly and indeed probably a bad idea, for the following reasons.

  • It is inefficient if the synchronous operation is high-latency IO work. This essentially hires a worker and makes that worker sleep until a task is done. Threads are insanely expensive. They consume a million bytes of address space minimum by default, they take time, they take operating system resources; you do not want to burn a thread doing useless work.

  • The synchronous operation might not be written to be thread safe.

  • This is a more reasonable technique if the high-latency work is processor bound, but if it is then you probably do not want to simply hand it off to a worker thread. You likely want to use the task parallel library to parallelize it to as many CPUs as possible, you likely want cancellation logic, and you can't simply make the synchronous version do all that, because then it would be the asynchronous version already.

Further reading; again, Stephen explains it very clearly:

Why not to use Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

More "do and don't" scenarios for Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


What does that then leave us with? Both techniques for sharing code lead to either deadlocks or large inefficiencies. The conclusion that we reach is that you must make a choice. Do you want a program that is efficient and correct and delights the caller, or do you want to save a few keystrokes entailed by duplicating a small amount of code between the synchronous and asynchronous paths? You don't get both, I'm afraid.

like image 169
Eric Lippert Avatar answered Oct 23 '22 08:10

Eric Lippert


It's difficult to give a one-size-fits-all answer to this one. Unfortunately there's no simple, perfect way to get reuse between asynchronous and synchronous code. But here are a few principles to consider:

  1. Asynchronous and Synchronous code is often fundamentally different. Asynchronous code should usually include a cancellation token, for example. And often it ends up calling different methods (as your example calls Query() in one and QueryAsync() in the other), or setting up connections with different settings. So even when it's structurally similar, there are often enough differences in behavior to merit them being treated as separate code with different requirements. Note the differences between Async and Sync implementations of methods in the File class, for example: no effort is made to make them use the same code
  2. If you're providing an Asynchronous method signature for the sake of implementing an interface, but you happen to have a synchronous implementation (i.e. there's nothing inherently async about what your method does), you can simply return Task.FromResult(...).
  3. Any synchronous pieces of logic which are the same between the two methods can be extracted to a separate helper method and leveraged in both methods.

Good luck.

like image 29
StriplingWarrior Avatar answered Oct 23 '22 09:10

StriplingWarrior