1. 程式人生 > >執行緒篇---Task(任務)和執行緒池不得不說的祕密

執行緒篇---Task(任務)和執行緒池不得不說的祕密

整理自部落格園一大佬的文章 :https://www.cnblogs.com/tuyile006/p/7154924.html
和碼農之家一匿名大佬 https://www.e-learn.cn/content/net/1114080
和部落格園大佬 https://www.cnblogs.com/wangchuang/p/5737188.html
PS:以上排名不分先後 哈哈哈

一、先介紹下Task

對於多執行緒,我們經常使用的是Thread。在我們瞭解Task之前,如果我們要使用多核的功能可能就會自己來開執行緒,然而這種執行緒模型在.net 4.0之後被一種稱為基於“任務的程式設計模型”所衝擊,因為task會比thread具有更小的效能開銷,不過大家肯定會有疑惑,任務和執行緒到底有什麼區別呢?

任務和執行緒的區別:
1、任務是架構線上程之上的,也就是說任務最終還是要拋給執行緒去執行。

2、任務跟執行緒不是一對一的關係,比如開10個任務並不是說會開10個執行緒,這一點任務有點類似執行緒池,但是任務相比執行緒池有很小的開銷和精確的控制。
3、Task的優勢
  ThreadPool相比Thread來說具備了很多優勢,但是ThreadPool卻又存在一些使用上的不方便。比如:
  ◆ ThreadPool不支援執行緒的取消、完成、失敗通知等互動性操作;
  ◆ ThreadPool不支援執行緒執行的先後次序;
  以往,如果開發者要實現上述功能,需要完成很多額外的工作,現在,微軟提供了一個功能更強大的概念:Task。Task線上程池的基礎上進行了優化,並提供了更多的API。在Framework 4.0中,如果我們要編寫多執行緒程式,Task顯然已經優於傳統的方式。
  以下是一個簡單的任務示例:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task t = new Task(() =>
            {
                Console.WriteLine("任務開始工作……");
                //模擬工作過程
                Thread.Sleep(5000);
            });
            t.Start();
            t.ContinueWith((task) =>
            {
                Console.WriteLine("任務完成,完成時候的狀態為:");
                Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
            });
            Console.ReadKey();
        }
    }
}

二、建立Task

建立Task的方法有兩種,一種是直接建立——new一個出來,一種是通過工廠建立。下面來看一下這兩種建立方法:

    //第一種建立方式,直接例項化
    Task task1 = new Task(() =>
     {
        //To Do you code  也可以在這直接呼叫方法,直接傳遞引數,也比較方便
     });

這是最簡單的建立方法,可以看到其建構函式是一個Action,其建構函式有如下幾種,比較常用的是前兩種。

/第二種建立方式,工廠建立
         var task2 = Task.Factory.StartNew(() =>
         {
            //TODO you code
         });
這種方式通過靜態工廠,建立一個Task並執行。

建構函式建立的task,必須手動Start,而通過工廠建立的Task直接就啟動了。

下面我們來看一下Task的宣告週期,編寫如下程式碼:

var task1 = new Task(() =>
      {
        Console.WriteLine("Begin");
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("Finish");
      });
      Console.WriteLine("Before start:" + task1.Status);
      task1.Start();
      Console.WriteLine("After start:" + task1.Status);
      task1.Wait();
      Console.WriteLine("After Finish:" + task1.Status);

      Console.Read();

ask1.Status就是輸出task的當前狀態,其輸出結果如下:
在這裡插入圖片描述
可以看到呼叫Start前的狀態是Created,然後等待分配執行緒去執行,到最後執行完成。
從我們可以得出Task的簡略生命週期:
Created:表示預設初始化任務,但是“工廠建立的”例項直接跳過。

WaitingToRun: 這種狀態表示等待任務排程器分配執行緒給任務執行。

RanToCompletion:任務執行完畢。

三、Task的任務控制

Task最吸引人的地方就是他的任務控制了,你可以很好的控制task的執行順序,讓多個task有序的工作。下面來詳細說一下:

1、Task.Wait
在上個例子中,我們已經使用過了,task1.Wait();就是等待任務執行完成,我們可以看到最後task1的狀態變為Completed。

2、Task.WaitAll
看字面意思就知道,就是等待所有的任務都執行完成,下面我們來寫一段程式碼演示一下:

 static void Main(string[] args)
    {
      var task1 = new Task(() =>
      {
        Console.WriteLine("Task 1 Begin");
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("Task 1 Finish");
      });
      var task2 = new Task(() =>
      {
        Console.WriteLine("Task 2 Begin");
        System.Threading.Thread.Sleep(3000);
        Console.WriteLine("Task 2 Finish");
      });
      
      task1.Start();
      task2.Start();
      Task.WaitAll(task1, task2);
      Console.WriteLine("All task finished!");

      Console.Read();
    }

