. net core bottom entry learning notes

Posted by WesPear on Fri, 21 Jan 2022 18:39:38 +0100

. net core bottom entry learning notes (8)

This is the main record Asynchronous operation in. net

preface

Asynchronous operation means that after an operation is executed, it does not wait for the end of the operation, but you can receive an additional notification after the end of the operation.

1, Blocking operations and event loops

As mentioned earlier, a thread is a task that the CPU can perform. Threads can go into sleep and wait to be awakened by the operating system to continue execution. Switching threads has a certain performance cost.
Many times, the program needs to call some blocking operations (such as some IO, some network connections, etc.) to make the thread enter the waiting state when the operating system schedules execution. After the operation is completed, the thread will be put into the waiting queue for wake-up execution. This will cause problems. Once there are many blocking operations, it means that many threads have to be started. The memory consumption, CPU occupation and thread switching of starting threads will cause performance problems.
In order to solve this problem, the initial method is to use the event loop mechanism, such as the cross platform select interface, the epoll interface of Linux, and the kqueue interface of OSX. The program needs to use one or more threads to obtain events, and then replace the execution of blocking operations with non blocking operations. The registered events are used to receive notifications after processing is completed. In this way, the thread issuing blocking operations can be changed to one, and one thread can create multiple blocking operations at the same time. Continuously receive and process events in the loop, execute corresponding callbacks for different events and event callback data, and complete the whole operation.
It is difficult to write programs based on event loops, and many code structures are similar. Therefore, on this basis (event loop basis), a framework is encapsulated, which is called asynchronous operation. Asynchronous operation provides a callback based mechanism. It will first execute non blocking operations, register events and associate callbacks, and automatically call the previously associated callbacks after receiving events. Famous asynchronous operation frameworks: C language libevent, C + + ASIO, JAVA Netty. Some operating systems also provide native interfaces, such as IOCP for Windows and AIO for Linux.

2, A Asynchronous programming model in. net

The. net framework itself supports asynchronous operations based on callbacks: APM, such as TCP connection functions, can use this mode to call back again in callbacks, so as to form a series of complex processing processes (this callback hell style is basically abandoned now)
The asynchronous programming model in. net is based on different implementations of different operating systems. At runtime, there are a certain number of threads used to wait for events and execute callbacks.
Note: the thread calling the callback is not necessarily the same as the thread executing the asynchronous callback. The asynchronous programming model is based on the event loop mechanism. Some threads are dedicated to waiting and processing events, and callbacks are used net internal thread pool. The reason why the thread handling callback events is not directly used to call callback is that it may affect other pending events.

3, Task parallel library

Using the callback based asynchronous programming model is not universal enough, and each asynchronous callback needs to write its own unique callback function.
. net encapsulates a layer of Task-based interface, which is now the famous task.

Note: Task is based on asynchronous programming model, asynchronous programming model and event loop callback mechanism.

The biggest feature of the task parallel library is the processing of asynchronous operation and registration callback. Make any asynchronous operation have the same method to register callback, wait for end and handle error.
Asynchronous operation returns system Threading. Tasks. Task type, or task < T> The former is used to indicate that the asynchronous operation has no return value, and the latter represents the return value of type T. Using the returned task type, call the ContinueWith method to register the callback method called after the asynchronous operation is completed. Note that the thread that the task parallel library calls the callback is not necessarily the same as the thread that performs asynchronous operations, but when creating a task, you can pass in an object-defined scheduling method based on the TaskScheduler type.

Implementation principle of task parallel library

Structure of Task parallel library in Task class:

  • m_action, the delegate object in the task running. If the commitment future mode is used, it is null here
  • m_continuationObject, the callback after the task is completed, which may be null, a single callback or a callback list
  • m_ Containgentproperties, which save infrequently used items and allocate them when necessary, mainly include the following: 1 m_ Capturedcontext, the execution context captured when creating a task; 2.m_completionEvent, which needs to synchronize the event object created when waiting for the task to end; 3.m_exceptionsHolder, which saves exceptions that occur during task execution; 4.m_completionCountdown: the current number of unfinished subtasks + 1, + 1 represents yourself; 5.m_exceptionalChildren, list of subtasks with exceptions; 6.m_parent, parent task.

There are two ways to use the task parallel library:
1. Directly specify the delegation and call task Run (action) and task Factory. Start with startnew (action) and call task ContinueWith registers callback, the delegate Action will be called in the internal thread pool, and the callback will be invoked after the task is completed.
2. Use it as a commitment object. The commitment future mode divides asynchrony into two objects. The commitment object is responsible for setting the result or exception. After the user completes the operation, use this object to set the result or exception; The future object is used to register the callback and accept the result or exception set by the commitment object. The common usage is to obtain a commitment object, associate it with a future object, set a callback for the future object, and set the result after the asynchronous completion of the commitment object.

In the Task parallel library, both the commitment object and the future object are implemented in the Task class, but the commitment object is not open to the public. The commitment object can only be called through the taskcompletionsource class. Usage:

var promise = new TaskCompletionSouce<object>();

//Get the Task in TaskCompletionSouce as the future object
var future = promise.Task;

//Callback registration of future objects
future.ContinueWith(arr =>{
...
})

//A wave of asynchronous operations. Set the result of the commitment object through TaskCompletionSouce
promise.SetResult(null);



The task parallel library also provides support for subtasks. The parent task will wait for all subtasks to complete, and the exceptions of the subtasks will be passed to the callback of the parent task.

Implementation of async and await keywords

Previously, there was a note in ET's notes that specifically described how ETTask in ET realized asynchrony, which can be seen together. The code is posted below net how to use state machine to help us realize async and await mechanism.

namespace dotcore_test
{
    class Program
    {
        static void Main(string[] args)
        {

            Task task = ConnetTest(IPAddress.Any, 8888, 5);
        }

        private static async Task ConnetTest(IPAddress address, int port, int x)
        {
            var tcpClient = new TcpClient();
            try
            {
                await tcpClient.ConnectAsync(address,port);
                var bytes = BitConverter.GetBytes(x);
                var stream = tcpClient.GetStream();
                await stream.WriteAsync(bytes, 0, bytes.Length);
                Console.WriteLine("connet and send {0}");
            }
            catch (SocketException e)
            {
                Console.WriteLine(e);
                throw;
            }

        }
    }

    
}
// Decompiled with JetBrains decompiler
// Type: dotcore_test.Program
// Assembly: dotcore_test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: 9AFBF37E-94F4-4084-9701-74571DD02522
// Assembly location: E:\dotcore_test\bin\Debug\net5.0\dotcore_test.dll
// Compiler-generated code is shown

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace dotcore_test
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      Program.ConnetTest(IPAddress.Any, 8888, 5);
    }

    [AsyncStateMachine(typeof (Program.<ConnetTest>d__1))]
    [DebuggerStepThrough]
    private static Task ConnetTest(IPAddress address, int port, int x)
    {
      Program.<ConnetTest>d__1 stateMachine = new Program.<ConnetTest>d__1();
      stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
      stateMachine.address = address;
      stateMachine.port = port;
      stateMachine.x = x;
      stateMachine.<>1__state = -1;
      stateMachine.<>t__builder.Start<Program.<ConnetTest>d__1>(ref stateMachine);
      return stateMachine.<>t__builder.Task;
    }

    public Program()
    {
      base.\u002Ector();
    }

    [CompilerGenerated]
    private sealed class <ConnetTest>d__1 : IAsyncStateMachine
    {
      public int <>1__state;
      public AsyncTaskMethodBuilder <>t__builder;
      public IPAddress address;
      public int port;
      public int x;
      private TcpClient <tcpClient>5__1;
      private byte[] <bytes>5__2;
      private NetworkStream <stream>5__3;
      private SocketException <e>5__4;
      private TaskAwaiter <>u__1;

      public <ConnetTest>d__1()
      {
        base.\u002Ector();
      }

      void IAsyncStateMachine.MoveNext()
      {
        int num1 = this.<>1__state;
        try
        {
          switch (num1)
          {
            case 0:
            case 1:
              try
              {
                TaskAwaiter awaiter1;
                int num2;
                TaskAwaiter awaiter2;
                switch (num1)
                {
                  case 0:
                    awaiter1 = this.<>u__1;
                    this.<>u__1 = new TaskAwaiter();
                    this.<>1__state = num2 = -1;
                    break;
                  case 1:
                    awaiter2 = this.<>u__1;
                    this.<>u__1 = new TaskAwaiter();
                    this.<>1__state = num2 = -1;
                    goto label_11;
                  default:
                    awaiter1 = this.<tcpClient>5__1.ConnectAsync(this.address, this.port).GetAwaiter();
                    if (!awaiter1.IsCompleted)
                    {
                      this.<>1__state = num2 = 0;
                      this.<>u__1 = awaiter1;
                      Program.<ConnetTest>d__1 stateMachine = this;
                      this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<ConnetTest>d__1>(ref awaiter1, ref stateMachine);
                      return;
                    }
                    break;
                }
                awaiter1.GetResult();
                this.<bytes>5__2 = BitConverter.GetBytes(this.x);
                this.<stream>5__3 = this.<tcpClient>5__1.GetStream();
                awaiter2 = this.<stream>5__3.WriteAsync(this.<bytes>5__2, 0, this.<bytes>5__2.Length).GetAwaiter();
                if (!awaiter2.IsCompleted)
                {
                  this.<>1__state = num2 = 1;
                  this.<>u__1 = awaiter2;
                  Program.<ConnetTest>d__1 stateMachine = this;
                  this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<ConnetTest>d__1>(ref awaiter2, ref stateMachine);
                  return;
                }
label_11:
                awaiter2.GetResult();
                Console.WriteLine("connet and send {0}");
                this.<bytes>5__2 = (byte[]) null;
                this.<stream>5__3 = (NetworkStream) null;
                break;
              }
              catch (SocketException ex)
              {
                this.<e>5__4 = ex;
                Console.WriteLine((object) this.<e>5__4);
                throw;
              }
            default:
              this.<tcpClient>5__1 = new TcpClient();
              goto case 0;
          }
        }
        catch (Exception ex)
        {
          this.<>1__state = -2;
          this.<tcpClient>5__1 = (TcpClient) null;
          this.<>t__builder.SetException(ex);
          return;
        }
        this.<>1__state = -2;
        this.<tcpClient>5__1 = (TcpClient) null;
        this.<>t__builder.SetResult();
      }

      [DebuggerHidden]
      void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
      {
      }
    }
  }
}


You can always understand the specific logic slowly. Points to note:
1. When executing the state machine, it is possible that the tasks contained in awaiter have been completed and the state machine state needs to be set.
2. The initial state is - 1. If it is waiting for awaiter1, the state machine state is 0. If it is waiting for awaiter2, the state machine state is 1
3.this.<> t__ builder. Awaitunsafeoncompleted this function has many internal flows. The most important function is to set callback.

Async and await can be decoupled from the task parallel library. The awaitable awaiter mode is defined, and the awaitable object is responsible for creating and obtaining the awaiter object. If a GetAwaiter method is defined in an object, and this method returns an object that implements the INoifyCompletion interface (i.e. awaiter object), then this object is a waitable object. In this way, async and await can be used without the task parallel library. ET has implemented its own set of asynchronous methods.

Additional Awaiter objects can directly implement the ICriticalNotifyCompletion interface, which inherits the INoifyCompletion interface. The difference is that implementing the ICriticalNotifyCompletion interface does not restore the execution context.

Stacked and heatless processes

Asynchronous functions that do not occupy threads are called coroutines (note that Unity implements a coroutine). The stacked coprocess is realized through user layer thread scheduling. It needs to rely on the stack space. For example, the coprocess in go performs asynchronous operation every time, saves the current register and stack space address, and switches back after recovery. Heap free coroutines are implemented through callbacks and do not need to rely on stack space. Advantages of heap Ctrip: there is no need to dynamically allocate memory to save callback data, and there is no heap Ctrip. Advantages: it occupies small memory and does not need to support stack space expansion.

4, Asynchronous local variables and execution context

Thread local variables are not applicable to asynchronous operation code, because the thread executing asynchrony in asynchronous operation (especially asynchronous operation using Task Parallel Library) is not necessarily the same as that of callback.
. net implements asynchronous local variables through execution context (pay attention to distinguish thread context). Each managed thread object holds an execution context object (used to hold the data type of asynchronous local variables).
When the Task parallel library creates a Task, the execution context of the current managed thread will be recorded and restored before the callback is executed.

Specific steps:

  • The execution context of the current managed thread is saved in thread m_ ExecutionContext member;
  • The default execution context is saved in the global variable ExecutionContext Default, there are no asynchronous local variables in it
  • When creating a Task, call ExecutionContext The capture() method obtains the execution context of the current managed thread: 1 If capture is not currently prohibited, the execution context of the current thread is returned (if the current execution context is null, the default execution context is returned) 2 Null if capture is prohibited
  • If the returned execution context is not the default execution context and is not null, the current execution context will be recorded to task m_ contingentProperties. m_ Capturedcontext member
  • When the callback is completed after the asynchronous operation is completed, check whether the execution context recorded in Task is null:1.. If it is null, the callback will be executed directly; 2. If it is not null, call ExecutionContext Runinternal method and pass in the execution context and callback. Specifically: back up the current execution context (because a managed thread may have been changed), set the current execution context as the incoming execution context, execute the delegate (callback), and restore the current context as the backup context after execution.
    Note that the execution context is an invariant object. Each time you modify an asynchronous local variable, a new execution context will be created to overwrite the current execution context, and the modification will not be reflected to the call source. Before calling asynchrony, you can call ExecutionContext SuppressFlow() prohibits capturing the current execution context, that is, the asynchronous execution variable value cannot be recovered during callback. If the capture is prohibited, you can restore the capture through Undo() of AsyncFlowControl type with the return value of SuppressFlow().

5, Synchronization context

The above mentioned security mechanisms are provided when resources are accessed by multiple threads; Another security mechanism is to allow specific resources to be accessed only by the specified thread, which enables the specified thread to perform the specified operation. How to work in net, which specifies another thread and relies on the synchronization context mechanism.
The synchronization context is divided into two parts, a sending part and a receiving part net runtime provides the basic class of the sending part. If you want to use the synchronization context, you must provide your own implementation. There are two ways to Send: Send, Send the delegate to the specified location and wait for execution to complete; Post, Send the delegate to the specified location but do not wait for execution to complete.
Each thread can have its own synchronization context or not. Some special threads have their own synchronization context implementation (such as WinForm thread).
By default, await will automatically capture the previous thread synchronization context. When a callback is called (that is, the subsequent code execution of await), the previously recorded synchronization context will be automatically used for callback processing. See the following logic for details

class Program
    {
        class TestSynchronizationContext:SynchronizationContext
        {
            public override void Send(SendOrPostCallback d, object? state)
            {
                Console.WriteLine("({0} trhead88888)",Thread.CurrentThread.ManagedThreadId);
            }

            public override void Post(SendOrPostCallback d, object? state)
            {
                Console.WriteLine("({0} trhead000000)",Thread.CurrentThread.ManagedThreadId);
                _workItems.Enqueue((d, state));
            }
            
            private readonly ConcurrentQueue<(SendOrPostCallback Callback, object State)> _workItems;
            public TestSynchronizationContext()
            {
                _workItems = new ConcurrentQueue<(SendOrPostCallback Callback, object State)>();
                var thread = new Thread(StartLoop);
                Console.WriteLine("TestSynchronizationContext.ThreadId:{0}", thread.ManagedThreadId);
                thread.Start();
                void StartLoop()
                {
                    while (true)
                    {
                        if (_workItems.TryDequeue(out var workItem))
                        {
                            workItem.Callback(workItem.State);
                        }
                    }
                }
            }
            
        }
        
        
        static async Task Main(string[] args)
        {
            
            var context = new TestSynchronizationContext();
            SynchronizationContext.SetSynchronizationContext(context);
            Console.WriteLine("({0} trhead11111)",Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(1000);
            Console.WriteLine("({0} trhead22222)",Thread.CurrentThread.ManagedThreadId);
        }

Result display:

TestSynchronizationContext.ThreadId:5
(1 trhead11111)
(6 trhead000000)
(5 trhead22222)

A synchronization context is constructed above, and the context of the current thread is set as the synchronization context. At the same time, a new thread 5 is opened in the constructor to continue from_ Take the callback from the workItems queue for execution, and see that the newly opened thread id is 5.
You can also see that when await is called, the subsequent code will be encapsulated into a callback by default, The Post of the synchronization context is invoked for processing. (after repeated verification, it is found that only when the thread in the user-defined synchronization context is set to use await Task, can post processing be used. During the first await, when a new thread is changed and await is called again, the new thread needs to set the synchronization context again to ensure that the second await can call the post implicit processing of the synchronization context.). If no synchronization context is set, the default synchronization context is used. As mentioned earlier, the await Task callback is not necessarily executed by the previous thread. The callback is executed here (i.e. the post thread is No. 6), but the synchronization context used is indeed the previously set synchronization context, and its callback can be put into_ workItems. Then synchronize thread 5 in the context constructor, continuously take out the callback for execution, and execute console Writeline ('({0} trhead22222)' is thread 5.

Conclusion:
1. Using await, the callback is not necessarily executed by the previous thread, but the synchronization context of this thread is the synchronization context set by the previous thread. With this feature, all the code after awaiter can be returned to a thread for execution.
2. Note that when await Task is used for the first time in the function, the thread executing the callback (which may be different from the previous thread, depending on the implementation mechanism of Task or Task like Task) will execute using the Post of the current context, and the second await will not enter the Post again (because it is a new thread, and the new thread does not set the synchronization context), Therefore, it is recommended to use the Post or Send method to display the synchronization context if the synchronization context is required.
3. Each thread can have a synchronization object. When using communication, you can use variables to make threads Post or Send through this variable, and then process callbacks into a unified thread, avoiding the problem of resource access between threads
4. When using the synchronization context, you need to pay attention to the deadlock problem. In particular, when using the Send method, you need to wait for the execution to be completed, while other threads Send delegates to the synchronization context for waiting. The best way is not to block the threads executing delegates in the synchronization context.

summary

This article mainly records the of asynchronous operation net, based on event loop mechanism; Task parallel library, which simplifies asynchronous operations; The powerful async and await syntax based on task and the implementation mechanism of its internal state machine; Asynchronous local variables implemented by the execution context (task will automatically capture and replace); Implemented by the synchronization context, the specified thread performs the specified operation (await will use the synchronization context Post of the current thread to perform callback).

Topics: .NET