Understand task and async await

Time:2021-11-29

This article will explain in detail the task in the c# class and the relationship between async await and task

1、 Past and present life of task

1.Thread

At the beginning, when we need to create threads, we usually create threads through threads. Generally, there are the following ways to create threads:

static void Main(string[] args)
        {
            Console.WriteLine("begin");

            Thread thread = new Thread(() => TestMethod(2));
            thread.IsBackground = true;// Set as background thread, default foreground thread
            thread.Start();

            Thread thread1 = new Thread(() => TestMethod1());
            //Set the priority of thread1 to the highest. The system schedules the thread in unit time as much as possible. The default is normal
            thread1.Priority = ThreadPriority.Highest;
            thread1.Start();

            Thread thread2 = new Thread((state) => TestMethod2(state));
            thread2.Start("data");
            thread2.Join();// Wait for thread2 execution to complete
            Console.WriteLine("end");
        }

        static void TestMethod(int a)
        {
            Thread.Sleep(1000);
            Console.WriteLine($"TestMethod: run on Thread id :{Thread.CurrentThread.ManagedThreadId},is threadPool:{Thread.CurrentThread.IsThreadPoolThread}" +
                $",is Backgound:{Thread.CurrentThread.IsBackground}, result:{a}");
        }

        static void TestMethod1()
        {
            Thread.Sleep(1000);
            Console.WriteLine($"TestMethod1: run on Thread id :{Thread.CurrentThread.ManagedThreadId},is threadPool:{Thread.CurrentThread.IsThreadPoolThread}" +
                $",is Backgound:{Thread.CurrentThread.IsBackground},no result ");
        }

        static void TestMethod2(object state)
        {
            Thread.Sleep(2000);
            Console.WriteLine($"TestMethod2 :run on Thread id :{Thread.CurrentThread.ManagedThreadId},is threadPool:{Thread.CurrentThread.IsThreadPoolThread}" +
               $",is Backgound:{Thread.CurrentThread.IsBackground},result:{state}");
        }

Output results:

begin
TestMethod: run on Thread id :4,is threadPool:False,is Backgound:True, result:2
TestMethod1: run on Thread id :5,is threadPool:False,is Backgound:False,no result
TestMethod2 :run on Thread id :7,is threadPool:False,is Backgound:False,result:data
end

or

begin
TestMethod1: run on Thread id :5,is threadPool:False,is Backgound:False,no result
TestMethod: run on Thread id :4,is threadPool:False,is Backgound:True, result:2
TestMethod2 :run on Thread id :7,is threadPool:False,is Backgound:False,result:data
end

Since my PC is a multi-core CPU, the two threads of testmethod and testmethod1 are really parallel, so the output results may be uncertain. Although the priority of the thread of testmethod1 is set to highest, the system may not give priority to scheduling. In fact, thread.start is not recommended to create threads at present. The disadvantages are as follows:

  • Because when a large number of threads need to be created, using thread. Start to create threads will waste thread resources, because threads run out and are not reusable
  • Now, the CLR in a process will create a thread pool and some working threads by default (don’t waste), and the working threads in the thread pool will return to the thread pool after running out, which can be reused,

Except for the following reasons:

  • Do you really need operation thread priority

  • A foreground thread needs to be created. Because it is similar to the console program, the process will exit when the initial foreground thread is executed, creating a foreground thread can ensure the normal and successful execution of the foreground thread before the process exits

    For example, comment out the original examplethread2.Join();, we will find that after outputting the console, the initial foreground thread outputs end and does not exit the process. It only exits after testmethod2 (the thread is frozen for 2 seconds)

    static void Main(string[] args)
            {
                Console.WriteLine("begin");
    
                Thread thread = new Thread(() => TestMethod(2));
                thread.IsBackground = true;// Set as background thread, default foreground thread
                thread.Start();
    
                Thread thread1 = new Thread(() => TestMethod1());
                //Set the priority of thread1 to the highest. The system schedules the thread in unit time as much as possible. The default is normal
                thread1.Priority = ThreadPriority.Highest;
                thread1.Start();
    
                Thread thread2 = new Thread((state) => TestMethod2(state));
                thread2.Start("data");
                //thread2.Join();// Wait for thread2 execution to complete
                Console.WriteLine("end");
            }

    Output:

    begin
    end
    TestMethod1: run on Thread id :5,is threadPool:False,is Backgound:False,no result
    TestMethod: run on Thread id :4,is threadPool:False,is Backgound:True, result:2
    TestMethod2 :run on Thread id :7,is threadPool:False,is Backgound:False,result:data
  • A background thread needs to be created to execute for a long time. In fact, the taskscheduler of a task also creates a background thread thread in the default setting of taskcreationoptions.longrunning, rather than executing in the ThreadPool. When some other functions of the task are not required, the thread is lighter

    Thread longTask = new Thread(() => Console.WriteLine("doing long Task..."));
      longTask.IsBackground = true;
      longTask.Start();
    
    //Equivalent to
    
       new Task(() => Console.WriteLine("doing long Task..."), TaskCreationOptions.LongRunning).Start();
       //OR
       Task.Factory.StartNew(() => Console.WriteLine("doing long Task..."), TaskCreationOptions.LongRunning);

2.ThreadPool

