理解Task和和async await

本文將詳解C#類當中的Task,以及異步函數async await和Task的關係web

一.Task的前世此生

1.Thread

一開始咱們須要建立線程的時候通常是經過Thread建立線程,通常經常使用建立線程方式有如下幾種:編程

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

            Thread thread = new Thread(() => TestMethod(2));
            thread.IsBackground = true;//設置爲後臺線程,默認前臺線程
            thread.Start();

            Thread thread1 = new Thread(() => TestMethod1());
            //設置thread1優先級爲最高,系統儘量單位時間內調度該線程,默認爲Normal
            thread1.Priority = ThreadPriority.Highest;
            thread1.Start();

            Thread thread2 = new Thread((state) => TestMethod2(state));
            thread2.Start("data");
            thread2.Join();//等待thread2執行完成
            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}");
        }

輸出結果:c#

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

orwindows

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

因爲個人PC是多核CPU,那麼TestMethod和TestMethod1所在兩個線程是真正並行的,因此有可能輸出結果前後不肯定,雖然TestMethod1所在線程設置優先級爲Highest最高,但可能系統不會優先調度,其實目前不怎麼推薦用Thread.Start去建立線程,缺點大概以下:api

  • 由於在大量須要建立線程狀況下,用Thread.Start去建立線程是會浪費線程資源,由於線程用完就沒了,不具有重複利用能力
  • 如今一個進程中的CLR默認會建立線程池和一些工做線程(不要浪費),且線程池的工做線程用完會回到線程池,可以重複利用,

除非是如下緣由:數組

  • 真的須要操做線程優先級數據結構

  • 須要建立一個前臺線程,因爲相似於控制檯程序當初始前臺線程執行完就會退出進程,那麼建立前臺線程能夠保證進程退出前該前臺線程正常執行成功多線程

    例如在原來的例子註釋掉thread2.Join();,咱們會發現輸出完控制檯初始的前臺線程輸出完end沒退出進程,只有在TestMethod2(該線程凍結2秒最久)執行完才退出併發

    static void Main(string[] args)
            {
                Console.WriteLine("begin");
    
                Thread thread = new Thread(() => TestMethod(2));
                thread.IsBackground = true;//設置爲後臺線程,默認前臺線程
                thread.Start();
    
                Thread thread1 = new Thread(() => TestMethod1());
                //設置thread1優先級爲最高,系統儘量單位時間內調度該線程,默認爲Normal
                thread1.Priority = ThreadPriority.Highest;
                thread1.Start();
    
                Thread thread2 = new Thread((state) => TestMethod2(state));
                thread2.Start("data");
                //thread2.Join();//等待thread2執行完成
                Console.WriteLine("end");
            }

    輸出:app

    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
  • 須要建立一個後臺線程,長時間執行的,其實一個Task的TaskScheduler在Default狀況下,設置TaskCreationOptions.LongRunning內部也是建立了一個後臺線程Thread,而不是在ThreadPool執行,在不須要Task的一些其餘功能狀況下,Thread更輕量

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

2.ThreadPool

一個.NET進程中的CLR在進程初始化時,CLR會開闢一塊內存空間給ThreadPool,默認ThreadPool默認沒有線程,在內部會維護一個任務請求隊列,當這個隊列存在任務時,線程池則會經過開闢工做線程(都是後臺線程)去請求該隊列執行任務,任務執行完畢則回返回線程池,線程池儘量會用返回的工做線程去執行(減小開闢),若是沒返回線程池,則會開闢新的線程去執行,然後執行完畢又返回線程池,大概線程池模型以下:

咱們經過代碼來看:

static void Main(string[] args)
        {
            //獲取默認線程池容許開闢的最大工做線程樹和最大I/O異步線程數
            ThreadPool.GetMaxThreads(out int maxWorkThreadCount, 
                                     out int maxIOThreadCount);
            Console.WriteLine($"maxWorkThreadCount:{maxWorkThreadCount},
                              maxIOThreadCount:{maxIOThreadCount}");
            //獲取默認線程池併發工做線程和I/O異步線程數
            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);//模擬工做線程運行
                });
            }
            Console.ReadLine();
        }