其輸出結果如下:
在這裡插入圖片描述
3、Task.WaitAny
這個用發同Task.WaitAll,就是等待任何一個任務完成就繼續向下執行,將上面的程式碼WaitAll替換為WaitAny,輸出結果如下:
在這裡插入圖片描述
4、Task.ContinueWith
就是在第一個Task完成後自動啟動下一個Task,實現Task的延續,下面我們來看下他的用法,編寫如下程式碼:

static void Main(string[] args)
    {
      var task1 = new Task(() =>
      {
        Console.WriteLine("Task 1 Begin");
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("Task 1 Finish");
      });
      var task2 = new Task(() =>
      {
        Console.WriteLine("Task 2 Begin");
        System.Threading.Thread.Sleep(3000);
        Console.WriteLine("Task 2 Finish");
      });

      task1.Start();
      task2.Start();
      var result = task1.ContinueWith<string>(task =>
      {
        Console.WriteLine("task1 finished!");
        return "This is task result!";
      });
      
      Console.WriteLine(result.Result.ToString());
      Console.Read();
    }

在這裡插入圖片描述
可以看到,task1完成之後,開始執行後面的內容,並且這裡我們取得task的返回值。

在每次呼叫ContinueWith方法時,每次會把上次Task的引用傳入進來,以便檢測上次Task的狀態,比如我們可以使用上次Task的Result屬性來獲取返回值。我們還可以這麼寫:

Task.Factory.StartNew<string>(() => {return "One";}).ContinueWith(ss => { Console.WriteLine(ss.Result);});

輸出One
要寫可伸縮的軟體,一定不能使你的執行緒阻塞。這意味著如果呼叫Wait或者在任務未完成時查詢Result屬性,極有可能造成執行緒池建立一個新執行緒,這增大了資源的消耗,並損害了伸縮性。

注意下面程式碼中TaskContinuationOptions 列舉,挺有意思的:

static void Main(string[] args)
        {
            Task<Int32> t = new Task<Int32>(i => Sum((Int32)i),10000);

            t.Start();

            t.ContinueWith(task=>Console.WriteLine("The sum is:{0}",task.Result),
                TaskContinuationOptions.OnlyOnRanToCompletion);
            
            t.ContinueWith(task=>Console.WriteLine("Sum throw:"+task.Exception),
                TaskContinuationOptions.OnlyOnFaulted);
           
            t.ContinueWith(task=>Console.WriteLine("Sum was cancel:"+task.IsCanceled),
                TaskContinuationOptions.OnlyOnCanceled);
            try
            {
                t.Wait();  // 測試用
            }
            catch (AggregateException)
            {
                Console.WriteLine("出錯");
            }
        }

        private static Int32 Sum(Int32 i)
        {
            Int32 sum = 0;
            for (; i > 0; i--)
            {
                checked { sum += i; }
            }
            return sum;
        }
    }

AttachedToParnt列舉型別(父任務)也不能放過!看看怎麼用,寫法有點新奇,看看:

static void Main(string[] args)
        {
            Task<Int32[]> parent = new Task<Int32[]>(() => {
                var results = new Int32[3];
                //
                new Task(() => results[0] = Sum(10000), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[1] = Sum(20000), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[2] = Sum(30000), TaskCreationOptions.AttachedToParent).Start();
                return results;
            });

            var cwt = parent.ContinueWith( parentTask=>Array.ForEach(parentTask.Result,Console.WriteLine));
                   

            parent.Start();
            cwt.Wait();
        }

        private static Int32 Sum(Int32 i)
        {
            Int32 sum = 0;
            for (; i > 0; i--)
            {
                checked { sum += i; }
            }
            return sum;
        }
    }

例子中,父任務建立啟動3個Task物件。預設情況下,一個任務建立的Task物件是頂級任務,這些任務跟建立它們的那個任務沒有關係。

TaskCreationOptions.AttachedToParent標誌將一個Task和建立它的那個Task關聯起來,除非所有子任務(子任務的子任務)結束執行,否則建立任務(父任務)不會認為已經結束。呼叫ContinueWith方法建立一個Task時,可以指定TaskContinuationOptions.AttachedToParent標誌將延續任務置頂為一個子任務。
在這裡插入圖片描述
  看了這麼多工的方法操作示例了,現在來挖挖任務內部構造:

每個Task物件都有一組構成任務狀態的欄位。

