Using async and await in C # - best practices

Original author: Jon Wagner
  • Transfer
  • Tutorial

The async and await keywords introduced in C # 5.0 greatly simplify asynchronous programming. They also hide some complexities that, if you lose vigilance, can add problems to your code. The practices described below are useful if you are creating asynchronous code for .NET applications.


Use async / await only for places that can last “long.”

Everything is simple here. Creating Task other structures for managing asynchronous operations adds some overhead. If your operation is really long, for example, performing an IO request, then these costs will not be noticeable in the main. And if your operation is short or takes several processor cycles, then it may be better to perform this operation synchronously.

In general, the team working on the .NET Framework did a good job of selecting features that should be asynchronous. So, if the framework method ends with Async and returns a task, then most likely you should use it asynchronously.

Prefer async / await instead of Task

Writing asynchronous code using async/await, greatly simplifies the process of creating code and reading it, rather than using tasks Task.

public Task GetDataAsync()
{
    return MyWebService.FetchDataAsync()
        .ContinueWith(t => new Data (t.Result));
}


public async Task GetDataAsync()
{
    var result = await MyWebService.FetchDataAsync();
    return new Data (result);
}

In terms of performance, both of the methods presented above have small overheads, but they scale somewhat differently as the number of tasks in them increases:
  • Task builds a chain of continuations, which increases in accordance with the number of tasks connected in series, and the state of the system is controlled through closures found by the compiler.
  • Async/awaitbuilds a state machine that does not use additional resources when adding new steps. However, the compiler can define more variables for storing state in the machine's stacks, depending on your code (and the compiler). The article on MSDN has excellent details on what is happening.

In most scenarios, it async/awaitwill use less resources and run faster than tasks Task.

Use an already completed empty static task for conditional code.

Sometimes you want to run a task only under some condition. Unfortunately, it await will cause NullReferenceExceptionit if it receives null instead of a task, and processing this will make your code less readable.

public async Task GetDataAsync(bool getLatestData)
{
    Task task = null;
    if (getLatestData)
        task = MyWebService.FetchDataAsync();
    // здесь выполним другую работу
    // и не забудем проверить на null
    WebData result = null;
    if (task != null)
        result = await task;
    return new Data (result);
}

One way to simplify the code a bit is to use an empty task that has already been completed. The resulting code will be cleaner:

public async Task GetDataAsync(bool getLatestData)
{
    var task = getLatestData ? MyWebService.FetchDataAsync() : Empty.Task;
    // здесь выполним другую работу
    // task всегда не null
    return new Data (await task);
}

Ensure that the task is static and created as completed. For instance:

public static class Empty
{
    public static Task Task { get { return _task; } }    
    private static readonly Task _task = System.Threading.Tasks.Task.FromResult(default(T));
}


Performance: prefer to cache the tasks themselves than their data.

There are some overheads when creating tasks. If you cache your results, but then convert them back to tasks, you might create additional task objects.

public Task GetContentsOfUrl(string url)
{
    byte[] bytes;
    if (_cache.TryGetValue(url, out bytes))
        // дополнительная задача создаётся здесь
        return Task.Factory.StartNew(() => bytes);
    bytes = MyWebService.GetContentsAsync(url)
        .ContinueWith(t => { _cache.Add(url, t.Result); return t.Result; );
}
// это не потокобезоспасно (не копируйте себе этот код как есть)
private static Dictionary _cache = new Dictionary();

Instead, it would be better to cache the tasks themselves. In this case, the code using them can wait for the task already completed. There are optimizations in the Task Parallel Library so that the code waiting to complete an already completed task runs faster .

public Task GetContentsOfUrl(string url)
{
    Task bytes;
    if (!_cache.TryGetValue(url, out bytes))
    {
        bytes = MyWebService.GetContentsAsync(url);
        _cache.Add(url, bytes);
    }
    return bytes;
}
//  это не потокобезоспасно (не копируйте себе этот код как есть)
private static Dictionary> _cache = new Dictionary>();


Performance: Understand how await saves state.

When you use it async/await, the compiler creates a state machine that stores variables and the stack. For instance:

public static async Task FooAsync()
{
  var data = await MyWebService.GetDataAsync();
  var otherData = await MyWebService.GetOtherDataAsync();
  Console.WriteLine("{0} = "1", data, otherdata);
}

This will create a state object with several variables. See how the compiler saves the method variables:

[StructLayout(LayoutKind.Sequential), CompilerGenerated]
private struct d__0 : <>t__IStateMachine {
  private int <>1__state;
  public AsyncTaskMethodBuilder <>t__builder;
  public Action <>t__MoveNextDelegate;
  public Data 5__1;
  public OtherData 5__2;
  private object <>t__stack;
  private object <>t__awaiter;
  public void MoveNext();
  [DebuggerHidden]
  public void <>t__SetMoveNextDelegate(Action param0);
}

Note 1. If you declare a variable, it will be saved in the state-holding object. This can cause objects to remain in memory longer than you might expect.

Remark 2. But if you do not declare the variable, but use the value of the Async call along with await, the variable will go to the internal stack:

public static async Task FooAsync()
{
  var data = MyWebService.GetDataAsync();
  var otherData = MyWebService.GetOtherDataAsync();
  // промежуточные результаты попадут во внутренний стек и 
  // добавятся дополнительные переключения контекстов между await-ами
  Console.WriteLine("{0} = "1", await data, await otherdata);
}

You should not worry too much about this until you see performance issues. If you still decide to go deeper into optimization, there is a good article on MSDN about this: Async Performance: Understanding the Costs of Async and Await .

Stability: async / await is not Task.Wait

The state machine generated async/awaitis not the same as Task.ContinueWith/Wait. In general, you can replace the implementation with Task with await, but some performance and stability issues may occur. Let's see in more detail.

Stability: Know Your Sync Context

.NET code always executes in some context. This context defines the current user and other values ​​required by the framework. In some execution contexts, the code works in the context of synchronization, which controls the execution of tasks and other asynchronous work.

By default, after the await code will continue to work in the context in which it was run. This is convenient, because basically you want the security context to be restored, and you want your code after await to have access to the Windows UI objects if it already had access to them at startup. Note that Task.Factory.StartNew- does not restore the context.

Some synchronization contexts do not support re-entry into them and are single-threaded. This means that only one unit of work can be performed in this context at a time. An example of this would be a Windows UI thread or an ASP.NET context.

In such single-threaded synchronization contexts, it's pretty easy to get deadlock. If you create a task in a single-threaded context, and then wait in the same context, your code that is waiting will block the execution of the background task.

public ActionResult ActionAsync()
{
    // DEADLOCK: это блокирует асинхронную задачу
    // которая ждёт, когда она сможет выполняться в этом контексте
    var data = GetDataAsync().Result;
    return View(data);
}
private async Task GetDataAsync()
{
    // простой вызов асинхронного метода
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}


Stability: do not use Waitto wait for the task to finish right here.

As a basic rule, if you are creating asynchronous code, be careful about using it Wait. (c await is a little better.)

Do not use Wait for tasks in single-threaded synchronization contexts, such as:
  • UI threads
  • ASP.NET Context

The good news is that the framework allows you to return Task in certain cases, and the framework itself will wait for the task to complete. Trust him this process:

public async Task ActionAsync()
{
    // этот метод использует async/await и возвращает Task
    var data = await GetDataAsync();
    return View(data);
}

If you create asynchronous libraries, your users will need to write asynchronous code. This used to be a problem since writing asynchronous code was tedious and vulnerable to errors, but with the advent of async/awaitmost of the complexity is now handled by the compiler. And your code gets more reliability, and now you are less likely to be forced to deal with nuances ThreadPool.

Stability: consider using ConfigureAwaitif you are creating a library.

If you must wait for a task to complete in one of these contexts, you can useConfigureAwaitto tell the system that it should not perform a background task in your context. The disadvantage of this is that the background task will not have access to the same synchronization context, so you lose access to the Windows UI or HttpContext (although your security context will still be with you).

If you create a “library” function that returns Task, you most likely don’t know how it will be called. So it may be safer to add ConfigureAwait(false)to your task before returning it.

private async Task GetDataAsync()
{
    // ConfigureAwait(false) говорит системе, чтобы она
    // позволила оставшемуся коду выполняться в любом контексте
    var result = await MyWebService.GetDataAsync().ConfigureAwait(false);
    return result.ToString();
}


Stability: Understand how exceptions behave.

When you look at asynchronous code, sometimes it's hard to say what happens to exceptions. Will it be passed to the calling function, or to the code that is waiting for the task to complete?

The rules in this case are pretty straightforward, but it’s still sometimes difficult to answer the question simply by looking at the code.

Some examples:
  • Exceptions thrown from the async / await method itself will be sent to the code waiting for the task to complete (awaiter).
    public async Task GetContentsOfUrl(string url)
    {
        // это исключение будет вызвано на коде, ожидающем 
        // выполнения этой задачи
        if (url == null) throw new ArgumentNullException();
        var data = await MyWebService.GetContentsOfUrl();
        return data.DoStuffToIt();
    }
    

  • Exceptions Taskthrown from the task delegate will also be sent to code waiting for the task to complete (awaiter).
    public Task GetContentsOfUrl(string url)
    {
        return Task.Factory.StartNew(() =>
        {
            // это исключение будет вызвано на коде, ожидающем 
            // выполнения этой задачи
            if (url == null) throw new ArgumentNullException();
            var data = await MyWebService.GetContentsOfUrl();
            return data.DoStuffToIt();
        }
    }
    

  • Exceptions thrown during the creation of the Task will be sent to the code that called this method (caller) (which, in general, is obvious):
    public Task GetContentsOfUrl(string url)
    {
        // это исключение будет вызвано на вызывающем коде
        if (url == null) throw new ArgumentNullException();
        return Task.Factory.StartNew(() =>
        {
            var data = await MyWebService.GetContentsOfUrl();
            return data.DoStuffToIt();
        }
    }
    


The last example is one of the reasons why I prefer async/awaitinstead of creating task chains through Task.

Additional links (in English)

Also popular now: