Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

1. Asynchronous programming

Asynchronous programming is a key technique that directly handles blocking I/O and concurrent operations on multiple cores. With an easy-to-use language-level asynchronous programming model in C#, Visual Basic, and F#,.net makes applications and service offerings responsive and resilient.

The above is about asynchronous programming. We use asynchronous programming more or less in our daily programming process. Why try asynchronous programming? Because application processes use file and network I/O, such as handling file reads and writes to disk, the NETWORK request API generally blocks by default. As a result, our user interface is stuck and experience is poor, some servers have low hardware utilization, and service processing capacity request response is slow. Task-based asynchronous apis and language-level asynchronous programming models have changed this model to allow asynchronous execution by default with just a few new concepts.

The most commonly used asynchronous programming mode is TAP mode, which is the async and await keywords provided by C#. In fact, there are two other asynchronous modes: event-based asynchronous mode (EAP) and asynchronous programming model (APM).

APM is asynchronous programming based on IAsyncResult interface. For example, BeginRead of FileStream class and EndRead are APM implementations that provide a pair of start-end methods to start and receive asynchronous results. Asynchronous programming is implemented using the delegate BeginInvoke and EndInvoke methods. EAP is introduced in.NET Framework 2.0, which is mostly reflected in WinForm programming. Many control events in WinForm programming are based on the event model, and BeginInvoke and Invoke are often used when cross-thread updating interface is used. The event mode is a complement to APM, defining a set of events including complete, progress, and cancel events that allow us to register the response events to operate on during asynchronous calls.

class Program { static void Main(string[] args) { Console.WriteLine(DateTime.Now + " start"); IAsyncResult result = BeginAPM(); //EndAPM(result); Console.WriteLine(DateTime.Now + " end"); Console.ReadKey(); } delegate void DelegateAPM(); static DelegateAPM delegateAPM = new DelegateAPM(DelegateAPMFun); public static IAsyncResult BeginAPM() { return delegateAPM.BeginInvoke(null, null); } public static void EndAPM(IAsyncResult result) { delegateAPM.EndInvoke(result); } public static void DelegateAPMFun() { Console.WriteLine("DelegateAPMFun... start"); Thread.Sleep(5000); Console.WriteLine("DelegateAPMFun... end"); }}Copy the code

In the code above I use a delegate to make an asynchronous call, the BeginAPM method starts an asynchronous call with BeginInvoke, then the DelegateAPMFun asynchronous method stops for 5 seconds. If the main method is printed first and the asynchronous method is printed last, the operation is asynchronous.

One line of code EndAPM(result) is commented out and calls the delegate EndInvoke method, which blocks until the asynchronous call is complete, so we can put it in place to get the result of the execution. This is similar to the await keyword in TAP mode, leaving the line change code to execute.

The above two methods are not recommended to use, write to understand the relatively difficult, interested can understand, and this way in.net 5 does not support the delegation of asynchronous call, so if you want to run in the FRAMEWORK of the.NET framework. TAP, introduced in THE.NET Framework 4, is the recommended asynchronous design pattern and is the focus of this article, but TAP is not necessarily a thread. It is a task, understood as an asynchronous abstraction of work, rather than an abstraction on top of a thread.

2, async await

It is easy to implement asynchronous programming with the keyword async await. We need to add the keyword async to the method, and the asynchronous operation within the method shall use await to wait for the asynchronous operation to complete before executing the subsequent operation.

class Program { static void Main(string[] args) { Console.WriteLine(DateTime.Now + " start"); AsyncAwaitTest(); Console.WriteLine(DateTime.Now + " end"); Console.ReadKey(); } public static async void AsyncAwaitTest() { Console.WriteLine("test start"); await Task.Delay(5000); Console.WriteLine("test end"); }}Copy the code

The AsyncAwaitTest method uses the async keyword, waits 5 seconds with the await keyword and prints “test end”. Call AsyncAwaitTest inside the Main method.

Using await gives control to its caller until the task is complete, allowing applications and services to perform useful work. After the task is complete, the code can continue to execute without relying on callbacks or events. The language and task API integration will do this for you. Methods that use await must use the async keyword. If we want to wait for AsyncAwaitTest inside the Main method, we need to add async and return Task.

3. Async await principle

After compiling the Main method above without calling with await, decompilate the DLL using ILSpy and use C# 4.0 to see what the compiler does for us. Since 4.0 does not support async await, it will decompile into specific code. After 4.0 decompile, async await syntax will be displayed directly.

After decomcompiling, we can see that in the asynchronous method, a generic d__1 implementation interface IAsyncStateMachine is generated, and then we call the Start method, After some thread processing in Start, statemachine.movenext () is called, which calls the MoveNext method of the d__1 instantiated object.

public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { if (stateMachine == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);  } Thread currentThread = Thread.CurrentThread; Thread thread = currentThread; ExecutionContext executionContext = currentThread._executionContext; ExecutionContext executionContext2 = executionContext; SynchronizationContext synchronizationContext = currentThread._synchronizationContext; try { stateMachine.MoveNext(); } finally { SynchronizationContext synchronizationContext2 = synchronizationContext; Thread thread2 = thread; if (synchronizationContext2 ! = thread2._synchronizationContext) { thread2._synchronizationContext = synchronizationContext2; } ExecutionContext executionContext3 = executionContext2; ExecutionContext executionContext4 = thread2._executionContext; if (executionContext3 ! = executionContext4) { ExecutionContext.RestoreChangedContextToThread(thread2, executionContext3, executionContext4); }}}Copy the code

Class

d__1 generated by the compiler:

The MoveNext method contains the AsyncAwaitTest logic. Because our source code has only one await operation, if we have multiple await operations, we should have multiple segments in MoveNext. Put the MoveNext segments into different state segments. There is also an if judgment in the class, with the 1__state argument, initially called -1, and called in num! = 0 executes our business code inside if, which executes the business code sequentially until we reach await

awaiter = Task.Delay(5000).GetAwaiter(); if (! awaiter.IsCompleted) { num = (<> 1__state = 0); <> u__1 = awaiter; < AsyncAwaitTest > d__1 stateMachine = this; <> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; }Copy the code

In the process of the < > t__builder. AwaitUnsafeOnCompleted (ref awaiter, ref the stateMachine) will await transfer call AwaitUnsafeOnCompleted method and state machine, This method goes all the way down to find the thread pool operation.

// System.Threading.ThreadPool internal static void UnsafeQueueUserWorkItemInternal(object callBack, bool preferLocal) { s_workQueue.Enqueue(callBack, ! preferLocal); }Copy the code

The program calls the wrapped task in a thread pool, at which point the asynchronous method is switched to another thread, or executed on the original thread. (If the asynchronous method execution time is short, the thread switch may not occur, depending on the scheduler.) After execution of await, state 1__state has changed to 0, the program will call MoveNext again to enter else, and no return or other logic will continue to execute to the end. This is a state controlled execution logic. It is a design mode of “state machine mode”. The AsyncAwaitTest logic for the Main method enters if, and the thread is scheduled to execute if the AsyncAwaitTest logic is await. When the state machine execution switches to another state, MoveNext is executed again until the asynchronous method is complete.

4. Async and threads

With the above basis, we know that async and await are usually used in pairs. When our method is marked as asynchronous, time-consuming operations in it need await to mark and execute subsequent logic after completion. The caller calling the asynchronous method can decide whether to wait or not. Without await the caller executes asynchronously or executes the asynchronous method on the original thread.

If a method modified by the async keyword does not contain await expressions or statements, the method will execute synchronously, optionally requesting the Task to Run on a separate thread explicitly through the task.run API. We can change the AsyncAwaitTest method to show the thread running:

public static async Task AsyncAwaitTest()
{
    Console.WriteLine("test start");
    await Task.Run(() =>
    {
        Thread.Sleep(5000);
    });
    Console.WriteLine("test end");
}

Copy the code

5. Cancel the task CancellationToken

If you don’t want to wait for the asynchronous method to complete, you can cancel the task by CancellationToken, A CancellationToken is a struct, and a CancellationTokenSource is usually used to create a CancellationToken, Because CancellationTokenSource has a series of [methods] that we can use to cancel the task without operating on the CancellationToken structure.

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
Copy the code

Cts. CancelAfter(3000) CancelAfter(3000) 3 seconds We listen for CancellationToken and return IsCancellationRequested==true.

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken ct = cts.Token;
    cts.CancelAfter(3000);

