CodeGym /Courses /C# SELF /Interaction of asynchronous and synchronous code

Interaction of asynchronous and synchronous code

C# SELF
Level 62 , Lesson 4
Available

1. Introduction

Asynchrony in C# is a powerful tool. But sometimes you need to deal with situations where asynchronous code must be called from synchronous code (or vice versa). It seems like it should "just work", but in practice you can get weird hangs (deadlock), performance loss and even unexpectedly… broken user interfaces. Often the bug only shows up with real data: in production or at a user's machine. The root cause is often wrong interaction between sync and async code and non-obvious details of the .NET task scheduler.

Modern libraries and frameworks also push heavy use of async methods, and you need to know how to smartly "glue" async code into existing synchronous call chains or, conversely, how to call synchronous code from an async method correctly.

Quick recap: what happens on await

When you write:

await SomeAsyncMethod();

The code "breaks" into two parts: before the await and after. The first part runs until the first awaited async operation (for example, a network request), and the continuation runs after it completes. Question: where will the continuation run? On the same thread? On a different one? What if we're writing a desktop app (like WPF or WinForms), or a console app? The answer — it depends. That's where, for example, ConfigureAwait comes into play.

What is SynchronizationContext?

SynchronizationContext is a special .NET mechanism that lets code "remember" where and how the continuation of an async operation should be invoked.

  • In classic WinForms/WPF apps SynchronizationContext ensures that after await the rest of the method continues on the same UI thread, so you don't get "access to the control from the wrong thread" errors.
  • In ASP.NET (the old one, not Core) SynchronizationContext lets you restore HttpContext and continue working with the HTTP request.
  • In console and ASP.NET Core apps SynchronizationContext is usually absent (equals null), and continuations run on the thread pool.

What is TaskScheduler?

TaskScheduler is a lower-level mechanism. In most cases you work with TaskScheduler.Default, which uses the .NET thread pool. It is responsible for deciding which tasks run where and when.

2. Deadlock when mixing await and Result/Wait()

One of the most famous traps in C#:

// Somewhere in UI code
var result = SomeAsyncMethod().Result;

or

SomeAsyncMethod().Wait();

Everything: the app is "frozen". Why?

How does it happen?

  1. You call an async method and immediately ask for .Result or .Wait() — i.e. you block the current thread and wait for the async task to finish.
  2. SomeAsyncMethod inside does an await, and schedules the continuation to the same thread via SynchronizationContext, but that thread is already blocked waiting for Result/Wait().
  3. Since the thread is waiting for Result/Wait(), it can't run the continuation.
  4. That's it, deadlock: the thread is waiting for itself.

This is especially easy to reproduce in UI apps, where everything runs on the UI thread and continuations expect that thread. In console apps and ASP.NET Core (without a synchronization context) these deadlocks are rare.

Real-life joke: If you've managed to get a deadlock with .Result — congrats, you're a few steps closer to Senior :D

3. How to properly embed async code into sync?

Recommendation #1: async all the way up

If you have an async method, propagate async/await up the call stack to the UI or entry point. Don't "hold" async halfway up.

Bad (blocks the thread):

// A synchronous method calls an async one via .Result
public void DoStuff()
{
    var data = GetDataAsync().Result;
    // ...
}

Good (async all the way):

public async Task DoStuffAsync()
{
    var data = await GetDataAsync();
    // ...
}

If possible — always prefer await over .Result / .Wait().

4. But sometimes you must call async from sync

Rewrite the whole top tree to async (best option if you can).

Use special patterns: for example, run the task on a separate thread via Task.Run, and call the async method inside it.

public void DoStuff()
{
    var result = Task.Run(() => SomeAsyncMethod()).Result;
}

But: there are caveats with synchronization in UI apps, so avoid this unless you really need to.

5. What does ConfigureAwait(false) do?

Sometimes you don't need the code after await to continue on the same thread (for example, in server-side code or in a library where SynchronizationContext doesn't matter). In fact, it's better if .NET doesn't tie you to one thread — it's faster!

Syntax and how it works

await SomeAsyncMethod().ConfigureAwait(false);
  • ConfigureAwait(false) says: "I don't need the original SynchronizationContext, continue execution anywhere, even on another thread."
  • ConfigureAwait(true) (the default) — "continue where the await was called, preferably on the same SynchronizationContext."

Visual diagram


        ┌─────────────────────────────────────────────────────┐
        │                     SynchronizationContext          │
        └─────────────────────────────────────────────────────┘
                      ↑                             ↑
       (UI-thread)   await SomeAsyncMethod()        Continuation (after await)
                  ──────────────────────────────>  (same thread — if ConfigureAwait(true))
                      ↓
                    (any thread — if ConfigureAwait(false))

Example of using ConfigureAwait(false) — "library" code

Imagine you're writing a library that can be used by anyone: WinForms, WPF, ASP.NET, console apps…

You must not depend on their threading model. So always use ConfigureAwait(false) in async methods of your library:

public async Task<string> LoadDataFromUrlAsync(string url)
{
    using var client = new HttpClient();
    string content = await client.GetStringAsync(url).ConfigureAwait(false);
    return content;
}

Now your method won't "require" execution on any specific SynchronizationContext. That's safer and more performant (fewer thread switches).

Example: what happens with await without ConfigureAwait

Consider a WPF app:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    Button1.Content = "Loading...";
    await Task.Delay(2000);  // Simulate long operation
    Button1.Content = "Done!";
}

Task.Delay internally does an "await". By default the continuation after await goes back to the UI thread so you can continue updating UI controls.

If inside your long operation you use ConfigureAwait(false):

await Task.Delay(2000).ConfigureAwait(false);
Button1.Content = "Done!";  // Error!

You'll get an exception: InvalidOperationException: "The calling thread cannot access this object because a different thread owns it."
Because the continuation now runs on a different thread, and you can't touch the UI from there.

Conclusion: use ConfigureAwait(false) only where you don't need access to the UI/context.

6. Useful nuances

Where and when to use ConfigureAwait

Scenario Use .ConfigureAwait(false)? Why?
Library code Yes Called from any context, UI not needed
Inside ASP.NET Core code Yes (there's almost no context) Improves performance
In WinForms/WPF, when accessing UI No You need to return control to the UI thread
Synchronous methods Doesn't make sense There's no synchronization context
In console apps You can, but no visible effect No context

How to check if a SynchronizationContext exists?

You can check directly from code:

Console.WriteLine(SynchronizationContext.Current == null
    ? "No context"
    : "Context exists");
  • In console and ASP.NET Core apps it will print "No context".
  • In WinForms/WPF — "Context exists".

Visualizing thread transitions

sequenceDiagram
    participant MainThread as Main thread (UI/Console)
    participant ThreadPool as Thread from pool

    MainThread->>SomeAsyncMethod: Call method
    SomeAsyncMethod->>MainThread: await without ConfigureAwait
    Note right of MainThread: After await we return to same thread
    SomeAsyncMethod->>ThreadPool: await with ConfigureAwait(false)
    Note right of ThreadPool: After await work can continue on any thread

Short rules of thumb

  • Don't use .Result and .Wait() in UI code with async methods.
  • In library code always use ConfigureAwait(false) for all awaits.
  • In UI apps use ConfigureAwait(false) only inside methods that don't touch UI elements.
  • Avoid mixing sync and async code unless absolutely necessary.
  • If you need to call async from sync — think twice: can't you make the whole chain async?

7. Common beginner mistakes when working with asynchrony

Mistake #1: using .Result or .Wait() everywhere.
Very often devs, especially after reading a couple of articles about async, start adding .Result or .Wait() everywhere to "synchronize" async calls. It seems convenient at first, but in practice it's a guaranteed path to deadlock in UI apps. It's especially dangerous if async methods inside don't use ConfigureAwait(false) — the UI thread gets blocked and can't run the continuation.

Mistake #2: wrong or excessive use of ConfigureAwait(false).
Some newbies apply ConfigureAwait(false) everywhere, even in code that works with UI elements. In UI apps this leads to control access errors (InvalidOperationException) because the continuation now runs on a thread different from the UI thread.

Mistake #3: forgetting that ConfigureAwait only works with awaitable objects.
Many think ConfigureAwait can be used for any method, but it actually only works with objects that return Task, Task<T>, ValueTask and other awaitable types. For synchronous methods ConfigureAwait has no effect, and waiting for such methods doesn't become asynchronous.

Mistake #4: mixing sync and async code unnecessarily.
Attempts to call async methods from sync code without a clear strategy (for example, via .Result, .Wait() or Task.Run) often lead to subtle bugs, performance loss and hard-to-diagnose deadlocks. Even if a method seems short and safe, such calls in a chain can break the whole app.

Mistake #5: underestimating the impact of SynchronizationContext.
Beginners often forget that a continuation after await can execute on the same thread (UI) if you don't use ConfigureAwait(false). This leads to unpredictable behavior, especially when mixing UI and library code where awaits and continuations get interleaved.

2
Task
C# SELF, level 62, lesson 4
Locked
Calling an asynchronous method from a synchronous one
Calling an asynchronous method from a synchronous one
1
Survey/quiz
Asynchronous Data Streams, level 62, lesson 4
Unavailable
Asynchronous Data Streams
A deep dive into async
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION