Design a good asynchronous method in C# :
- It should have as few or no arguments as possible. Avoid it if possible
ref
andout
Parameters. - If it makes sense, it should have a return type that really expresses the result of the method code, rather than the success identifier of C++.
- It should have a name that explains its behavior without relying on extra symbols or comments.
useAsync void
Is a big no-no:
Async void
An exception thrown by a method cannot be caught by an outside method
When an async Task or async Task
method throws an exception, the exception is caught and placed in the Task object. Async void has no Task, so any exception raised by Async void will be raised directly on the active SynchronizationContext when Async void methods are started.
SynchronizationContext
SynchronizationContext enables one thread to communicate with another thread. Suppose you have two threads, Thead1 and Thread2. Thread1 does something, and then it wants to execute some code inside Thread2. One way to do this is to: Request Thread to get the SynchronizationContext and give it to Thread1. Thread1 can then invoke the send method of SynchronizationContext to execute code in Thread2.
Not every thread has a SynchronizationContext object. One thing that always has a SynchronizationContext object is the UI thread. Controls in the UI thread are created by placing SynchronizationContext in the thread. SynchronizationContext. The Current object is not an AppDomain a instance, but each thread one instance. This means that the two threads that call SynchronizationContext will have their own SynchronizationContext instance. Context Context is stored in the thread Data Store (not in the global memory space of the appDomain).
A SynchronizationContext has static send and POST methods.
Reference: Understand SynchronizationContext
public async void AsyncVoidMethodThrowsException()
{
throw new Exception("Hmmm, something went wrong!");
}
public void ThisWillNotCatchTheException()
{
try
{
AsyncVoidMethodThrowsException();
}
catch(Exception ex)
{
//The below line will never be reachedDebug.WriteLine(ex.Message); }}public async Task AsyncTaskMethodThrowsException()
{
throw new Exception("Hmmm, something went wrong!");
}
public async Task ThisWillCatchTheException()
{
try
{
await AsyncTaskMethodThrowsException();
}
catch (Exception ex)
{
//The below line will actually be reachedDebug.WriteLine(ex.Message); }}Copy the code
Async void
Methods are hard to test
returnTask
Instead of returningawait
public async Task<string> AsyncTask()
{
//Not great!
/ /... Non-async stuff happens here
//The await is the very last line of the code path - There is no continuation after it
return await GetData();
}
public Task<string> JustTask()
{
//Better!
/ /... Non-async stuff happens here
//Return a Task instead
return GetData();
}
Copy the code
Because every time a method is declared async, the compiler creates a state machine class to encapsulate the method logic, which adds a certain amount of overhead. If the method does not need to be asynchronous, but instead returns a Task
, let it be handled elsewhere, and make the method’s return type Task
(rather than async T), thus avoiding the generation of a state machine and making the code cleaner and less time-consuming.
However, there is an exception to everything. If a Task
is returned, the return happens immediately, so if the code is in a try/catch block, the exception is not caught. Similarly, if the code is in a using block, it will immediately release the object.
Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
// will return immediately, then Foo will be disposed, so an exception will be thrown
returnfoo.DoAnotherThingAsync(); }}// It works properly
async Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
return awaitfoo.DoAnotherThingAsync(); }}Copy the code
Avoid the use of.Wait()
or.Result
, the use of.GetAwaiter().GetResult()
Instead of
use.Wait()
or .Result
There are life and death locks in GUI applications
By default, when an unfinished Task is await, the current context will retrieve and continue executing the rest of the code when the Task completes. This context is the current SynchronizationContext, unless it’s empty. The SynchronizationContext of a GUI application is exclusive, allowing only one thread to run.
public class DeadlockClass
{
private static async Task DelayAsync()
{
await Task.Delay(1000);
}
// When this method accesses the interface element, haha, the deadlock comes out
public static void Test()
{
/ / start delay
var delayTask = DelayAsync();
// Wait for the end of DelaydelayTask.Wait(); }}private void TextButton_OnClick(object sender, RoutedEventArgs e)
{
DeadlockClass.Test();
TextButton.Content = "Helius";
}
Copy the code
Call deadlockclass.test () on the UI thread. When the await completes, it attempts to execute the rest of the await in its original code context (the UI thread), but that code context already has a thread in it, which has been waiting synchronously for the Async to complete. The two of them wait for each other, and then they’re deadlocked.
It is also important to note that the console does not cause deadlocks, and that the console’s SynchronizationContext is similar to a thread pool mechanism rather than exclusive. So when the await ends, it can regain the original context and execute the rest of the code. This difference is a source of confusion for many people, when they test the program on the console and it’s OK, but when they run the GUI program, the deadlock occurs.
use.Wait()
or.Result
Will wrap the exception inAggregateException
This increases the complexity of error handling.
class Program
{
// The try/catch in MainAsync catches specific exception types, but if the try/catch is placed in Main, it will always catch aggregateExceptions.
static void Main(string[] args)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
MainAsync().Wait();
Console.ReadKey();
}
static async Task MainAsync()
{
try
{
await Task.Delay(1000);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
}
catch (Exception ex)
{
}
}
}
Copy the code
advanced.GetAwaiter().GetResult()
A normal exception is returned
public void GetAwaiterGetResultExample()
{
//This is ok, but if an error is thrown, it will be encapsulated in an AggregateException
string data = GetData().Result;
//This is better, if an error is thrown, it will be contained in a regular Exception
data = GetData().GetAwaiter().GetResult();
}
Copy the code
Asynchronous library methods should be consideredTask.ConfigureAwait(false)
To improve performance and avoid deadlocks
As asynchronous GUI applications use it, you may find that many widgets of async methods are using GUI threads as their context. This could create a lag. ConfigureAwait can improve performance.
public class ConfigureAwaitFalse
{
async Task MyMethodAsync()
{
// This is the context of the calling thread.
await Task.Delay(1000);
// This is also the context of the calling thread
await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
// The context here is not the calling thread's context, but a random context
//do Something}}Copy the code
The above code and comments are pretty obvious. It is important to note that you cannot set ConfigureAwait(false) if there is a context behind the await in a method. For example, in a GUI program, a thread exception will be thrown if there is an action on some interface element behind the await in the method.
private async void TextButton_OnClick(object sender, RoutedEventArgs e)
{
TextButton.IsEnabled = false;
try
{
await Task.Delay(2000).ConfigureAwait(false);
}
finally
{
TextButton.IsEnabled = true;// An exception will be thrown}}Copy the code
This can be modified as follows:
private async void TextButton_OnClick(object sender, RoutedEventArgs e)
{
TextButton.IsEnabled = false;
try
{
//await Task.Delay(2000).ConfigureAwait(false);
await HandleClickAsync();
}
finally
{
TextButton.IsEnabled = true; }}private async Task HandleClickAsync()
{
await Task.Delay(2000).ConfigureAwait(false);
}
Copy the code
When the Task completes, the synchronization Context calls the post() method to restore to its original location. However, when writing library code, you rarely need to go back to the previous context. When using task.configureawait(false), the code will no longer attempt to revert to its previous position. This slightly improves performance and helps avoid deadlocks.