When a. Net process is initialized, the CLR will open up a memory space for the ThreadPool. By default, the ThreadPool has no threads. Internally, it will maintain a task request queue. When there are tasks in the queue, the thread pool will request the queue to execute tasks by opening up working threads (all background threads). After the task is executed, it will return to the thread pool, As far as possible, the thread pool will use the returned working thread to execute (reduce development). If the thread pool is not returned, a new thread will be opened for execution, and then return to the thread pool after execution. The approximate thread pool model is as follows:

From the code:

static void Main(string[] args)
        {
            //Gets the maximum worker thread tree and the maximum number of I / O asynchronous threads allowed in the default thread pool
            ThreadPool.GetMaxThreads(out int maxWorkThreadCount, 
                                     out int maxIOThreadCount);
            Console.WriteLine($"maxWorkThreadCount:{maxWorkThreadCount},
                              maxIOThreadCount:{maxIOThreadCount}");
            //Gets the number of concurrent worker threads and I / O asynchronous threads in the default thread pool
            ThreadPool.GetMinThreads(out int minWorkThreadCount, 
                                     out int minIOThreadCount);
            Console.WriteLine($"minWorkThreadCount:{minWorkThreadCount},
                              minIOThreadCount:{minIOThreadCount}");
            for (int i = 0; i < 20; i++)
            {
                ThreadPool.QueueUserWorkItem(s =>
                {
                    var workThreadId = Thread.CurrentThread.ManagedThreadId;
                    var isBackground = Thread.CurrentThread.IsBackground;
                    var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;
                    Console.WriteLine($"work is on thread {workThreadId}, 
                                      Now time:{DateTime.Now.ToString("ss.ff")}," +
                        $" isBackground:{isBackground}, isThreadPool:{isThreadPool}");
                    Thread.Sleep(5000);// Simulate worker thread running
                });
            }
            Console.ReadLine();
        }

The output is as follows:

maxWorkThreadCount:32767,maxIOThreadCount:1000
minWorkThreadCount:16,minIOThreadCount:16
work is on thread 18, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 14, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 16, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 5, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 13, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 12, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 10, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 4, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 15, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 7, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 19, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 17, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 8, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 11, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 9, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 6, Now time:06.50, isBackground:True, isThreadPool:True

work is on thread 20, Now time:07.42, isBackground:True, isThreadPool:True
work is on thread 21, Now time:08.42, isBackground:True, isThreadPool:True
work is on thread 22, Now time:09.42, isBackground:True, isThreadPool:True
work is on thread 23, Now time:10.42, isBackground:True, isThreadPool:True

Since my CPU is 8-core and 16 threads, the default thread pool allocates 16 working threads and I / O threads to me to ensure real parallelism in this process. It can be seen that the start time of the first 16 working threads is consistent. To the last four, the thread pool attempts to use the previous working thread to request that task queue to execute tasks, Since the first 16 are still running and are not returned to the thread pool, a new worker thread is created every second to request execution, and the maximum number of threads opened is related to the maximum number of worker thread trees and I / O asynchronous threads allowed to be opened by the thread pool

We can passThreadPool.SetMaxThreadsSet the maximum number of working threads to 16, and add a few lines of code before executing the task:

var success = ThreadPool.SetMaxThreads(16, 16);// Only > = minimum concurrent worker threads and I / O threads can be set
Console.WriteLine($"SetMaxThreads success:{success}");
ThreadPool.GetMaxThreads(out int maxWorkThreadCountNew, out int maxIOThreadCountNew);
Console.WriteLine($"maxWorkThreadCountNew:{maxWorkThreadCountNew},
                  maxIOThreadCountNew:{maxIOThreadCountNew}");

The output is as follows:

maxWorkThreadCount:32767,maxIOThreadCount:1000
minWorkThreadCount:16,minIOThreadCount:16
SetMaxThreads success:True
maxWorkThreadCountNew:16,maxIOThreadCountNew:16
work is on thread 6, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 12, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 7, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 8, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 16, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 10, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 15, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 13, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 11, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 4, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 9, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 19, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 17, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 5, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 14, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 18, Now time:01.71, isBackground:True, isThreadPool:True

work is on thread 8, Now time:06.72, isBackground:True, isThreadPool:True
work is on thread 5, Now time:06.72, isBackground:True, isThreadPool:True
work is on thread 19, Now time:06.72, isBackground:True, isThreadPool:True
work is on thread 10, Now time:06.72, isBackground:True, isThreadPool:True

It is clear that since the thread pool only allows 16 worker threads and I / O threads at most, after another 16 threads are opened in the thread pool, no new threads will be opened, and new tasks can only wait for the previous worker threads to execute the return thread pool, and then use the returned thread to execute the new task, resulting in the start execution time of the new task in 5 seconds

ThreadPool has the following advantages:

  • The default thread pool has been configured according to its own CPU. When complex multi task parallelism is required, intelligence can achieve balance in time and space. It has certain advantages in CPU intensive operation, rather than requiring its own judgment and consideration like thread. Start
  • You can also use some methods through the thread pool, such asThreadPool.SetMaxThreadsManually configure the thread pool, which is convenient to simulate the execution of different computer hardware
  • There are special I / O threads, which can realize non blocking I / O. I / O intensive operation has advantages (mentioned in subsequent tasks)

However, the disadvantages are also obvious:

  • ThreadPool native does not support interactive operations such as cancellation, completion and failure notification of working threads. It also does not support obtaining function return values. It is not flexible enough. Thread native has options such as abort (also not recommended), join, etc
  • It is not suitable for longtask, because it will cause multiple threads to be created in the thread pool (as can be seen from the above code). At this time, you can use thread to execute longtask alone

3.Task

In. Net 4.0, the task parallel library, the so-called TPL (Task Parallel Library), was introducedTaskClasses and support return valuesTaskAt the same time, in 4.5, the use of task is improved and optimized. Task solves some of the above problems of thread and ThreadPool. What exactly is a task? Let’s look at the following code:

The following is a WPF application. Click event on button:

private void Button_Click(object sender, RoutedEventArgs e)
 {
     Task.Run(() =>
     {
         var threadId = Thread.CurrentThread.ManagedThreadId;
         var isBackgound = Thread.CurrentThread.IsBackground;
         var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;
         Thread.Sleep(3000);// Simulate time-consuming operations
         Debug.WriteLine($"task1 work on thread:{threadId},isBackgound:{isBackgound},isThreadPool:{isThreadPool}");
            });
         new Task(() =>
         {
             var threadId = Thread.CurrentThread.ManagedThreadId;
             var isBackgound = Thread.CurrentThread.IsBackground;
             var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;
             Thread.Sleep(3000);// Simulate time-consuming operations
             Debug.WriteLine($"task2 work on thread:{threadId},isBackgound:{isBackgound},isThreadPool:{isThreadPool}");
         }).Start(TaskScheduler.FromCurrentSynchronizationContext());

         Task.Factory.StartNew(() =>
         {
            var threadId = Thread.CurrentThread.ManagedThreadId;
            var isBackgound = Thread.CurrentThread.IsBackground;
            var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;
            Thread.Sleep(3000);// Simulate time-consuming operations
            Debug.WriteLine($"task3 work on thread:{threadId},isBackgound:{isBackgound},isThreadPool:{isThreadPool}");
          }, TaskCreationOptions.LongRunning);
    }

Output:

main thread id :1
//Because it is parallel, the sequence of output results may be different every time
task1 work on thread:4,isBackgound:True,isThreadPool:True
task3 work on thread:10,isBackgound:True,isThreadPool:False
task2 work on thread:1,isBackgound:False,isThreadPool:False

I use three different tasks to develop ways to run tasks. You can see that tasks run on three different threads:

  • Task1 runs on the thread pool without any task settings
  • Task2 by settingTaskSchedulerbyTaskScheduler.FromCurrentSynchronizationContext()There is no open thread, using the main thread to run
  • Task3 by settingTaskCreationOptionsbyLongRunningAnd defaultTaskSchedulerIn this case, a background thread is actually opened to run

Therefore, in fact, a task does not necessarily mean that a new thread has been opened. It can run on the thread pool, or a background thread has been opened, or there is no thread. Run the task through the main thread. Here is a sentenceTaskScheduler.FromCurrentSynchronizationContext(), if you run the console or asp.net core program, an error will occur because the synchronizationcontext of the main thread is empty. You can know from the taskscheduler source code:

public static TaskScheduler FromCurrentSynchronizationContext()
{
     return new SynchronizationContextTaskScheduler();
}
        
internal SynchronizationContextTaskScheduler()
{
     m_synchronizationContext = SynchronizationContext.Current ??
     throw new InvalidOperationException
     (SR.TaskScheduler_FromCurrentSynchronizationContext_NoCurrent);
}

For tasks, after setting through taskscheduler and taskcreationoptions, tasks are assigned to different threads, as shown in the following figure:

Native supports continuation, cancellation and exception (failure notification)

1. Continuation

There are actually two ways to continue a task. One is throughContinueWithMethod, which is supported by task in. Net framework 4.0. One is throughGetAwaiterMethod is supported in. Net framework 4.5, and this method is also used by async await asynchronous function

Console Code:

static void Main(string[] args)
 {
      Task.Run(() =>
      {
          Console.WriteLine($"ContinueWith:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
                return 25;
      }).ContinueWith(t =>
      {
          Console.WriteLine($"ContinueWith Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
          Console.WriteLine($"ContinueWith Completed:{t.Result}");
      });

//Equivalent to
     
     var awaiter = Task.Run(() =>
     {
          Console.WriteLine($"GetAwaiter:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
          return 25;
     }).GetAwaiter();
     awaiter.OnCompleted(() =>
     {
          Console.WriteLine($"GetAwaiter Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
          Console.WriteLine($"GetAwaiter Completed:{awaiter.GetResult()}");
     });

     Console.ReadLine();
}

Output results:

ContinueWith:threadId:4,isThreadPool:True
GetAwaiter:threadId:5,isThreadPool:True
GetAwaiter Completed:threadId:5,isThreadPool:True
GetAwaiter Completed:25
ContinueWith Completed:threadId:4,isThreadPool:True
ContinueWith Completed:25

//In fact, the running code thread may not be the same thread as the continuation thread, depending on the scheduling of the thread pool itself
You can manually set taskcontinuationoptions. Executesynchronously (same thread)
Or taskcontinuationoptions.runcontinuationsasynchronously (different threads)
The default runcontinuationsasynchronously priority is greater than executesynchronously

Interestingly, the output of the same code is different in WPF / WinForm and other programs:

WPF program code:

private void Button_Click(object sender, RoutedEventArgs e)
        {
            Task.Run(() =>
            {
                Debug.WriteLine($"ContinueWith:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
            }).ContinueWith(t =>
            {
                Debug.WriteLine($"ContinueWith Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
            }, TaskContinuationOptions.ExecuteSynchronously);


            Task.Run(() =>
            {
                Debug.WriteLine($"GetAwaiter:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
            }).GetAwaiter().OnCompleted(() =>
            {
                Debug.WriteLine($"GetAwaiter Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
            });
        }

Output:

ContinueWith:threadId:7,isThreadPool:True
GetAwaiter:threadId:9,isThreadPool:True
ContinueWith Completed:threadId:7,isThreadPool:True
GetAwaiter Completed:threadId:1,isThreadPool:False

The reason isGetAwaiter().OnCompleted()I’ll check if there’s anySynchronizationContextTherefore, it is actually equivalent to the following code:

Task.Run(() =>
  {
       Debug.WriteLine($"GetAwaiter:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
  }).ContinueWith(t =>
  {
       Debug.WriteLine($"GetAwaiter Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
  },TaskScheduler.FromCurrentSynchronizationContext());

If you want to get the effect of console in WPF program, you only need to modify it toConfigureAwait(false), the continuation task is not presentSynchronizationContextOK, as follows:

Task.Run(() =>
 {
      Debug.WriteLine($"GetAwaiter:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
 }).ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
 {
     Debug.WriteLine($"GetAwaiter Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
 });

2. Cancellation

While. Net framework 4.0 brings tasks, it also brings classes related to canceling tasksCancellationTokenSourceandCancellationTokenNext, we will roughly demonstrate its usage

WPF program code is as follows:

CancellationTokenSource tokenSource;


private void BeginButton_Click(object sender, RoutedEventArgs e)
{

      tokenSource = new CancellationTokenSource();
      LongTask(tokenSource.Token);
}
        
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
      tokenSource?.Cancel();
}

private void LongTask(CancellationToken cancellationToken)
{
      Task.Run(() =>
      {
          for (int i = 0; i < 10; i++)
          {
               Dispatcher.Invoke(() =>
               {
                  this.tbox.Text += $"now is {i} \n";
               });
               Thread.Sleep(1000);
               if (cancellationToken.IsCancellationRequested)
               {
                   MessageBox. Show ("the operation was canceled");
                   return;
               }
           }
        }, cancellationToken);
}

The effects are as follows:

In fact, the above code can also be applied to thread and ThreadPool, which is equivalent to the following code:

//When taskcreationoptions are longrunning and the default taskscheduler
new Thread(() =>
{
    for (int i = 0; i < 10; i++)
    {
         Dispatcher.Invoke(() =>
         {
            this.tbox.Text += $"now is {i} \n";
         });
         Thread.Sleep(1000);
         if (cancellationToken.IsCancellationRequested)
         {
             MessageBox. Show ("the operation was canceled");
             return;
         }
   }
}).Start();

//By default, taskscheduler
ThreadPool.QueueUserWorkItem(t =>
{
      for (int i = 0; i < 10; i++)
      {
           Dispatcher.Invoke(() =>
           {
                this.tbox.Text += $"now is {i} \n";
           });
           Thread.Sleep(1000);
           if (cancellationToken.IsCancellationRequested)
           {
               MessageBox. Show ("the operation was canceled");
               return;
           }
      }
});

Therefore, after. Net framework 4.0ThreadandThreadPoolIt can also passCancellationTokenSourceandCancellationTokenClass supports the cancel function, but in general, both of them can be set through task, and the bottom layer can also callThreadandThreadPoolTherefore, it is generally not used in this way, and many basic methods of task are supported by default, such as task.wait, task.waitall, task.waitany, task.whenall, task.whenany, task.delay, etc

3. Exception (failure notification)

The following console Code:

static void Main(string[] args)
 {
      var parent = Task.Factory.StartNew(() =>
      {
            int[] numbers = { 0 };
            var childFactory = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.None);
            childFactory.StartNew(() => 5 / numbers[0]); // Division by zero 
            childFactory.StartNew(() => numbers[1]); // Index out of range 
            childFactory.StartNew(() => { throw null; }); // Null reference 
       });
       try
       {
            parent.Wait();
       }
       catch (AggregateException aex)
       {
            foreach (var item in aex.InnerExceptions)
            {
                Console.WriteLine(item.InnerException.Message.ToString());
            }
        }
        Console.ReadLine();
   }

The output is as follows:

Try dividing by zero.
Index exceeds array bounds.
The object reference is not set to an instance of the object.

The parent task has three subtasks. The three parallel subtasks throw different exceptions and return to the parent task. When you wait for the parent task or obtain its result attribute, an exception will be thrown. Using aggregateexception, you can put all exceptions in its innerexception exception list, and we can handle different exceptions respectively, This is very easy to use when multitasking is parallel, and the function of aggregateexception is extremely powerful, far more than the above functions. However, if you are only a single task and use aggregateexception than ordinary, it will actually waste performance. You can also do this;

try
{
     var task = Task.Run(() =>
     {
         string str = null;
         str.ToLower();
         return str;
     });
     var result = task.Result;
}
catch (Exception ex)
{

     Console.WriteLine(ex.Message.ToString());
}

//Or async await
try
{
      var result = await Task.Run(() =>
      {
          string str = null;
          str.ToLower();
          return str;
      });
      
catch (Exception ex)
{

      Console.WriteLine(ex.Message.ToString());
}

Output:

The object reference is not set to an instance of the object.

2、 Asynchronous function async await

Async await is a c#5.0 syntax introduced during the period of. Net framework 4.5. It forms a new asynchronous programming model, namely tap (Task-Based asynchronous pattern), task-based asynchronous mode, through the task parallel library introduced with. Net framework 4.0, that is, the so-called TPL (Task Parallel Library)

Syntax sugar async await

Let’s write down the code and see the usage of async await:

Here is a console Code:

static async Task Main(string[] args)
 {
     var result = await Task.Run(() =>
     {
         Console.WriteLine($"current thread:{Thread.CurrentThread.ManagedThreadId}," +
                    $"isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
         Thread.Sleep(1000);
         return 25;
     });
    Console.WriteLine($"current thread:{Thread.CurrentThread.ManagedThreadId}," +
    $"isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
    Console.WriteLine(result);
    Console.ReadLine();
 }

Output results:

current thread:4,isThreadPool:True
current thread:4,isThreadPool:True
25

Instead, execute in WPF / WinForm program, and the results are as follows:

current thread:4,isThreadPool:True
current thread:1,isThreadPool:false
25

Does it feel like deja vu? The colored eggs buried above are revealed here. When talking about the continuation of task, we talked about the adoption of. Net framework 4.5GetAwaiterContinuation method, in fact,async awaitIt is the above syntax sugar. It will be roughly compiled like that when compiling, so we usually don’t write it manuallyGetAwaiterBut throughasync await, it greatly simplifies the programming method and says it is a syntax sugar, so what is the evidence?

Let’s write some more code to verify:

class Program
{
    static void Main(string[] args)
    {
       ShowResult(classType: typeof(Program), methodName: nameof(AsyncTaskResultMethod));
       ShowResult(classType: typeof(Program), methodName: nameof(AsyncTaskMethod));
       ShowResult(classType: typeof(Program), methodName: nameof(AsyncVoidMethod));
       ShowResult(classType: typeof(Program), methodName: nameof(RegularMethod));
       Console.ReadKey();
    }

    public static async Task AsyncTaskResultMethod()
    {
       return await Task.FromResult(5);
    }

    public static async Task AsyncTaskMethod()
    {
       await new TaskCompletionSource().Task;
    }

    public static async void AsyncVoidMethod()
    {

    }

    public static int RegularMethod()
    {
        return 5;
    }

    private static bool IsAsyncMethod(Type classType, string methodName)
    {
       MethodInfo method = classType.GetMethod(methodName);

       Type attType = typeof(AsyncStateMachineAttribute);

       var attrib = (AsyncStateMachineAttribute)method.GetCustomAttribute(attType);

       return (attrib != null);
    }

    private static void ShowResult(Type classType, string methodName)
    {
       Console.Write((methodName + ": ").PadRight(16));

       if (IsAsyncMethod(classType, methodName))
           Console.WriteLine("Async method");
       else
           Console.WriteLine("Regular method");
    }
}

Output:

AsyncTaskResultMethod: Async method
AsyncTaskMethod: Async method
AsyncVoidMethod: Async method
RegularMethod:  Regular method

In fact, async only allows the method name, and the return value is voidTaskandTaskOtherwise, compilation errors will occur. In fact, this is related to the compiled results. We decompile this code through ilspy. Screenshot of the key code:

internal class Program
{
  [CompilerGenerated]
  private sealed class d__1 : IAsyncStateMachine
  {
	  public int <>1__state;
	  public AsyncTaskMethodBuilder <>t__builder;
	  private int <>s__1;
	  private TaskAwaiter <>u__1;
	  void IAsyncStateMachine.MoveNext()
	  {
		  int num = this.<>1__state;
		  int result;
		  try
		  {
			 TaskAwaiter awaiter;
			 if (num != 0)
			 {
				awaiter = Task.FromResult(5).GetAwaiter();
				if (!awaiter.IsCompleted)
				{
					this.<>1__state = 0; 
					this.<>u__1 = awaiter;
				    Program.d__1 d__ = this;
					this.<>t__builder.AwaitUnsafeOnCompleted, Program.d__1>(ref awaiter, ref d__);
					return;
				}
		         }
		         else
		         {
		                awaiter = this.<>u__1;
				this.<>u__1 = default(TaskAwaiter);
				this.<>1__state = -1;
		         }
			 this.<>s__1 = awaiter.GetResult();
			 result = this.<>s__1;
		  }
		  catch (Exception exception)
		  {
			this.<>1__state = -2;
			this.<>t__builder.SetException(exception);
			return;
		  }
		  this.<>1__state = -2;
		  this.<>t__builder.SetResult(result);
	}
	[DebuggerHidden]
	void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
	{
	}
  }
    
  [CompilerGenerated]
  private sealed class d__2 : IAsyncStateMachine
  {
	  public int <>1__state;
	  public AsyncTaskMethodBuilder <>t__builder;
	  private TaskAwaiter <>u__1;
	  void IAsyncStateMachine.MoveNext()
	  {
		   int num = this.<>1__state;
		   try
		   {
				TaskAwaiter awaiter;
				if (num != 0)
				{
					awaiter = new TaskCompletionSource().Task.GetAwaiter();
					if (!awaiter.IsCompleted)
					{
						this.<>1__state = 0;
						this.<>u__1 = awaiter;
						Program.d__2 d__ = this;
						this.<>t__builder.AwaitUnsafeOnCompleted, Program.d__2>(ref awaiter, ref d__);
						return;
					}
				}
				else
				{
					awaiter = this.<>u__1;
					this.<>u__1 = default(TaskAwaiter);
					this.<>1__state = -1;
				}
				awaiter.GetResult();
			}
			catch (Exception exception)
			{
				this.<>1__state = -2;
				this.<>t__builder.SetException(exception);
				return;
			}
			this.<>1__state = -2;
			this.<>t__builder.SetResult();
		}
      
		[DebuggerHidden]
		void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
		{
		}
	}
    
    private sealed class d__3 : IAsyncStateMachine
	{
		public int <>1__state;
		public AsyncVoidMethodBuilder <>t__builder;
		void IAsyncStateMachine.MoveNext()
		{
			int num = this.<>1__state;
			try
			{
			}
			catch (Exception exception)
			{
				this.<>1__state = -2;
				this.<>t__builder.SetException(exception);
				return;
			}
			this.<>1__state = -2;
			this.<>t__builder.SetResult();
		}
		[DebuggerHidden]
		void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
		{
		}
	}
    
   [DebuggerStepThrough, AsyncStateMachine(typeof(Program.d__1))]
   public static Task AsyncTaskResultMethod()
   {
	   Program.d__1 d__ = new Program.d__1();
	  d__.<>t__builder = AsyncTaskMethodBuilder.Create();
	  d__.<>1__state = -1;
	  d__.<>t__builder.Startd__1>(ref d__);
	  return d__.<>t__builder.Task;
	}
    
  [DebuggerStepThrough, AsyncStateMachine(typeof(Program.d__2))]
   public static Task AsyncTaskMethod()
   {
		Program.d__2 d__ = new Program.d__2();
		d__.<>t__builder = AsyncTaskMethodBuilder.Create();
		d__.<>1__state = -1;
		d__.<>t__builder.Startd__2>(ref d__);
		return d__.<>t__builder.Task;
   }

   [DebuggerStepThrough, AsyncStateMachine(typeof(Program.d__3))]
   public static void AsyncVoidMethod()
   {
	Program.d__3 d__ = new Program.d__3();
	d__.<>t__builder = AsyncVoidMethodBuilder.Create();
	d__.<>1__state = -1;
	d__.<>t__builder.Startd__3>(ref d__);
   }
    
   public static int RegularMethod()
   {
	return 5;
   }
    
}

Let’s go through it. In fact, we can see some things from the decompiled code. The compiler is roughly like this. Take the asynctask resultmethod method as an example:

  1. Mark the method identifying async withAsyncStateMachinecharacteristic
  2. according toAsyncStateMachineWith this feature, the compiler adds a new class for the method with the name of the methodAsyncTaskMethodClassAnd implement the interfaceIAsyncStateMachine, the most important one is itsMoveNextmethod
  3. This method removes the identification async and instantiates the newly added class internallyAsyncTaskMethodClass, useAsyncTaskMethodBuilderThe Create method of creates a state machine object assigned to the build field of this type of object, sets the state state to – 1, that is, the initial state, and then starts the state machine through the build field

In fact, the above is only what the compiler does for async. We can see that the things generated by the compiler through the asyncvoidmethod method are roughly the same as other methods, so what await does for the compiler isMoveNextThe try section in the method is also the inconsistency between the asyncvoidmethod method and other methods:

private TaskAwaiter <>u__1;

try
{
	  TaskAwaiter awaiter;
	  if (num != 0)
	  {
		  awaiter = new TaskCompletionSource().Task.GetAwaiter();
		  if (!awaiter.IsCompleted)
		  {
			  this.<>1__state = 0;
			  this.<>u__1 = awaiter;
			  Program.d__2 d__ = this;
			  this.<>t__builder.AwaitUnsafeOnCompleted, Program.d__2>(ref awaiter, ref d__);
			  return;
		  }
	  }
	  else
	  {
		awaiter = this.<>u__1;
	        this.<>u__1 = default(TaskAwaiter);
		this.<>1__state = -1;
	  }
	  awaiter.GetResult();
}

Let’s look at this. < > t__ Builder.awaitunsafeoncompleted internal:

public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine
{
	try
	{
		AsyncMethodBuilderCore.MoveNextRunner runner = null;
		Action completionAction = this.m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runner);
		if (this.m_coreState.m_stateMachine == null)
		{
			Task task = this.Task;
			this.m_coreState.PostBoxInitialization(stateMachine, runner, task);
		}
		awaiter.UnsafeOnCompleted(completionAction);
	}
	catch (Exception exception)
	{
		AsyncMethodBuilderCore.ThrowAsync(exception, null);
	}
}

Getcompletionaction method internal:

[SecuritySafeCritical]
internal Action GetCompletionAction(Task taskForTracing, ref AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize)
{
	Debugger.NotifyOfCrossThreadDependency();
	ExecutionContext executionContext = ExecutionContext.FastCapture();
	Action action;
	AsyncMethodBuilderCore.MoveNextRunner moveNextRunner;
	if (executionContext != null && executionContext.IsPreAllocatedDefault)
	{
		action = this.m_defaultContextAction;
		if (action != null)
		{
			return action;
		}
		moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext, this.m_stateMachine);
		action = new Action(moveNextRunner.Run);
		if (taskForTracing != null)
		{
			action = (this.m_defaultContextAction = this.OutputAsyncCausalityEvents(taskForTracing, action));
		}
		else
		{
			this.m_defaultContextAction = action;
		}
	}
	else
	{
		moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext, this.m_stateMachine);
		action = new Action(moveNextRunner.Run);
		if (taskForTracing != null)
		{
		    action = this.OutputAsyncCausalityEvents(taskForTracing, action);
		}
	}
	if (this.m_stateMachine == null)
	{
	    runnerToInitialize = moveNextRunner;
	}
	return action;
}

void moveNextRunner.Run()
{
  if (this.m_context != null)
  {
	 try
	 {
		ContextCallback contextCallback = AsyncMethodBuilderCore.MoveNextRunner.s_invokeMoveNext;
		if (contextCallback == null)
		{
		    contextCallback = (AsyncMethodBuilderCore.MoveNextRunner.s_invokeMoveNext = new ContextCallback(AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext));
		}
		ExecutionContext.Run(this.m_context, contextCallback, this.m_stateMachine, true);
		return;
	}
	finally
	{
	     this.m_context.Dispose();
	}
  }
	this.m_stateMachine.MoveNext();
}

As can be seen from the above code, this. < > t__ The builder.awaitunsafeoncompleted does the following internally:

  1. Get the action to be given to await.unsafeoncompleted from the getcompletionaction method
  2. Getcompletionaction first captures the execution context of the current thread with ExecutionContext. Fastcapture(), and then executes the callback method with the execution contextMoveNextThat is to return to the beginning againMoveNextmethod

The general execution flow chart is as follows:

Therefore, we have verified that async await is indeed a syntax sugar. The compiler has done too much behind it, simplifying the way we write asynchronous code. We have also noticed some of these problems:

  • The method id is async. Await is not used inside the method. It is actually a synchronous method, but it will compile things related to async, which will waste some performance
  • In fact, the reason why you can use await task is that the compiler uses some things of await, such as:
    • !awaiter.IsCompleted
    • awaiter.GetResult()
    • awaiter.UnsafeOnCompleted

Indeed, as guessed, the object to be await, such as await task. Yield (), must contain the following conditions:

  • There is a getawaiter method, which is an instance method or an extension method

  • The return value class of getawaiter method must contain the following conditions

    • The inotifycompletion interface is implemented directly or indirectly. Icriticalnotifycompletion also inherits from the icriticalnotifycompletion interface, that is, it implements its unsafeoncompleted or oncompleted methods

    • There is a Boolean attribute iscompleted and get is open

    • There is a getResult method, and the return value is void or tresult

    Therefore, you can customize some classes that can be await. For details on how to customize, please refer to this article by boss Lin Dexi:C await advanced usage

Proper use of async await

In fact, we also buried a colored egg in the thread pool. There are working threads in the thread pool suitable for CPU intensive operations, and I / O completion port threads suitable for I / O intensive operations. In fact, the main venue of async await asynchronous function is I / O intensive. Here, let’s pass a piece of code first

static void Main(string[] args)
{
     ThreadPool.SetMaxThreads(8, 8);// Set the maximum number of worker threads and I / O completion port threads in the thread pool
     Read();
     Console.ReadLine();
}

static void Read()
{
      byte[] buffer;
      byte[] buffer1;

       FileStream fileStream = new FileStream("E:/test1.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 10000, useAsync: true);
       buffer = new byte[fileStream.Length];
       var state = Tuple.Create(buffer, fileStream);

       FileStream fileStream1 = new FileStream("E:/test2.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 10000, useAsync: true);
       buffer1 = new byte[fileStream1.Length];
       var state1 = Tuple.Create(buffer1, fileStream1);

       fileStream.BeginRead(buffer, 0, (int)fileStream.Length, EndReadCallback, state);
       fileStream1.BeginRead(buffer, 0, (int)fileStream1.Length, EndReadCallback, state1);

}

 static void EndReadCallback(IAsyncResult asyncResult)
 {
       Console.WriteLine("Starting EndWriteCallback.");
       Console.WriteLine($"current thread:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
       try
       {
          var state = (Tuple)asyncResult.AsyncState;
          ThreadPool.GetAvailableThreads(out int workerThreads, out int portThreads);
          Console.WriteLine($"AvailableworkerThreads:{workerThreads},AvailableIOThreads:{portThreads}");
          state.Item2.EndRead(asyncResult);
        }
        finally
        {
           Console.WriteLine("Ending EndWriteCallback.");
        }
}

Output results:

Starting EndWriteCallback.
current thread:3,isThreadPool:True
AvailableworkerThreads:8,AvailableIOThreads:7
Ending EndWriteCallback.
Starting EndWriteCallback.
current thread:3,isThreadPool:True
AvailableworkerThreads:8,AvailableIOThreads:7
Ending EndWriteCallback.

We can see that in fact, the two callback methods call the same thread and are the I / O completion port thread of the thread pool. If the parameters of the two instantiated FileStream are changed to useasync: false, the output results are as follows:

Starting EndWriteCallback.
current thread:4,isThreadPool:True
AvailableworkerThreads:6,AvailableIOThreads:8
Ending EndWriteCallback.
Starting EndWriteCallback.
current thread:5,isThreadPool:True
AvailableworkerThreads:7,AvailableIOThreads:8
Ending EndWriteCallback.

We will find that the two working threads of the thread pool are used this time. In fact, this is the difference between synchronous I / O and asynchronous I / O. we can roughly look at the bottom beginread Code:

private unsafe int ReadFileNative(SafeFileHandle handle, byte[] bytes, int offset, int count, NativeOverlapped* overlapped, out int hr)
 {
       if (bytes.Length - offset < count)
       {
            throw new IndexOutOfRangeException(Environment.GetResourceString("IndexOutOfRange_IORaceCondition"));
       }

       if (bytes.Length == 0)
       {
           hr = 0;
           return 0;
       }

       int num = 0;
       int numBytesRead = 0;
       fixed (byte* ptr = bytes)
       {
           num = ((!_isAsync) ? Win32Native.ReadFile(handle, ptr + offset, count, out numBytesRead, IntPtr.Zero) : Win32Native.ReadFile(handle, ptr + offset, count, IntPtr.Zero, overlapped));
       }

       if (num == 0)
       {
           hr = Marshal.GetLastWin32Error();
           if (hr == 109 || hr == 233)
           {
               return -1;
           }

           if (hr == 6)
           {
               _handle.Dispose();
           }

           return -1;
       }
        hr = 0;
        return numBytesRead;
 }

In fact, the bottom layer is Pinvoke calling Win32 API, Win32 native.readfile. For details of the Win32 function, please refer to MSDN:ReadFile, the key to asynchrony is to judge whether the overlapped object is passed in, and the object will be associated with a window kernel object, IOCP (I / O completion port), that is, I / O completion port. In fact, when a process is created, such an I / O completion port kernel object will be created at the same time as the thread pool is created. The general process is as follows:

  • Our two I / O requests actually correspond to the two IRP (I / O request packet) data structures we passed in, including the file handle and the offset in the file. Pinvoke will call the Win32 API to enter the Win32 user mode
  • Then enter the window kernel mode through the Win32 API function, and our two requests will be placed in an IRP queue
  • After that, the system will process different I / O devices according to the file handle, offset and other information from the IRP queue. After completion, it will be put into a completed IRP queue
  • Then, the thread pool I / O completion port thread uses the thread pool I / O completion port object to get those completed IRP queues

In the case of multiple requests, the IOCP model is asynchronous. A small number of I / O completion port threads can do all this. In synchronization, one thread has to wait for the completion of the request processing, which will greatly waste threads. As above, two requests need two worker threads to complete the notification. In the async await period, Some of the above methods have been encapsulated toTaskandTaskObject to represent the completion of reading, so the above can be simplified as:

static async Task Main(string[] args)
{
      ThreadPool.SetMaxThreads(8, 8);// Set the maximum number of worker threads and I / O completion port threads in the thread pool
      await ReadAsync();
      Console.ReadLine();
}

static async Task ReadAsync()
{
      FileStream fileStream = new FileStream("E:/test1.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 10000, useAsync: true);
      var buffer = new byte[fileStream.Length];
      var result = await fileStream.ReadAsync(buffer, 0, (int)fileStream.Length);
      return result;
 }

The bottom layer has not changed, but the I / O completes the port thread during callback and then calls back through the working thread (this can avoid blocking the I / O completes the port thread during previous callback), but it greatly simplifies asynchronous I / O programming. Async await is not suitable for CPU intensive, but I / O operations are generally time-consuming. If you use the working thread of thread pool, It is possible to create more threads to cope with more requests. The CPU intensive task parallel library (TPL) has many suitable APIs

summary

We learned that task is a very convenient high-level abstract class for writing multithreads in. Net. You don’t have to worry about underlying thread processing. You can write high-performance multithreaded concurrent programs through different configurations of task. Then we explored what is done inside the async await asynchronous function introduced in. Net 4.5, and knew that Async await works with TPL, It simplifies the way of writing asynchronous programming, which is especially suitable for I / O-Intensive asynchronous operations. This article only plays a role in a quick understanding of task and async await, and there is much more about Microsoft’s work around task, such asValueTaskOptimizing tasks is also more conducive to CPU intensive operations in TPLParallelAnd plinq API, etc. you can refer to other books or MSDN for more in-depth understanding

reference resources

Asynchronous programming patterns
Async in depth
ThreadPool class
Understanding C# async / await
CLR via c# 4th Edition
Windows core programming Fifth Edition

Recommended Today

On the mutation mechanism of Clickhouse (with source code analysis)

Recently studied a bit of CH code.I found an interesting word, mutation.The word Google has the meaning of mutation, but more relevant articles translate this as “revision”. The previous article analyzed background_ pool_ Size parameter.This parameter is related to the background asynchronous worker pool merge.The asynchronous merge and mutation work in Clickhouse kernel is completed […]