輸出以下:

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

​ 因爲我CPU爲8核16線程,默認線程池給我分配了16條工做線程和I/O線程,保證在該進程下實現真正的並行,能夠看到前16條工做線程的啓動時間是一致的,到最後四條,線程池嘗試去用以前的工做線程去請求那個任務隊列執行任務,因爲前16條還在運行沒返回到線程池,則每相隔一秒,建立新的工做線程去請求執行,並且該開闢的最多線程數是和線程池容許開闢的最大工做線程樹和最大I/O異步線程數有關的

咱們能夠經過ThreadPool.SetMaxThreads 將工做線程數設置最多隻有16,在執行任務前新增幾行代碼:

var success = ThreadPool.SetMaxThreads(16, 16);//只能設置>=最小併發工做線程數和I/O線程數
Console.WriteLine($"SetMaxThreads success:{success}");
ThreadPool.GetMaxThreads(out int maxWorkThreadCountNew, out int maxIOThreadCountNew);
Console.WriteLine($"maxWorkThreadCountNew:{maxWorkThreadCountNew},
                  maxIOThreadCountNew:{maxIOThreadCountNew}");

輸出以下:

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

能夠很清楚知道,因爲線程池最多隻容許開闢16條工做線程和I/O線程,那麼在線程池再開闢了16條線程以後,將不會再開闢新線程,新的任務也只能等前面的工做線程執行完回線程池後,再用返回的線程去執行新任務,致使新任務的開始執行時間會在5秒後

ThreadPool的優勢以下:

  • 默認線程池已經根據自身CPU狀況作了配置,在須要複雜多任務並行時,智能在時間和空間上作到均衡,在CPU密集型操做有必定優點,而不是像Thread.Start那樣,須要本身去判斷和考慮
  • 一樣能夠經過線程池一些方法,例如ThreadPool.SetMaxThreads手動配置線程池狀況,很方便去模擬不一樣電腦硬件的執行狀況
  • 有專門的I/O線程,可以實現非阻塞的I/O,I/O密集型操做有優點(後續Task會提到)

但一樣,缺點也很明顯:

  • ThreadPool原生不支持對工做線程取消、完成、失敗通知等交互性操做,一樣不支持獲取函數返回值,靈活度不夠,Thread原生有Abort (一樣不推薦)、Join等可選擇
  • 不適合LongTask,由於這類會形成線程池多建立線程(上述代碼可知道),這時候能夠單獨去用Thread去執行LongTask

3.Task

在.NET 4.0時候,引入了任務並行庫,也就是所謂的TPL(Task Parallel Library),帶來了Task類和支持返回值的Task<TResult> ,同時在4.5完善優化了使用,Task解決了上述Thread和ThreadPool的一些問題,Task到底是個啥,咱們來看下代碼:

如下是一個WPF的應用程序,在Button的Click事件:

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);//模擬耗時操做
         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);//模擬耗時操做
             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);//模擬耗時操做
            Debug.WriteLine($"task3 work on thread:{threadId},isBackgound:{isBackgound},isThreadPool:{isThreadPool}");
          }, TaskCreationOptions.LongRunning);
    }

輸出:

main thread id :1
//因爲是並行,輸出結果的先後順序可能每次都不同
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

我用三種不一樣的Task開闢運行任務的方式,能夠看到,Task運行在三種不一樣的線程:

  • task1是運行在線程池上,是沒進行任何對Task的設置
  • task2經過設置TaskSchedulerTaskScheduler.FromCurrentSynchronizationContext()是沒有開闢線程,利用主線程運行
  • task3經過設置TaskCreationOptionsLongRunning和默認TaskScheduler狀況下,實際是開闢了一個後臺Thread去運行

所以,其實Task不必定表明開闢了新線程,可爲在線程池上運行,又或是開闢一個後臺Thread,又或者沒有開闢線程,經過主線程運行任務,這裏提一句TaskScheduler.FromCurrentSynchronizationContext(),假設在控制檯或者ASP.NET Core程序運行,會發生報錯,緣由是主線程的SynchronizationContext爲空,可經過TaskScheduler源碼得知:

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

大體對於Task在經過TaskScheduler和TaskCreationOptions設置後對於將任務分配在不一樣的線程狀況,以下圖:

原生支持延續、取消、異常(失敗通知)

1.延續

Task其實有兩種延續任務的方式,一種經過ContinueWith方法,這是Task在.NET Framework4.0就支持的,一種則是經過GetAwaiter方法,則是在.NET Framework4.5開始支持,並且該方法也是async await異步函數所用到

控制檯代碼:

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

//等價於
     
     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();
}

輸出結果:

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

//事實上,運行的代碼線程,可能和延續的線程有可能不是同一線程,取決於線程池自己的調度
能夠手動設置TaskContinuationOptions.ExecuteSynchronously(同一線程)
或者 TaskContinuationOptions.RunContinuationsAsynchronously(不一樣線程)
默認RunContinuationsAsynchronously優先級大於ExecuteSynchronously

但有意思的是,一樣的代碼,在WPF/WinForm等程序,運行的輸出是不同的:

WPF程序代碼:

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

輸出:

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

緣由就是GetAwaiter().OnCompleted()會去檢測有沒有SynchronizationContext,所以其實就是至關於如下代碼:

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

若是在WPF程序中要得到控制檯那樣效果,只須要修改成ConfigureAwait(false),延續任務不在SynchronizationContext便可,以下:

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.取消

在.NET Framework4.0帶來Task的同時,一樣帶來了與取消任務有關的類CancellationTokenSourceCancellationToken,下面咱們將大體演示下其用法

WPF程序代碼以下:

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("取消了該操做");
                   return;
               }
           }
        }, cancellationToken);
}

效果以下:

其實上述代碼,也能夠適用於Thread和ThreadPool,等價於以下代碼:

//當TaskCreationOptions爲LongRunning和默認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("取消了該操做");
             return;
         }
   }
}).Start();

//默認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("取消了該操做");
               return;
           }
      }
});

所以,.NET Framework4.0後ThreadThreadPool也一樣可以經過CancellationTokenSourceCancellationToken類支持取消功能,只是通常這二者均可以用Task經過設置,底層一樣調用的ThreadThreadPool,因此通常沒怎麼這麼使用,並且關於Task的基本不少方法都默認支持了,例如,Task.Wait、Task.WaitAll、Task.WaitAny、Task.WhenAll、Task.WhenAny、Task.Delay等等

3.異常(失敗通知)

下面控制檯代碼:

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

輸出以下:

嘗試除以零。
索引超出了數組界限。
未將對象引用設置到對象的實例。

這裏面parent任務有三個子任務,三個並行子任務分別都拋出不一樣異常,返回到parent任務中,而當你對parent任務Wait或者獲取其Result屬性時,那麼將會拋出異常,而使用AggregateException則能將所有異常放在其InnerExceptions異常列表中,咱們則能夠分別對不一樣異常進行處理,這在多任務並行時候是很是好用的,並且AggregateException的功能異常強大,遠遠不止上面的功能,可是若是你只是單任務,使用AggregateException比普通則其實會有浪費性能,也能夠這樣作;

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

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

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

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

輸出:

未將對象引用設置到對象的實例。

二.異步函數async await

async await是C#5.0,也就是.NET Framework 4.5時期推出的C#語法,經過與.NET Framework 4.0時引入的任務並行庫,也就是所謂的TPL(Task Parallel Library)構成了新的異步編程模型,也就是TAP(Task-based asynchronous pattern),基於任務的異步模式

語法糖async await

咱們先來寫下代碼,看看async await的用法:

下面是個控制檯的代碼:

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

輸出結果:

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

換成在WPF/WinForm程序執行,結果以下:

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

是否是感受似曾相識?上面埋下的彩蛋在這裏揭曉了,在講Task的延續的時候咱們講到.NET Framework4.5的一種經過GetAwaiter延續方法,事實上,async await就是上面的一種語法糖,編譯的時候大體會編譯成那樣,因此咱們通常不手動寫GetAwaiter的延續方法,而是經過async await,大大簡化了編程方式,說它是語法糖,那麼有啥證據呢?

咱們再寫一些代碼來驗證:

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

輸出:

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

在這其中,其實async在方法名的時候,只容許,返回值爲void,Task,Task ,不然會發生編譯報錯,事實上,這和其編譯後的結果有關,咱們經過ILSpy反編譯這段代碼,截圖關鍵代碼:

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

咱們大體來捋一捋,事實上,從反編譯後的代碼能夠看出來一些東西了,編譯器大體是這樣的,以AsyncTaskResultMethod方法爲例子:

  1. 將標識async的方法,打上AsyncStateMachine 特性
  2. 根據AsyncStateMachine 該特性,編譯器爲該方法新增一個以該方法名爲名的類AsyncTaskMethodClass,而且實現接口IAsyncStateMachine,其中最主要的就是其MoveNext方法
  3. 該方法去除標識async,在內部實例化新增的類AsyncTaskMethodClass,用AsyncTaskMethodBuilder 的Create方法建立一個狀態機對象賦值給對象的該類型的build字段,而且將狀態state設置爲-1.即初始狀態,而後經過build字段啓動狀態機

實際上,上述只是編譯器爲async作的事情,咱們能夠看到經過AsyncVoidMethod方法編譯器生成的東西和其餘方法大體同樣,那麼await爲編譯器作的就是MoveNext方法裏面try那段,這也是AsyncVoidMethod方法和其餘方法不一致的地方:

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

咱們再看看this.<>t__builder.AwaitUnsafeOnCompleted內部:

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方法內部:

[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();
}

從上面的代碼能夠看出,其實this.<>t__builder.AwaitUnsafeOnCompleted內部就作了如下:

  1. 從GetCompletionAction方法獲取要給awaiter.UnsafeOnCompleted的action
  2. GetCompletionAction內部先用ExecutionContext.FastCapture()捕獲了當前線程的執行上下文,在用執行上下文執行了那個回調方法MoveNext,也就是又一次回到那個一開始那個MoveNext方法

大體執行流程圖以下:

所以,咱們驗證了async await確實是語法糖,編譯器爲其在背後作了太多的事情,簡化了咱們編寫異步代碼的方式,咱們也注意到了其中一些問題:

  • 方法標識async,方法內部沒使用await實際就是同步方法,可是會編譯出async有關的東西,會浪費一些性能
  • 能await Task,事實上能await Task是由於後面編譯器有用到了awaiter的一些東西,例如:
    • !awaiter.IsCompleted
    • awaiter.GetResult()
    • awaiter.UnsafeOnCompleted

確實如猜測的,像await Task.Yield()等等,被await的對象,它必須包含如下條件:

  • 有一個GetAwaiter方法,爲實例方法或者擴展方法

  • GetAwaiter方法的返回值類,必須包含如下條件

    • 直接或者間接實現INotifyCompletion接口,ICriticalNotifyCompletion也繼承自ICriticalNotifyCompletion接口,也就是實現了其UnsafeOnCompleted或者OnCompleted方法

    • 有個布爾屬性IsCompleted,且get開放

    • 有個GetResult方法,返回值爲void或者TResult

    所以能夠自定義一些能被await的類,關於如何自定義的細節,能夠參考林德熙大佬的這篇文章:C# await 高級用法

async await的正確用途

事實上,咱們在線程池上還埋下一個彩蛋,線程池上有工做線程適合CPU密集型操做,還有I/O完成端口線程適合I/O密集型操做,而async await異步函數實際上的主場是在I/O密集型這裏,咱們先經過一段代碼

static void Main(string[] args)
{
     ThreadPool.SetMaxThreads(8, 8);//設置線程池最大工做線程和I/O完成端口線程數量
     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.");
        }
}

輸出結果:

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.

咱們看到,事實上,兩個回調方法都調用了相同的線程,且是線程池的I/O完成端口線程,假如將兩個實例化FileStream時的參數改下,改成useAsync: false,輸出結果以下:

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.

咱們會發現此次用到的是線程池的兩條工做線程了,其實這就是同步I/O和異步I/O的區別,咱們能夠大概看下最底層BeginRead代碼:

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

實際上底層是Pinvoke去調用win32api ,Win32Native.ReadFile,關於該win32函數細節可參考MSDN:ReadFile,是否異步的關鍵就是判斷是否傳入overlapped對象,而該對象會關聯到一個window內核對象,IOCP(I/O Completion Port),也就是I/O完成端口,事實上進程建立的時候,建立線程池的同時就會建立這麼一個I/O完成端口內核對象,大體流程以下:

  • 咱們兩個I/O請求,事實上對應着咱們傳入的兩個IRP(I/O request packet)數據結構,其中包括文件句柄和文件中偏移量,會在Pinvoke去調用win32api進入win32用戶模式
  • 而後經過win32api函數進入window內核模式,咱們兩個請求以後會放在一個IRP隊列
  • 以後系統就會從該IRP隊列,根據文件句柄和偏移量等信息去對應請求處理不一樣的I/O設備,完成後會放入到一個完成IRP隊列中
  • 而後線程池的I/O完成端口線程經過線程池的I/O完成端口對象去拿取那些已經完成IRP隊列

那麼在多請求的時候,IOCP模型異步的這種狀況,少許的I/O完成端口線程就能作到這一切,而同步則要由於一條線程要等待該請求處理的完成,那麼會大大浪費線程,正如上面同樣,兩個請求卻要兩個工做線程完成通知,而在async await時期,上面的一些方法已經被封裝以TaskTask<TResult> 對象來表明完成讀取了,那麼上面能夠簡化爲:

static async Task Main(string[] args)
{
      ThreadPool.SetMaxThreads(8, 8);//設置線程池最大工做線程和I/O完成端口線程數量
      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;
 }

底層沒變,只是回調的時候I/O完成端口線程再經過工做線程進行回調(這能避免以前回調的時候阻塞I/O完成端口線程的操做),可是大大的簡化了異步I/O編程,而async await並不是不適合CPU密集型,只是I/O操做通常比較耗時,若是用線程池的工做線程,就會有可能建立更多線程來應付更多的請求,CPU密集型的任務並行庫 (TPL)有不少合適的api

總結

咱們瞭解了Task是.NET 編寫多線程的一個很是方便的高層抽象類,你能夠不用擔憂底層線程處理,經過對Task不一樣的配置,能寫出較高性能的多線程併發程序,而後探尋了.NET 4.5引入了的async await異步函數內部作了些啥,知道async await經過和TPL的配合,簡化了編寫異步編程的方式,特別適合I/O密集型的異步操做,本文只是起到對於Task和async await有個快速的理解做用,而關於微軟圍繞Task作的事情遠遠不止如此,例如經過ValueTask優化Task,還有更利於CPU密集型操做的TPL中的Parallel和PLINQ api等等,能夠參考其餘書籍或者msdn更深刻了解

參考

Asynchronous programming patterns
Async in depth
ThreadPool 類
Understanding C# async / await 《CLR Via C# 第四版》 《Window核心編程第五版》

相關文章
相關標籤/搜索