Skip to content

Instantly share code, notes, and snippets.

@ghuntley
Last active May 9, 2024 10:12
Show Gist options
  • Save ghuntley/e5b5642ecc4428255e61185bb79856e4 to your computer and use it in GitHub Desktop.
Save ghuntley/e5b5642ecc4428255e61185bb79856e4 to your computer and use it in GitHub Desktop.

Async and Await: Here be dragons

Overview

  • Background
  • Basics
  • Concepts
  • Examples

In the Beginning …

  • Introduced in C# 5
  • Manual thread management (Thread, BackgroundWorker)

Then there was light!

  • Task-based Asynchronous Pattern (TAP)
  • Added Task and Task<T> classes (called awaitables, there are others...)
  • Added async and await keywords
  • Separates tasks from threads -- better runtime performance

The Basics

// no return
public async Task MethodWithNoReturnAsync()
{
   await GetUrlAsStringAsync("http://google.com");
}

// return string
public async Task<string> MethodWithReturnAsync()
{
   return await GetUrlAsStringAsync("http://google.com");
}

Async calls typically follow a few rules:

  • return an awaitable (ie, Task or Task<TResult>)
  • awaitables are await'ed
  • marked with the async keyword
  • end with Async

Hot tip

Naming async methods with the suffix Async is a common pattern and used to be the norm. These days it's less common and you probably don't want to do it. Whatever you choose, be consistent.


Why TAP?

cpu_io_bound

Tasks. Are. Awesome.

tl;dr

TAP decouples tasks from threads. Many tasks can execute on a single thread, without blocking.

tap

Await'ing Tasks

public async Task<string> FooAsync(string url)
{
  var task = MethodWithReturnAsync();
  
  // do something here ...

  // task may, or may not, have completed
  return await task;
}
public async Task<string> AnotherMethodAsync()
{
  var result = await GetUrlAsStringAsync("http://google.com");
  
  // return the generic type, not Task<string>
  // can seem odd at first
  return result.ToUpper();
}

Returning Task

public Task DoNothingReallyQuicklyAsync()
{
  // no await, no async
  // returning 1 is convention
  return Task.FromResult(1);
}

public Task<string> ToUpper(string value) // no async!
{
  return Task.FromResult(value.ToUpper());
}

public async Task Caller()
{
   await DoNothingReallyQuicklyAsync();
   var result = await ToUpper("the string");
}

Hot Tip

In the above situation, you can mark the method with async, however you should never do this. Marking the method with async causes the runtime to instantiate the async machinery for the method, which will have significant performance overhead. If you don’t need this, it is best to avoid it.

In general, if you can avoid calling await and use Task.FromResult you should.


Why wait?

tl;dr

You can return void from an async method. Doesn't mean you should.

private async void Handler(object o, Event e)
{
    var result = await FooAsync();
    DisplayFoo(damageResult);
}

fooButton.Clicked += Handler;

Hot Tip

Returning void from an async method has a number of problems. You have no ability to interact with the task after running it: waiting for completion, cancelling, progress etc.

Secondly, the asynchronous programming model uses the awaitable to return any exceptions that occurred. This is what enables calling methods to catch exceptions thrown by asynchronous code. Returning void means that the caller will never see the exception.

private async void ThrowExceptionAsync()
{
  await Task.Run(_ => throw new InvalidOperationException());
}

public void WillNeverCatchException()
{
  try
  {
    // Cannot await 'void'
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught
    throw;
  }
}


Hot Tip

Don't ignore compiler warnings about missing await calls. If you really, really mean it, use a pragma to hide the warning.


Multiple Tasks

public async Task<User> GetUser(int userId)
{
    return await db.GetUserAsync(userId);
}

public static Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();

    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUser(id));
    }

    return await Task.WhenAll(getUserTasks);
    // alternative ...
    return await Task.WhenAny(getUserTasks);
}

Hot Tip

Be aware of LINQ’s lazy evaluation when mixing with async code. Typically you will want to materialise the list (with ToList or ToArray) before waiting!


Some Context

tl;dr

Use configureAwait() to improve performance -- if you don't need to be on the UI / Request thread, use configureAwait(false).

context

From Stephen Cleary’s Blog:

private async Task DownloadFileAsync(string fileName)
{
  var fileContents = await DownloadFileContentsAsyn(fileName)
    .ConfigureAwait(false);

  // Because of the ConfigureAwait(false), we are no longer on the original context
  //  - unning on the thread pool

  await WriteToDiskAsync(fileName, fileContents)
    .ConfigureAwait(false);

  // Second call to ConfigureAwait(false) is not *required*
  //  - good practice (and less error prone!)
}

private async void DownloadFileButton_Click(object sender, EventArgs e)
{
  // Asynchronously wait
  //  - UI thread is not blocked
  await DownloadFileAsync(fileNameTextBox.Text);

  // Resume on the UI context
  //  - safe to directly access UI elements.
  resultTextBox.Text = "File downloaded!";
}

Hot Tip

Each level of async call has it’s own context. Once you have called configureAwait(false) you will be running on a ThreadPool context, but it’s good practice to still use configureAwait(false) for each subsequent await for clarity.


Gotcha If the Task is already complete, then the context isn’t captured, meaning you may still be on the UI thread, even after calling configureAwait(false).

async Task MyMethodAsync()
{
  // original context
  await Task.FromResult(1);

  // original context
  await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false);

  // original context
  var random = new Random();
  int delay = random.Next(2); // Delay is either 0 or 1
  await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false);
  // might or might not run in the original context
  //  - true when you await any Task that might complete very quickly
}

CPU Bound Operations

private DamageResult CalculateDamageDone()
{
    // Does an expensive calculation and returns
    // the result of that calculation.
}

calculateButton.Clicked += async (o, e) =>
{
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Guidelines

Old New Description
task.Wait await task Wait/await for a task to complete
task.Result await task Get the result of a completed task
Task.WaitAny await Task.WhenAny Wait/await for one of a collection of tasks to complete
Task.WaitAll await Task.WhenAll Wait/await for every one of a collection of tasks to complete
Thread.Sleep await Task.Delay Wait/await for a period of time

Resources

Microsoft Introduction to async / await (C#) Microsoft In-Depth doc on async c/ await

Six Essential Tips for Async | Channel 9

Stephen Cleary’s Blog - Async and Await

Best Practices in Async / Await

Task-Based Asynchronous Pattern

Dixon's Blog Series - Deep Dive into Async

@X39
Copy link

X39 commented Aug 15, 2021

Would like to see a "more in depth" for how to create custom awaiters, especially since this goes into how blocking tasks can impact performance because eg. One waits for some external event.

Resources: https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-2-awaitable-awaiter-pattern
https://devblogs.microsoft.com/pfxteam/await-anything/

Sadly I cannot seem to find a somewhat proper resource for the more fun discipline of implementing a custom awaitable class that would get returned by GetAwaiter.. But there is also that, allowing to await eg. User events easily

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment