当前位置:网站首页>Understanding task and async await

Understanding task and async await

2020-11-09 10:49:32 RyzenAdorer.

This article will explain in detail C# Class Task, And asynchronous functions async await and Task The relationship between

One .Task The past and this life

1.Thread

In the beginning, we usually need to create threads through Thread Create thread , Generally, there are several ways to create threads :

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

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

            Thread thread1 = new Thread(() => TestMethod1());
            // Set up thread1 The priority is highest , The system schedules the thread as much as possible per unit time , 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 completed 
            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

Because of my PC It's multicore CPU, that TestMethod and TestMethod1 The two threads are really parallel , So it is possible that the output sequence is uncertain , although TestMethod1 Set the priority of the thread to Highest The highest , But maybe the system will not schedule first , In fact, it is not recommended at present Thread.Start To create a thread , The disadvantages are as follows :

  • Because in the case of a large number of threads to be created , use Thread.Start Creating threads is a waste of thread resources , Because the thread runs out, it's gone , Not able to reuse
  • Now a process of CLR By default, thread pools and some worker threads are created ( Don't waste it ), And the thread pool will return to the thread pool when it is used up , Be able to reuse ,

Except for the following reasons :

  • You really need to manipulate thread priority

  • You need to create a foreground thread , Because it is similar to the console program, when the initial foreground thread finishes executing, it will exit the process , Then creating a foreground thread can ensure that the foreground thread can execute successfully before the process exits

    For example, comment out the original example thread2.Join();, We will find that the initial foreground thread of the console has finished outputting end No exit from the process , Only in TestMethod2( The thread freezes 2 The second is the longest ) Exit after execution

            static void Main(string[] args)
            {
                Console.WriteLine("begin");
    
                Thread thread = new Thread(() => TestMethod(2));
                thread.IsBackground = true;// Set to background thread , Default foreground thread 
                thread.Start();
    
                Thread thread1 = new Thread(() => TestMethod1());
                // Set up thread1 The priority is highest , The system schedules the thread as much as possible per unit time , 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 completed 
                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
    
  • You need to create a background thread , For a long time , Actually, one. Task Of TaskScheduler stay Default Under the circumstances , Set up TaskCreationOptions.LongRunning Internally, a background thread is created Thread, Not in ThreadPool perform , No need Task Some other features of ,Thread A lightweight

      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

One .NET In process CLR At process initialization ,CLR Will open up a memory space for ThreadPool, Default ThreadPool There are no threads by default , Internally, a task request queue is maintained , When there are tasks in this queue , The thread pool will open up the worker thread ( These are all background threads ) To request the queue to execute the task , When the task is finished, it will return to the thread pool , The thread pool uses the returned worker thread to execute as much as possible ( Reduce opening up ), If the thread pool is not returned , New threads will be created to execute , After the execution, it returns to the thread pool , The thread pool model is as follows :

Let's look at it in code :

        static void Main(string[] args)
        {
            // Gets the maximum worker thread tree and the maximum number of worker threads allowed to be opened by the default thread pool I/O Number of asynchronous threads 
            ThreadPool.GetMaxThreads(out int maxWorkThreadCount, 
                                     out int maxIOThreadCount);
            Console.WriteLine($"maxWorkThreadCount:{maxWorkThreadCount},
                              maxIOThreadCount:{maxIOThreadCount}");
            // Gets the default thread pool concurrent worker threads and I/O Number of asynchronous threads 
            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 a worker thread to run 
                });
            }
            Console.ReadLine();
        }

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

​ Because of me CPU by 8 nucleus 16 Threads , The default thread pool is assigned to me 16 Worker threads and I/O Threads , Ensure true parallelism in this process , You can see before 16 The starting time of worker threads is consistent , To the last four , The thread pool attempts to use the previous worker thread to request the task queue to execute the task , Because before 16 The thread pool is still running and has not returned to the thread pool , Every second , Create a new worker thread to request execution , And the maximum number of threads to be opened is the maximum number of worker thread trees and the maximum number of threads allowed by thread pool I/O The number of asynchronous threads

We can go through ThreadPool.SetMaxThreads Set the maximum number of worker threads to only 16, Add a few lines of code before executing the task :

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

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 is only allowed to open up at most 16 Worker threads and I/O Threads , So the online pool has been opened up again 16 After threads , There will be no new threads , The new task can only wait for the previous worker thread to execute the thread pool , Use the returned thread to perform the new task , Cause the start time of the new task will be 5 Seconds later

ThreadPool The advantages are as follows :

  • The default thread pool is already based on itself CPU The situation is configured , When complex multitask parallelism is required , Intelligence balances time and space , stay CPU Intensive operations have some advantages , Not like it Thread.Start like that , You need to judge and think about it yourself
  • You can also use thread pools in some ways , for example ThreadPool.SetMaxThreads Manually configure the thread pool , It's very convenient to simulate the execution of different computer hardware
  • Specialized I/O Threads , Can achieve non blocking I/O,I/O Intensive operations have advantages ( follow-up Task Will mention )

But the same , The disadvantages are obvious :

  • ThreadPool Native does not support canceling worker threads 、 complete 、 Interactive operations such as failure notification , It is also not supported to get the function return value , Not enough flexibility ,Thread Primitive Abort ( It's also not recommended )、Join Wait for a choice
  • Not suitable for LongTask, Because this kind of thread pool will create more threads ( The above code shows that ), You can use it alone at this time Thread To carry out LongTask

3.Task

stay .NET 4.0 When , The task parallel library is introduced , It's called TPL(Task Parallel Library), brought Task Class and support return values Task<TResult> , At the same time 4.5 Perfect and optimize the use of ,Task Solve the above problem Thread and ThreadPool Some of the problems ,Task What the hell is it , Let's look at the code :

Here is one WPF Applications for , stay Button Of Click event :

 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);// Simulation time operation 
         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);// Simulation time operation 
             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);// Simulation time operation 
            Debug.WriteLine($"task3 work on thread:{threadId},isBackgound:{isBackgound},isThreadPool:{isThreadPool}");
          }, TaskCreationOptions.LongRunning);
    }

Output :

main thread id :1
// Because it's parallel , The order of the output may be different each 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 Task Open up ways to run tasks , You can see ,Task Run on three different threads :

  • task1 It's running on the thread pool , It's not doing anything about Task Set up
  • task2 By setting TaskScheduler by TaskScheduler.FromCurrentSynchronizationContext() It's not opening up threads , Using the main thread to run
  • task3 By setting TaskCreationOptions by LongRunning And default TaskScheduler Under the circumstances , Actually, it opened up a backstage Thread To run

therefore , Actually Task It doesn't necessarily mean that new threads have been opened , Can be run on the thread pool , Or open up a backstage Thread, Or not opening up threads , Run the task through the main thread , Here's a word TaskScheduler.FromCurrentSynchronizationContext(), Suppose you're on the console or ASP.NET Core The program runs , There will be an error , The reason is that the main thread SynchronizationContext It's empty , It can be done by TaskScheduler Source learned :

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

Generally speaking, for Task Through TaskScheduler and TaskCreationOptions After setting, the task is assigned to different threads , Here's the picture :

Native support for continuation 、 Cancel 、 abnormal ( Failure notification )

1. continue

Task There are actually two ways to continue the mission , A through ContinueWith Method , This is a Task stay .NET Framework4.0 I support it , One is through GetAwaiter Method , It is in .NET Framework4.5 Start supporting , And the method is also async await Asynchronous functions use

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 , Running code threads , It is possible that the thread being continued may not be the same thread , Depends on the scheduling of the thread pool itself 
 You can set it manually TaskContinuationOptions.ExecuteSynchronously( The same thread )
 perhaps  TaskContinuationOptions.RunContinuationsAsynchronously( Different threads )
 Default RunContinuationsAsynchronously Priority is greater than ExecuteSynchronously

But what's interesting is , Same code , stay WPF/WinForm Applications such as , The output of running is different :

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 is that GetAwaiter().OnCompleted() It's going to check if there's SynchronizationContext, So it's 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 in WPF To get the effect of the console in the program , Just change it to ConfigureAwait(false), The continuation mission is not in SynchronizationContext that will do , 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. Cancel

stay .NET Framework4.0 bring Task At the same time , It also brings classes related to canceling tasks CancellationTokenSource and CancellationToken, Now we'll give a general demonstration of its usage

WPF The 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 cancelled ");
                   return;
               }
           }
        }, cancellationToken);
}


The effect is as follows :

In fact, the above code , It can also be applied to Thread and ThreadPool, This is equivalent to the following code :

// When TaskCreationOptions by LongRunning And default TaskScheduler Under the circumstances 
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 cancelled ");
             return;
         }
   }
}).Start();

// Default TaskScheduler Under the circumstances 
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 cancelled ");
               return;
           }
      }
});

therefore ,.NET Framework4.0 after Thread and ThreadPool It can also be done through CancellationTokenSource and CancellationToken Class supports the cancel function , It's just that in general, both can be used Task By setting , The underlying layer also calls Thread and ThreadPool, So it's not usually used in this way , And about the Task Many of the basic methods are supported by default , for example ,Task.Wait、Task.WaitAll、Task.WaitAny、Task.WhenAll、Task.WhenAny、Task.Delay wait

3. abnormal ( 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();
   }

Output is as follows :

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

Inside this parent The task has three subtasks , Each of the three parallel subtasks throws different exceptions , Back to parent Tasks , And when you're right about parent Mission Wait Or get it Result Attribute , Then an exception will be thrown , While using AggregateException You can put all the exceptions in its InnerExceptions In the exception list , We can handle different exceptions separately , This is very useful when multitasking is in parallel , and AggregateException It's very powerful , It's much more than that , But if you're just single tasking , Use AggregateException It's more wasteful than normal , You can do the same ;

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 by 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 .

Two . An asynchronous function async await

async await yes C#5.0, That is to say .NET Framework 4.5 Period launched C# grammar , Through and with .NET Framework 4.0 Task parallel library is introduced when , It's called TPL(Task Parallel Library) It forms a new asynchronous programming model , That is to say TAP(Task-based asynchronous pattern), Task-based asynchronous mode

Grammatical sugar async await

Let's write the code first , have a look async await Usage of :

Here's the 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

Change to WPF/WinForm Program execution , give the result as follows :

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

Do you feel familiar with me ? The eggs buried above are revealed here , Talking about Task We talked about .NET Framework4.5 A way of passing through GetAwaiter Continuation method , in fact ,async await It's a kind of grammar sugar above , When it's compiled, it's like that , So we don't usually write by hand GetAwaiter The continuation method of , But through async await, Greatly simplified the way of programming , Say it's grammar sugar , So what's 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<int> AsyncTaskResultMethod()
    {
       return await Task.FromResult(5);
    }

    public static async Task AsyncTaskMethod()
    {
       await new TaskCompletionSource<int>().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 the Middle East: , Actually async When it comes to method names , Only , The return value is void,Task,Task , Otherwise, compilation error will occur , in fact , This has something to do with the result of its compilation , We go through ILSpy Decompile this code , Screenshot key code :

internal class Program
{
  [CompilerGenerated]
  private sealed class <AsyncTaskResultMethod>d__1 : IAsyncStateMachine
  {
	  public int <>1__state;
	  public AsyncTaskMethodBuilder<int> <>t__builder;
	  private int <>s__1;
	  private TaskAwaiter<int> <>u__1;
	  void IAsyncStateMachine.MoveNext()
	  {
		  int num = this.<>1__state;
		  int result;
		  try
		  {
			 TaskAwaiter<int> awaiter;
			 if (num != 0)
			 {
				awaiter = Task.FromResult<int>(5).GetAwaiter();
				if (!awaiter.IsCompleted)
				{
					this.<>1__state = 0; 
					this.<>u__1 = awaiter;
				    Program.<AsyncTaskResultMethod>d__1 <AsyncTaskResultMethod>d__ = this;
					this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<AsyncTaskResultMethod>d__1>(ref awaiter, ref <AsyncTaskResultMethod>d__);
					return;
				}
		         }
		         else
		         {
		                awaiter = this.<>u__1;
				this.<>u__1 = default(TaskAwaiter<int>);
				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 <AsyncTaskMethod>d__2 : IAsyncStateMachine
  {
	  public int <>1__state;
	  public AsyncTaskMethodBuilder <>t__builder;
	  private TaskAwaiter<int> <>u__1;
	  void IAsyncStateMachine.MoveNext()
	  {
		   int num = this.<>1__state;
		   try
		   {
				TaskAwaiter<int> awaiter;
				if (num != 0)
				{
					awaiter = new TaskCompletionSource<int>().Task.GetAwaiter();
					if (!awaiter.IsCompleted)
					{
						this.<>1__state = 0;
						this.<>u__1 = awaiter;
						Program.<AsyncTaskMethod>d__2 <AsyncTaskMethod>d__ = this;
						this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<AsyncTaskMethod>d__2>(ref awaiter, ref <AsyncTaskMethod>d__);
						return;
					}
				}
				else
				{
					awaiter = this.<>u__1;
					this.<>u__1 = default(TaskAwaiter<int>);
					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 <AsyncVoidMethod>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.<AsyncTaskResultMethod>d__1))]
   public static Task<int> AsyncTaskResultMethod()
   {
	   Program.<AsyncTaskResultMethod>d__1 <AsyncTaskResultMethod>d__ = new Program.<AsyncTaskResultMethod>d__1();
	  <AsyncTaskResultMethod>d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
	  <AsyncTaskResultMethod>d__.<>1__state = -1;
	  <AsyncTaskResultMethod>d__.<>t__builder.Start<Program.<AsyncTaskResultMethod>d__1>(ref <AsyncTaskResultMethod>d__);
	  return <AsyncTaskResultMethod>d__.<>t__builder.Task;
	}
    
  [DebuggerStepThrough, AsyncStateMachine(typeof(Program.<AsyncTaskMethod>d__2))]
   public static Task AsyncTaskMethod()
   {
		Program.<AsyncTaskMethod>d__2 <AsyncTaskMethod>d__ = new Program.<AsyncTaskMethod>d__2();
		<AsyncTaskMethod>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
		<AsyncTaskMethod>d__.<>1__state = -1;
		<AsyncTaskMethod>d__.<>t__builder.Start<Program.<AsyncTaskMethod>d__2>(ref <AsyncTaskMethod>d__);
		return <AsyncTaskMethod>d__.<>t__builder.Task;
   }

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

Let's sort it out roughly , in fact , From the decompiled code, you can see something , The compiler looks like this , With AsyncTaskResultMethod Method as an example :

  1. Mark async Methods , In the play AsyncStateMachine characteristic
  2. according to AsyncStateMachine This feature , The compiler adds a class named after the method to the method AsyncTaskMethodClass, And implement the interface IAsyncStateMachine, One of the most important is its MoveNext Method
  3. This method removes the label async, Instantiate the new class internally AsyncTaskMethodClass, use AsyncTaskMethodBuilder Of Create Method creates a state machine object assigned to the object of that type build Field , And will state state Set to -1. The initial state , And then through build Field start state machine

actually , The above is just a compiler for async Do the things , We can see through AsyncVoidMethod The method compiler generates the same thing as any other method , that await What you do for the compiler is MoveNext Method inside try Period , This is also AsyncVoidMethod Where the method is inconsistent with other methods :

private TaskAwaiter<int> <>u__1;

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

Let's see this.<>t__builder.AwaitUnsafeOnCompleted Inside :

public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(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<TResult> task = this.Task;
			this.m_coreState.PostBoxInitialization(stateMachine, runner, task);
		}
		awaiter.UnsafeOnCompleted(completionAction);
	}
	catch (Exception exception)
	{
		AsyncMethodBuilderCore.ThrowAsync(exception, null);
	}
}

GetCompletionAction Methods the 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 you can see from the code above , Actually this.<>t__builder.AwaitUnsafeOnCompleted The interior has done the following :

  1. from GetCompletionAction Method to get to awaiter.UnsafeOnCompleted Of action
  2. GetCompletionAction Internal first use ExecutionContext.FastCapture() Capture the execution context of the current thread , After executing the callback method with the execution context MoveNext, That is to go back to the beginning again MoveNext Method

The flow chart is as follows :

therefore , We verified async await It's really grammar sugar , The compiler does too much behind it , Simplifies the way we write asynchronous code , We also noticed some of the problems :

  • Method identification async, The method is not used internally await It's actually a synchronous method , But it will compile async Something about it , It's going to waste some performance
  • can await Task, In fact, it can await Task It's because the compiler is useful awaiter Something about , for example :
    • !awaiter.IsCompleted
    • awaiter.GetResult()
    • awaiter.UnsafeOnCompleted

It's true that , image await Task.Yield() wait , By await The object of , It must contain the following conditions :

  • There is one GetAwaiter Method , For instance method or extension method

  • GetAwaiter Method's return value class , The following conditions must be included

    • Directly or indirectly INotifyCompletion Interface ,ICriticalNotifyCompletion Also inherit from ICriticalNotifyCompletion Interface , That is to say, it has realized its UnsafeOnCompleted perhaps OnCompleted Method

    • There's a Boolean property IsCompleted, And get to open up

    • There is one GetResult Method , The return value is void perhaps TResult

    So you can customize something that can be await Class , Details on how to customize , You can refer to this article of Lin Dexi :C# await Advanced usage

async await The right use of

in fact , We also buried a colored egg on the pool , There are worker threads on the thread pool for CPU Intensive operation , also I/O The completion port thread is suitable for I/O Intensive operation , and async await Asynchronous functions are actually home to I/O Dense here , Let's go through a piece of code first

static void Main(string[] args)
{
     ThreadPool.SetMaxThreads(8, 8);// Set the thread pool maximum worker thread and I/O Number of completed port threads 
     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<byte[], FileStream>)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 see , in fact , Both callback methods call the same thread , And it's a thread pool I/O Complete port thread , If you instantiate two FileStream Change the parameter of , Change it to useAsync: false, The output is 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 this time we are using two worker threads in the thread pool , In fact, this is synchronization I/O And asynchronous I/O The difference between , We can take a 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;
 }

Actually, the bottom is Pinvoke To call win32api ,Win32Native.ReadFile, About that win32 Function details can be referred to MSDN:ReadFile, The key to asynchronism is to determine whether it is passed in overlapped object , And the object is associated with a window Kernel object ,IOCP(I/O Completion Port), That is to say I/O Completion port , In fact, when a process is created , When you create a thread pool, you create this I/O Complete the port kernel object , The general flow is as follows :

  • We two I/O request , In fact, it corresponds to the two that we introduced IRP(I/O request packet) data structure , This includes the file handle and the offset in the file , Will be in Pinvoke To call win32api Get into win32 User mode
  • And then through win32api Function into window Kernel mode , After two requests, we'll put one IRP queue
  • Then the system will start from IRP queue , According to the file handle and offset information to deal with different requests I/O equipment , After completion, it will be put into a completed IRP In line
  • And then the thread pool I/O Complete the port thread through the thread pool I/O Finish the port object to get those that have been completed IRP queue

So when there are many requests ,IOCP In this case, the model is asynchronous , A small amount of I/O Completing the port thread can do all this , Synchronization is because a thread has to wait for the request to be processed , That would be a big waste of threads , Just like the above , Two requests require two worker threads to complete the notification , And in the async await period , Some of the above methods have been encapsulated in Task and Task<TResult> Object to represent finished reading , So the above can be simplified as :

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

static async Task<int> 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 doesn't change , It's just a callback I/O Finish the port thread and then call back through the worker thread ( This can avoid blocking the previous callback I/O Complete the port thread operation ), But it greatly simplifies asynchronous I/O Programming , and async await It's not inappropriate CPU intensive , It's just I/O The operation is generally time-consuming , If you use the worker thread of the thread pool , It's possible to create more threads to handle more requests ,CPU Intensive task parallel library (TPL) There are many suitable api

summary

We get it Task yes .NET Write multithreading a very convenient high-level abstract class , You don't have to worry about the underlying threading , Through to Task Different configurations , Can write high performance multithreaded concurrent programs , And then I explored .NET 4.5 Introduced async await What's done inside the asynchronous function , know async await Through the and TPL With , Simplify the way to write asynchronous programming , Perfect for I/O Intensive asynchronous operations , This article is only for the purpose of Task and async await There's a quick understanding , And about Microsoft around Task It's far more than that , For example, through ValueTask Optimize Task, And it's better for CPU Intensive operation TPL Medium Parallel and PLINQ api wait , You can refer to other books or msdn Learn more about

Reference resources

Asynchronous programming patterns
Async in depth
ThreadPool class
Understanding C# async / await
《CLR Via C# The Fourth Edition 》
《Window The fifth edition of core programming 》

版权声明
本文为[RyzenAdorer.]所创,转载请带上原文链接,感谢