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

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.


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.