📜 ⬆️ ⬇️

Using async and await in C # best practices


The async and await keywords entered in C # 5.0 greatly simplify asynchronous programming. They also hide behind them some difficulties that, if you lose your guard, can add problems to your code. The practices described below will be useful to you if you create asynchronous code for .NET applications.


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

Everything is simple here. Creating a Task and other structures to manage asynchronous operations adds some overhead. If your operation is really long, for example, performing an IO request, then these expenses will not be noticeable in general. 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 that worked on the .NET Framework did a good job of choosing functionality, which should be asynchronous. So, if the framework method ends on 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 makes the process of creating code and reading it much easier than using Task tasks.

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


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

In terms of performance, both of the methods presented above have a small overhead, but they scale somewhat differently as the number of tasks in them increases:

In most scenarios, async/await will use less resources and run faster than task tasks.

Use the already completed empty static task for the conditional code.

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

 public async Task<Data> GetDataAsync(bool getLatestData) { Task<WebData> 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<Data> GetDataAsync(bool getLatestData) { var task = getLatestData ? MyWebService.FetchDataAsync() : Empty<WebData>.Task; //     // task   null return new Data (await task); } 

Make sure the task is static and created as completed. For example:

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


Performance: prefer to cache the tasks themselves, rather than their data

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

 public Task<byte[]> GetContentsOfUrl(string url) { byte[] bytes; if (_cache.TryGetValue(url, out bytes)) //     return Task<byte[]>.Factory.StartNew(() => bytes); bytes = MyWebService.GetContentsAsync(url) .ContinueWith(t => { _cache.Add(url, t.Result); return t.Result; ); } //    (      ) private static Dictionary<string, byte[]> _cache = new Dictionary<string, byte[]>(); 

Instead, it will be better to copy the tasks themselves to the cache. In this case, the code using them can wait for the task that has already been completed. In Task Parallel Library there are optimizations to ensure that the code waiting for the execution of an already completed task runs faster .

 public Task<byte[]> GetContentsOfUrl(string url) { Task<byte[]> bytes; if (!_cache.TryGetValue(url, out bytes)) { bytes = MyWebService.GetContentsAsync(url); _cache.Add(url, bytes); } return bytes; } //    (      ) private static Dictionary<string, Task<byte[]>> _cache = new Dictionary<string, Task<byte[]>>(); 


Productivity: understand how await saves state

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

 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 method variables:

 [StructLayout(LayoutKind.Sequential), CompilerGenerated] private struct <FooAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public Data <data>5__1; public OtherData <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 stored in an object that stores the state. This can cause objects to remain in memory longer than you might expect.

Note 2. But if you do not declare a variable and use the Async value of the call with await , the variable will fall into 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 shouldn't worry too much about this until you see performance problems. If you still decide to delve into the optimization, on MSDN there is a good article about this: Async Performance: Understanding the Costs of Async and Await .

Stability: async / await is not Task.Wait

The state machine generated by async/await is not the same as Task.ContinueWith/Wait . In general, you can replace the implementation from Task with await , but there may be some performance and stability issues. Let's take a closer look.

Stability: Know your sync context

.NET code is always executed in some context. This context defines the current user and other values ​​required by the framework. In some execution contexts, the code runs in a synchronization context that controls the execution of tasks and other asynchronous work.

By default, after await code will continue to work in the context in which it was launched. 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 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 might be a Windows UI thread or an ASP.NET context.

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

 public ActionResult ActionAsync() { // DEADLOCK:     //  ,        var data = GetDataAsync().Result; return View(data); } private async Task<string> GetDataAsync() { //     var result = await MyWebService.GetDataAsync(); return result.ToString(); } 


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

As a general rule, if you create asynchronous code, be careful with Wait . (c await is somewhat better.)

Do not use Wait for tasks in single-threaded synchronization contexts, such as:

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

 public async Task<ActionResult> ActionAsync() { //    async/await   Task var data = await GetDataAsync(); return View(data); } 

If you are creating asynchronous libraries, your users will have to write asynchronous code. Previously, this was a problem, since writing asynchronous code was tedious and vulnerable to errors, but with the advent of async/await much 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 contend with the nuances of ThreadPool .

Stability: Consider using ConfigureAwait if you are creating a library.

If you must wait for a task to be executed in one of these contexts, you can use ConfigureAwait to 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 will lose access to the Windows UI or HttpContext (although you will still have your security context).

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

 private async Task<string> GetDataAsync() { // ConfigureAwait(false)  ,   //        var result = await MyWebService.GetDataAsync().ConfigureAwait(false); return result.ToString(); } 


Stability: understand how exceptions behave

When you look at an asynchronous code, it is sometimes 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 be completed?

The rules in this case are quite straightforward, but still it is sometimes difficult to answer a question just by looking at the code.

Some examples are:

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

Additional links (in English)

Source: https://habr.com/ru/post/162353/


All Articles