Asynchronous mode: event based asynchronous mode (EAP) and asynchronous programming model (APM).

. net review the old and learn the new: [5] asynchronous programming async await

 

1. Asynchronous programming

Asynchronous programming is a key technology, which can directly deal with blocking I/O and concurrent operations on multiple cores. Through the easy-to-use language level asynchronous programming model in C #, Visual Basic and F #,. NET can provide for applications and services, making them responsive and elastic.

The above explanation is about asynchronous programming. We use asynchronous programming more or less in our daily programming process. Why try asynchronous programming? Because files and network I/O are used during program processing, such as reading and writing files to disk and network request interface API, I/O API is generally blocked by default.
As a result, our user interface is stuck, the experience is poor, the hardware utilization of some servers is low, the service processing capacity and the request response are slow. Task based asynchronous API and language level asynchronous programming model change this model, and asynchronous execution can be carried out by default only by understanding a few new concepts.

The commonly used asynchronous programming mode is TAP mode, that is, the async and await keywords provided by C#. In fact, we have two other asynchronous modes: Event based asynchronous mode (EAP) , and Asynchronous programming model (APM) .

APM is asynchronous programming based on the IAsyncResult interface. For example, BeginRead of FileStream class and EndRead is the APM implementation method. It provides a pair of start and end methods to start and accept asynchronous results. Use BeginInvoke and EndInvoke of delegates to realize asynchronous programming.
EAP is introduced in. NET Framework 2.0, which is mostly reflected in WinForm programming. Many controls in WinForm programming process events based on event model. BeginInvoke and Invoke are often used when cross thread interface update is used. Event mode is a supplement to APM. It defines a series of events, including completion, progress and cancellation events, so that we can register the responding events for operation during asynchronous call.

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");

    }
}

In the above code, I use the delegate to implement the asynchronous call. The BeginAPM method uses BeginInvoke to start the asynchronous call, and then the DelegateAPMFun asynchronous method stops for 5 seconds. Take a look at the following print results. The print in the main method is before the print in the asynchronous method, indicating that the operation is asynchronous.

One line of code EndAPM(result) is annotated and invokes the delegate EndInvoke method, which will block the program until the asynchronous call is completed, so we can put it in an appropriate position to obtain the execution result, which is similar to the await keyword of TAP mode. Release and change the line of code for execution.

The above two methods are not recommended. They are obscure to write and understand. Those interested can understand them by themselves. Moreover, this method does not support delegated asynchronous calls in. net 5. Therefore, if you want to run it, you need to run it under the. net framework.
TAP is introduced in. NET Framework 4. It is the currently recommended asynchronous design pattern and the focus of our discussion in this paper. However, TAP is not necessarily a thread. It is a task, which is understood as the asynchronous abstraction of work, not the abstraction on threads.

2,async await

Asynchronous programming can be easily realized by using async await keyword. We need to add async keyword to the method. For asynchronous operations in the method, use await to wait for the completion of asynchronous operations before performing subsequent operations.

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");
    }
}

The AsyncAwaitTest method uses the async keyword, and uses the await keyword to wait for 5 seconds before printing "test end". Call the AsyncAwaitTest method in the Main method.

Using await to give control to its callers before the task is completed allows applications and services to perform useful work. After the task is completed, the code can continue to execute without relying on callbacks or events. Language and task API integration will do this for you.
The method using await must use the async keyword. If we want to wait for AsyncAwaitTest in the Main method, the Main method needs to add async and return Task.

3. async await principle

After compiling the above Main method without await call, use ILSpy to decompile the dll, and use C# 4.0 to see what the compiler has done for us. Because async await is not supported in 4.0, it will be decompiled to specific code. Async await syntax will be directly displayed after decompilation after 4.0.

After decompilation, you can see that a generic class is regenerated in the asynchronous method   d__1 realizing the interface IAsyncStateMachine, then calling the Start method, and calling some threads after processing in Start.   stateMachine.MoveNext()   That is, call d__1. Instantiate the MoveNext method of the 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);
		}
	}
}

Let's look at the classes generated by the compiler for  < AsyncAwaitTest>d__ one  :

The MoveNext method includes the AsyncAwaitTest logic code. Our source code has only one await operation. If there are multiple await operations, there should be multiple segmentation logic in MoveNext. Move next of different segments into different state segmentation blocks.
There is also an if judgment in this class, according to 1__ The state state parameter is - 1 at the beginning of the call, which is executed   num != 0   The business code in our business code if will be executed. At this time, the business code will be executed sequentially until await is encountered, and the following code will be executed

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;
}

In the process  <> t__ builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)   Pass await sentence to state machine   Awaitunsafeoncompleted method, which will find the operation of thread pool all the time.

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

The program puts the encapsulated task into the thread pool for calling. At this time, the asynchronous method switches to another thread or executes on the original thread (if the execution time of the asynchronous method is short, the thread switching may not be carried out, which mainly depends on the scheduler).
Status 1 after await is completed__ If the state has been changed to 0, the program will call MoveNext again. If there is no return or other logic after entering else, the execution will continue to the end.
You can see that this is an execution logic of state control, which is a“ State machine mode ”For the Main method call, the AsyncAwaitTest logic now enters if, and when it encounters await, it enters thread scheduling execution. If the asynchronous method switches to other thread calls, the method Main continues to execute. When the state machine execution switches to another state, move next again until the asynchronous method is executed.

4. async and thread

With the above foundation, we know that async and await are usually used in pairs. When our method is marked as asynchronous, the time-consuming operation inside await needs to be marked and wait for the completion of subsequent logic. The caller calling the asynchronous method can decide whether to wait or not, If await is not used, the caller executes the asynchronous method asynchronously or on the original thread.

If the method modified by async keyword does not contain await expression or statement, the method will be executed synchronously. You can optionally explicitly request the task to run on a separate thread through the Task.Run API.
You 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");
}

5. Cancelationtoken of canceling task

If you don't want to wait for the asynchronous method to complete, you can cancel the task through CancellationToken. CancellationToken is a struct. CancellationTokenSource is usually used to create CancellationToken, because CancellationTokenSource has some columns of [Methods] for us to cancel the task without operating the CancellationToken structure.

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

However, I modified the method and passed the CancellationToken to the asynchronous method, cts.CancelAfter(3000)   If the task is cancelled after 3 seconds, we listen to the canceltoken   IsCancellationRequested==true   Return directly.

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");
}

Because we end asynchronously by manually judging the status through the code, even if the task is finished after 3 seconds, but await Task.Delay(5000)   It will still wait for 5 seconds. Another way is to call ct.ThrowIfCancellationRequested() directly without determining whether to cancel   Give us judgment, this method if, but still can not end in time. At this time, we also have another processing method, that is, passing the CancellationToken to the asynchronous API method of await, which may or may not end immediately. This depends on the asynchronous implementation.

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

    //Call method processing cancel
    //ct.ThrowIfCancellationRequested();
    Console.WriteLine("test end");
}

6. Attention items

Do not use Thread.Sleep in asynchronous methods. There are two possibilities:
1. If Sleep is before await, it will directly block the caller's thread to wait for Sleep.
2. Sleep is after await, but await execution on the caller's thread will also block the caller's thread.
So we should use Task.Delay to wait for the operation. Then why do I use Thread.Sleep in the above Task.Run? Because Task.Run shows that the request runs on an independent thread, I know it will not block the caller. I just want to demonstrate above, so I don't recommend it.

Posted on Sun, 07 Nov 2021 19:52:34 -0500 by liljim