一個Int32 ID(只讀屬性)
1.代表Task執行狀態的一個Int32
2.對父任務的一個引用
3.對Task建立時置頂TaskSchedule的一個引用
4.對回撥方法的一個引用
5.對要傳給回撥方法的物件的一個引用(通過Task只讀AsyncState屬性查詢)
6.對一個ExceptionContext的引用
7.對一個ManualResetEventSlim物件的引用
8.還有沒個Task物件都有對根據需要建立的一些補充狀態的一個引用,補充狀態包含這些:
(1)一個CancellationToken
(2) 一個ContinueWithTask物件集合
(3)為丟擲未處理異常的子任務,所準備的一個Task物件集合
  ContinueWith便是一個更好的方式,一個任務完成時它可以啟動另一個任務。
更多ContinueWith用法參見:http://technet.microsoft.com/zh-CN/library/dd321405

5、Task的取消和異常處理
前面說了那麼多Task的用法,下面來說下Task的取消,比如我們啟動了一個task,出現異常或者使用者點選取消等等,我們可以取消這個任務
 在一個執行緒呼叫Wait方法時,系統會檢查執行緒要等待的Task是否已經開始執行,如果任務正在執行,那麼這個Wait方法會使執行緒阻塞,直到Task執行結束為止。

在一個任務丟擲一個未處理的異常時,這個異常會被“包含”不併儲存到一個集合中,而執行緒池執行緒是允許返回到執行緒池中的,在呼叫Wait方法或者Result屬性時,這個成員會丟擲一個System.AggregateException物件。

現在你會問,為什麼要呼叫Wait或者Result?或者一直不查詢Task的Exception屬性?你的程式碼就永遠注意不到這個異常的發生,如果不能捕捉到這個異常,垃圾回收時,丟擲AggregateException,程序就會立即終止,這就是“牽一髮動全身”,莫名其妙程式就自己關掉了,誰也不知道這是什麼情況。所以,必須呼叫前面提到的某個成員,確保程式碼注意到異常,並從異常中恢復。悄悄告訴你,其實在用Result的時候,內部會呼叫Wait。

怎麼恢復?

為了幫助你檢測沒有注意到的異常,可以向TaskScheduler的靜態UnobservedTaskException時間等級一個回撥方法,當Task被垃圾回收時,如果出現一個沒有被注意到的異常,CLR終結器會引發這個事件。
  
  一旦引發,就會向你的時間處理器方法傳遞一個UnobservedTaskExceptionEvenArgs物件,其中包含了你沒有注意的AggregateException。然後再呼叫UnobservedTasExceptionEvenArgs的SetObserved方法來指出你的異常已經處理好了,從而阻止CLR終止程序。這是個圖省事的做法,要少做這些,寧願終止程序,也不要呆著已經損壞的狀態而繼續執行。

除了單個等待任務,Task 還提供了兩個靜態方法:WaitAny和WaitAll,他們允許執行緒等待一個Task物件陣列。

WaitAny方法會阻塞呼叫執行緒,知道陣列中的任何一個Task物件完成,這個方法會返回一個索引值,指明完成的是哪一個Task物件。如果發生超時,方法將返回-1。它可以通過一個CancellationToken取消,會丟擲一個OperationCanceledException。

WaitAll方法也會阻塞呼叫執行緒,知道陣列中的所有Task物件都完成,如果全部完成就返回true,如果超時就返回false。當然它也能取消,同樣會丟擲OperationCanceledException。

說了取消任務的方法,現在來試試這個方法,加深下印象,修改先前例子程式碼,完整程式碼如下:

static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            
            Task<Int32> t = new Task<Int32>(() => Sum(cts.Token,10000), cts.Token);

            //可以現在開始,也可以以後開始 
            
            t.Start();

            //在之後的某個時間,取消CancellationTokenSource 以取消Task

            cts.Cancel();//這是個非同步請求,Task可能已經完成了。我是雙核機器,Task沒有完成過

            //註釋這個為了測試丟擲的異常
            //Console.WriteLine("This sum is:" + t.Result);
            try
            {
                //如果任務已經取消了,Result會丟擲AggregateException
                Console.WriteLine("This sum is:" + t.Result);
            }
            catch (AggregateException x)
            {
                //將任何OperationCanceledException物件都視為已處理。
                //其他任何異常都造成丟擲一個AggregateException,其中
                //只包含未處理的異常

                x.Handle(e => e is OperationCanceledException);
                Console.WriteLine("Sum was Canceled");
            }
         
        }

        private static Int32 Sum(CancellationToken ct ,Int32 i)
        {
            Int32 sum = 0;
            for (; i > 0; i--)
            {
                //在取消標誌引用的CancellationTokenSource上如果呼叫
                //Cancel,下面這一行就會丟擲OperationCanceledException

                ct.ThrowIfCancellationRequested();

                checked { sum += i; }
            }
            
            return sum;
        }
    }