    Console.WriteLine(DateTime.Now + " start");
    AsyncAwaitTest(ct);
    Console.WriteLine(DateTime.Now + " end");
    Console.ReadKey();
}

public static async Task AsyncAwaitTest(CancellationToken ct)
{
    Console.WriteLine("test start");
    await Task.Delay(5000);
    Console.WriteLine(DateTime.Now + " cancel");
    if (ct.IsCancellationRequested) {
        return;
    }
    //ct.ThrowIfCancellationRequested();
    Console.WriteLine("test end");
}

Copy the code

Delay(5000) is still waiting 5 seconds to finish the Task even though we have finished the Task after 3 seconds. Another way is we don’t determine whether cancel, direct call ct. ThrowIfCancellationRequested judging () to us, if this method, but still can’t end in a timely manner. There is another way to handle this, which is to pass the CancellationToken to the await asynchronous API method. This may or may not end immediately, depending on the asynchronous implementation.

public static async Task AsyncAwaitTest(CancellationToken ct) { Console.WriteLine("test start"); // pass CancellationToken to cancel await task. Delay(5000,ct); Console.WriteLine(DateTime.Now + " cancel"); / / manual processing to cancel / / if (ct) IsCancellationRequested) {/ / return; / /} handle void / / / / call methods ct. ThrowIfCancellationRequested (); Console.WriteLine("test end"); }Copy the code

6. Attention items

Do not use thread. Sleep in asynchronous methods. There are two possibilities: (1) Sleep blocks the caller Thread waiting for Sleep before await await. Sleep comes after await, but await execution on caller’s thread blocks caller’s thread. So we should use task.delay to wait for the operation. Thread.Sleep is used in task. Run, because task. Run shows that the request is running on a separate Thread, so I know that writing does not block the caller.