The Parallel Programming Of .NET Framework 4.0(2) - Task Library

Thread Pool的出現,減輕了撰寫多執行緒應用程式時,所需承擔的執行緒過多而導致效能低落的風險,同時也透過重用執行緒來節省建立執行緒的時間,但是Thread Pool原始的設計仍然是太陽春了點,如前面所展示的,當我們需要等待多個Threads結束才做下一件事時,要嘛就使用Wait Handle在主程式等,要嘛就另外開一個執行緒,於內使用Wait Handle來等待,前者會造成主程式的停滯,後者則會多使用一個執行緒,雖然還是有辦法來調整至完美,但還是需要一道手續。

 

The Parallel Programming Of .NET Framework 4.0(2) -Task Library
 
 
 
/黃忠成
 
 
New!! Introducing Task Library of .NET Framework 4
 
     Thread Pool的出現,減輕了撰寫多執行緒應用程式時,所需承擔的執行緒過多而導致效能低落的風險,同時也透過重用執行緒來節省建立執行緒的時間,但是Thread Pool原始的設計仍然是太陽春了點,如前面所展示的,當我們需要等待多個Threads結束才做下一件事時,要嘛就使用Wait Handle在主程式等,要嘛就另外開一個執行緒,於內使用Wait Handle來等待,前者會造成主程式的停滯,後者則會多使用一個執行緒,雖然還是有辦法來調整至完美,但還是需要一道手續。
     .NET Framework 4.0中,這些問題都將獲得解決,.NET Team基於原始的Thread Pool機制,製造了一個進階版的Thread Pool: Task Library,這組新增的函式庫除了擁有原先的Thread Pool機制外,還提供了更多對於Thread Pool中執行緒的控制函式,你再也不需要自己使用Wait Handle來控制執行緒的執行順序,也不需要為了執行緒發生例外後的處理傷腦筋,Task Library所提供的函式可以讓你在主程式中以Wait函式等待特定Task的結束,也可以讓你設定一個delegate,於執行緒正常結束時觸發,更棒的是,你也可以設定一個delegate,在執行緒發生例外時觸發。甚至,你還能指定這些delegate要執行在獨立的執行緒中,或是特定的執行緒中。除了更方便的Thread操控機制外,Task Library也針對原本的APM、舊Thread Pool程式提供了Wrapper物件,讓這些應用程式能快速的移轉到新的Task Library並得到其所有便利的優點。
     除了表象的功能之外,Task Library也改變了Thread Pool的排程規則,原始Thread Pool的排程規則是FIFO,也就是先進先出,新的Task Library提出了另一種設計:Thread Local Queue,用圖來說明比較能理解這兩者的不同,圖14是傳統Thread Pool的處理模式,採用單一Queue,採先進先出制。
 
14
 
圖中顯示,當Global Queue中以QueueUserWorkItem排入三個執行緒要求,其取出順序會依照排入的順序,也就是Thread 1會取到最先排入的Pool Item1,接著Thread 2會取到Pool Item 2,最後Thread 3會取到Pool Item3,這就是先進先出制。
Task Library依舊維持Global Queue的設計,但加入了Thread Local Queue概念,當建立Task時,其會將該Task排入該ThreadLocal Queue中,簡單的說,就是在Global Queue外添加另一個Local Queue,而Local Queue是每個Thread都擁有一個,如圖15
 
15
 
Step 1看起,當Global Queue裡有一個Item時,Thread A被排定執行此Item,而此Item中建立了兩個Task,此時這兩個Task會被排入Thread ALocal Queue中,而不是Global Queue,當Pool Item 1執行完畢後,Thread A變成閒置的Thread,此時它會先至Local Queue取出Task A-2來執行,此為LIFO模式,也就是後入先出制,當Task A-2執行完畢後,Thread A會接著執行Task A-1,在Thread Local Queue中無任何Task時,Thread A會嘗試到Global Queue找尋是否有Item待執行,有的話就拿來執行,沒有的話就開始找尋其它ThreadLocal Queue,如果有的話就拿過來執行,此為Work-Stealing 模式。
   OK,讓我們理清這幾個設計的目的,為何在Global Queue外為每個Thread另設一個Local Queue?很簡單,因為當所有Threads都查詢Global Queue中有沒有待執行的Item時,也代表著Global Queue會一直處於Lock/Unlock的循環,Lock區段的多寡影響著執行緒的效率,當使用Local Queue時,執行緒不需Lock就能取出Item,不需要付出查詢Global Queue而導致的Lock代價。
   那為何Local QueueLIFO而非FIFO?這是因為CPU的快取設計,最後放入的Item通常代表著其變數及狀態都可能還在CPU的快取區中,執行起來自然較先放入的Item較快,當然!這只是理論上的設計,提升的效能比其實要視情況而定,不過有機會總比沒機會好,搾取最高效能是Task Library的設計目標之一。
   Local Queue中沒有Item需執行,且Global Queue內也沒有待執行的Item時,就需要到別的Thread Local Queue找,這樣不會造成很多Lock嗎?是的,這是會有Lock存在的!如果目前有1000Pooled Thread,光查詢這些ThreadLocal Queue是否有Item待執行,就得耗費比查詢Global Queue更多的lock數,因此Task Library於此採取了動態亂數模式,它每次嘗試Work-Stealing時,會以現行Thread IDseed,以所有Pool Thread數為上數,於其間亂數產生一個數字,以此數字為基準開始找,舉個例來說,當所有Pool Thread數為30,空閒Thread ID12,那麼Work Stealing就會在12 - 30間取一個數出來,接著找尋對應的Queue,如果數字為13,那麼開始找的QueueThread ID13Local Queue,然後持續遞增此數往後找,第一個是(13%30 = 13),第二個是(14%30=14),當此數到達30時,就會從零開始找,因為搜尋次數是Pool Thread的總數。
那這樣真的比較快嗎?答案很複雜,亂數搜尋法則與循序搜尋法則,在次數多的情況下平均,亂數搜尋法在大多數情況下會比循序法快,有興趣的讀者可以用以下的程式試試。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
 
namespace ConsoleApplication8
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            int foundValue = 29;
            sw.Start();
            for (int i = 0; i < 30; i++)
            {
                Thread.Sleep(100);
                if (i == foundValue)
                    break;
            }
            sw.Stop();
            long first = sw.ElapsedMilliseconds;
            Console.WriteLine(sw.ElapsedMilliseconds.ToString());
            Random r2 = new Random(4);
            long sec = 0;
            for (int v = 0; v < 15; v++)
            {
                sw.Reset();
                sw.Start();
                Random r = new Random(r2.Next(30));
 
                int val = r.Next(30);
                for (int i = 30; i >= 0; i--)
                {
                    Thread.Sleep(100);
                    if (val % 30 == foundValue)
                        break;
                    val++;
                }
                sw.Stop();
                sec += sw.ElapsedMilliseconds;
                Console.WriteLine(sw.ElapsedMilliseconds.ToString());
            }
            Console.WriteLine("{0} - {1}", (first * 15).ToString(), sec.ToString());
            Console.ReadLine();
        }
    }
}
 
執行結果:
 
2836
2074
3279
2624
2952
545
2051
2279
1167
2732
2187
1639
874
1312
2625
435
42540 - 28775
 
你可能會認為,如果把前面的29改成2,那麼循序一定更快,但是別忘了,這個數字是不定的,也就是說它可能是2、也可能是29,在29的情況下,循序是最慢的,但是在亂數搜尋情況下,29的情況在多數情況下一定比循序快,效率決定在擁有ItemThread 是在前端還是尾端,也決定在亂數產生的基數是在前端還是尾端,看起來Task Library Team似乎嘗試將Work Stealing的耗費時間變成近似常數,無論如何,不管此法是否真的有效率,目前的Task Library實作是以此法來進行Work-Stealing的,而且這差異也很微妙,微妙到可以忽略。
 如你所知,Task Library不僅在功能上提升,也嘗試著在Thread Pool機制下,以Local QueueWork-Stealing 來榨出更高的效能。
 
 
 
Using Task Library
 
 
    使用Task Library很簡單,就像是在手動建立Thread般直覺,擁有Thread Pool的所有優點,但沒有Thread Pool的缺點。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            int i = 0;
            Random r = new Random(DateTime.Now.Second);
            Task t = new Task(() =>
                {
                    for(int v = 0; v < 100; v++)
                        i += r.Next(100);
                });
 
            t.Start();
 
            Console.ReadLine();
 
            t.Wait();
 
            Console.WriteLine(i.ToString());
            Console.ReadLine();
        }
    }
}
 
 
Task代表著一個Thread,當建立一個Task後呼叫Start時,內建的排程器就會尋找是否有空閒的Thread,有的話就將此Task交給其執行,否的話就看Pool Thread數是否到達上限(250/per core),否的話建立新的Thread來執行,是的話就排入等待有Thread空閒下來才執行,下面是同樣的目的,但使用ThreadPool.QueueUserWorkItem來寫。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace ConsoleApplication10
{
    class Program
    {
        static void Main(string[] args)
        {
            int i = 0;
            Random r = new Random(DateTime.Now.Second);
            ManualResetEvent mr = new ManualResetEvent(false);
            ThreadPool.QueueUserWorkItem((state) =>
            {
                for(int v = 0; v < 100;v++)
                    i += r.Next(100);
                mr.Set();
            });
 
           
            Console.ReadLine();
 
            mr.WaitOne();
 
            Console.WriteLine(i.ToString());
            Console.ReadLine();
        }
    }
}
 
 很明顯的,在使用ThreadPool.QueueUserWorkItem時,如果要達成此程式同樣的目的,比起Task Library來得麻煩,因為要建立Wait Handle來等待Pool Thread結束。
 Task Library簡化的程度還不止於此,使用另一個Task<TResult>類別,還能讓Thread程式更加清晰且簡單。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = new Task<int>(() =>
                {
                    int i = 0;
                    Random r = new Random(DateTime.Now.Second);
                    for(int v = 0; v < 100; v++)
                        i += r.Next(100);
                    return i;
               });
 
            t.Start();
 
            Console.ReadLine();
 
            Console.WriteLine(t.Result.ToString());
            Console.ReadLine();
        }
    }
}
 
發現到了嗎?原本得放在外面的i被搬到Threaddelegate程式碼裡了,這樣我們就不用記那個變數是放Thread的回傳值了,一律使用Task.Result來取。
另外,Task.Wait也不需要了,因為當取Task.Result值時,其自會呼叫Task.Wait來等待,Task Library確實擁有了Thread PoolThread的所有優點是吧。
還有另一種更清楚的寫法,就是利用ContinueWith
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = new Task<int>(() =>
                {
                    int i = 0;
                    Random r = new Random(DateTime.Now.Second);
                    for(int v = 0; v < 100; v++)
                        i += r.Next(100);
                    return i;
                });
 
            t.ContinueWith((Task<int> task) =>
                {
                    Console.WriteLine(task.Result.ToString());
                   
                });
 
            t.Start();
            Console.ReadLine();
        }
    }
}
 
呼叫ContinueWith所傳入的delegate,會在該Task結束時觸發,形成了Threads的執行串鏈,下面的例子應該可以讓你更清楚ContinueWith所謂的串鏈式設計。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = new Task<int>(() =>
                {
                    int i = 0;
                    Random r = new Random(DateTime.Now.Second);
                    for(int v = 0; v < 100; v++)
                        i += r.Next(100);
                    return i;
                });
 
            Task t2 = t.ContinueWith((Task<int> task) =>
                {
                    Console.WriteLine(task.Result.ToString());
                   
                });
 
            t2.ContinueWith(task =>
                {
                    Console.WriteLine("done");
                });
 
            t.Start();
            Console.ReadLine();
        }
    }
}
 
ContinueWith是一個大題目,我們稍後再來談。
 
 
Parent/Child Relationship of Tasks
 
 
    Thread的獨立性質不同,Task是可以有父子關係的,在Taskdelegate中再建立Task時,可以傳入一個TaskCreatingOptions來告訴Task Library,此Task必須是外圍Task的子Task,這樣一來在呼叫父TaskWait時,便會等到所有子Task都結束才會返回,見下例:
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = new Task<int>(() =>
                {
                    Task<int> t1 = new Task<int>(() =>
                        {
                            int i = 0;
                            Random r = new Random(DateTime.Now.Second);
                            for (int v = 0; v < 100; v++)
                                i += r.Next(100);
                            return i;
                        },TaskCreationOptions.AttachedToParent);
 
                    Task<int> t2 = new Task<int>(() =>
                    {
                        int i = 0;
                        Random r = new Random(DateTime.Now.Second);
                        for (int v = 0; v < 100; v++)
                            i += r.Next(100);
                        return i;
                    }, TaskCreationOptions.AttachedToParent);
 
                    Task<int> t3 = new Task<int>(() =>
                    {
                        int i = 0;
                        Random r = new Random(DateTime.Now.Second);
                        for (int v = 0; v < 100; v++)
                            i += r.Next(100);
                        return i;
                    }, TaskCreationOptions.AttachedToParent);
                    t1.Start();
                    t2.Start();
                    t3.Start();
                    return t1.Result + t2.Result + t3.Result;
                });
            t.Start();
            t.Wait();
            Console.WriteLine(t.Result.ToString());                               
            Console.ReadLine();
        }
    }
}
 
當然,此例一定會如此,因為我們在末端加總了Result,這是正常的寫法,要證明t會在t1,t2,t3結束時才返回,得用另一個另類的寫法。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            int i = 0;
            Task t = new Task(() =>
                {
                    Task t1 = new Task(() =>
                        {
                            Random r = new Random(DateTime.Now.Second);
                            for (int v = 0; v < 100; v++)
                                Interlocked.Add(ref i,r.Next(100));
                        },TaskCreationOptions.AttachedToParent);
 
                    Task t2 = new Task(() =>
                    {
                        Random r = new Random(DateTime.Now.Second);
                        for (int v = 0; v < 100; v++)
                            Interlocked.Add(ref i, r.Next(100));
                    }, TaskCreationOptions.AttachedToParent);
 
                    Task t3 = new Task(() =>
                    {
                        Random r = new Random(DateTime.Now.Second);
                        for (int v = 0; v < 100; v++)
                            Interlocked.Add(ref i, r.Next(100));
                        Thread.Sleep(5000);
                    }, TaskCreationOptions.AttachedToParent);
                    t1.Start();
                    t2.Start();
                    t3.Start();
                });
            t.Start();
            t.Wait();
            Console.WriteLine(i.ToString());                               
            Console.ReadLine();
        }
    }
}
 
那為何要在Task中建立子Task呢?Task不就已經在Thread中跑了嗎?這樣做的優點是什麼?目的是效能,現今電腦因CPU最高時脈很難到達4G以上,所以開始往多核心發展,也就是變相的多CPU環境,相信讀者們很多都已經是使用雙核已上的CPU了,在這種多核環境下,為每一個核心分配一個執行緒,就可以讓運算速度提升約70%以上,所以以本例而言,在4核電腦上的執行效能會比在雙核及單核上來得高,因為我們充份的使用了4Thread(當外圍結束後,其實只用到3核,有一核是處於等待狀態,隨時可被切走)
 
 
TaskCreatingOptions
 
    上例中,我們於建立Task時傳入TaskCreationOpions.AttachedToParent,代表著建立的Task必須是外圍Task的子Task,這個參數可以控制Task的型態,下表是其可能值。
 
說明
None
預設值,此Task會被排入Local Queue中等待執行,採LIFO模式。
AttachedToParent
建立的Task必須是外圍的Task之子Task,一樣是放入Local Queue,採LIFO模式。
LongRunning
建立的Task不受Thread Pool所管理,直接新增一個Thread來執行此Task,無等待、無排程。
PreferFairness
建立的Task直接放入Global Queue中,採FIFO模式。
 
NoneAttachedToParent在之前的例子都用過,所以不需特別解釋,比較有趣的是LongRunningPreferFairnessLongRunning很簡單,使用此參數所建立的Task就跟直接建立Thread一樣,其將立刻以Thread Pool之外的Thread執行,也就是其不受限於Thread Pool,這代表著Task Library不僅加強了Thread Pool的處理,也延伸了傳統手動建立Thread的寫法,令其也能獲得Task LibraryContinueWithWaitResult等優點。
    PreferFairness是比較有趣的參數,以此建立的Task會被強迫放到Global Queue中,如前面所提,只有Local Queue為空的情況下,Task Library才會嘗試由Global Queue取出待處理的Task,這多半用於優先權較低的Task,當然!我知道這很難理解,所以準備了一個可以輕易分辨放在Local Queue跟放在Global Queue不同之處的例子。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            List<string> bags = new List<string>();
            for (int i = 0; i < 10; i++)
            {
                Task t = new Task(() =>
                {
                    Task t2 = new Task(() =>
                    {
                        lock(bags)
                            bags.Add("Inner Thread
                             "+Thread.CurrentThread.ManagedThreadId.ToString());
                        Thread.Sleep(1000);
                    },TaskCreationOptions.PreferFairness);
                    t2.Start();
                    lock(bags)
                        bags.Add("Outer Thread "+Thread.CurrentThread.ManagedThreadId.ToString());
                    Thread.Sleep(1000);
                });
                t.Start();
            }
 
            Thread.Sleep(15000);
 
            foreach (var item in bags)
                Console.WriteLine(item);
 
            Console.ReadLine();
 
        }
    }
}
 
執行結果如下:
 
Outer Thread 12
Outer Thread 10
Outer Thread 13
Outer Thread 11
Outer Thread 14
Outer Thread 10
Outer Thread 12
Outer Thread 11
Outer Thread 13
Outer Thread 15
Inner Thread 14
Inner Thread 12
Inner Thread 10
Inner Thread 13
Inner Thread 11
Inner Thread 15
Inner Thread 14
Inner Thread 12
Inner Thread 10
Inner Thread 11
 
可以發現,設定為PreferFairness後,Inner Thread被排到後面才執行,當拿掉PreferFairness後,會發現截然不同的結果。
 
........
Task t2 = new Task(() =>
{
           lock (bags)
            bags.Add("Inner Thread " + Thread.CurrentThread.ManagedThreadId.ToString());
           Thread.Sleep(1000);
 });
..........
 

 

Outer Thread 10
Outer Thread 11
Outer Thread 12
Outer Thread 13
Outer Thread 14
Inner Thread 11
Inner Thread 12
Inner Thread 13
Inner Thread 10
Inner Thread 14
Outer Thread 12
Outer Thread 10
Outer Thread 13
Outer Thread 11
Outer Thread 14
Inner Thread 12
Inner Thread 10
Inner Thread 13
Inner Thread 11
Inner Thread 14

Inner Thread在沒有PreferFairness的情況下,會被放在Local Queue中,所以會很快的被取出執行,但如果掛上PreferFairness,那麼在Local Queue沒東西的情況下,這些Task才會被執行,所以Inner Thread會被排在Outer Thread全部開始後才開始。

 
Exception Handling
 
     Task執行期間發生例外情況時,該例外會被記錄下來,於你呼叫Wait或是取Result值時拋出一個AggreateException物件,其InnerExceptions屬性中即包含了所有Task執行期間的例外物件,見下例:
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            Task t1 = new Task(() =>
            {
                for (int v = 0; v < 10; v++)
                {
                    if (v == 3)
                        throw new DivideByZeroException();
                    Thread.Sleep(1000);
                    Console.WriteLine(v);
                }
            });
            t1.Start();
            Thread.Sleep(5000);
            try
            {
                t1.Wait();
            }
            catch (AggregateException ex)
            {
                foreach (Exception item in ex.InnerExceptions)
                    Console.WriteLine(item.Message);
            }
            Console.ReadLine();
        }
    }
}
 
就一般情況下來說,AggregateExceptionInnerExceptions應該只會有一個例外物件,因為Task於執行時期一旦遭遇例外後,就會停止執行。但這是在單CPU/單核情況下,在多CPU/多核情況下,如果此Task內放了另外兩個子Task,那麼這兩個子Task可能會處於同時執行,且同時產生例外的情況,此時InnerExceptions裡就會有兩個例外,在考慮到多核的情況下,建議還是以AggregateException中可能會有多個例外來考量。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            Task t1 = new Task(() =>
            {
                Task t2 = new Task(() =>
                    {
                        for (int v = 0; v < 10; v++)
                        {
                            if (v == 3)
                                throw new DivideByZeroException();
                            Thread.Sleep(1000);
                            Console.WriteLine(v);
                        }
                    },TaskCreationOptions.AttachedToParent);
                t2.Start();
            });
            t1.Start();
            Thread.Sleep(5000);
            try
            {
                t1.Wait();
            }
            catch (AggregateException ex)
            {
                DumpException(ex);       
            }
            Console.ReadLine();
        }
 
        public static void DumpException(AggregateException exception)
        {
            foreach (Exception item in exception.InnerExceptions)
            {
                if (item is AggregateException)
                    DumpException((AggregateException)item);
                else
                    Console.WriteLine(item.Message);
            }
        }
    }
}
 
請注意,當使用Parent/Child Task模式時,當子Task發生例外,若未於父Task中處理,該例外會被包入AggregateException後上移至父Task,因此我們可以於父TaskWait時處理這些例外,此時ex中應該包含著一個Exception,其型態是AggregateException,其內才是DivideByZeroException
另外,如果把AttachedToParent屬性拿掉,那麼你將無從取出該例外,因為此時t2並不是t1的子Task,所以得用一般的Task來看待,你得在t1中處理它的例外。
 
 
Cancelling Operations Of Task
 
    執行緒執行期間,偶而會因發生特殊情況或是由外部所導致必須停止執行,這稱為【執行緒的取消】,舉個例來說,你的執行緒正進行一個長時間的工作,但使用者或許趕著下班,所以在執行後反悔了想取消此動作,這可能會導致兩種情況,一是該程式未提供【取消】按鈕,所以使用者想直接離開程式,即使該程式不允許,他也逕自透過工作管理員來關掉程式,這自然不會是你所預期的最好結果。
    另一種情況是程式提供了取消按鈕,使用者直接按下取消,稍後片刻後,執行緒完成取消作業,程式既沒有非預期關閉的隱憂,使用者也不會不耐這幾秒,皆大歡喜!
 Task Library提供了兩種取消流程,皆是透過CancellationTokenCancellationTokenSource物件,下面是第一種。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
 
            var ctoken = cts.Token;
            Task t1 = new Task(() =>
            {
                for (int v = 0; v < 10; v++)
                {
                    ctoken.ThrowIfCancellationRequested();
                    Thread.Sleep(1000);                   
                    Console.WriteLine(v);
                }
            },ctoken);
 
            t1.Start();
            Thread.Sleep(2000);
            cts.Cancel();
            try
            {
                t1.Wait();
            }
            catch (Exception)
            {
                if (t1.IsCanceled)
                    Console.WriteLine("cancel");
            }           
            Console.ReadLine();
            cts.Dispose();
        }
    }
}
 
 
在建立Task時,可以傳入一個CancellationToken物件,該物件由CancellationTokenSource提供,此物件便是Task中用來判別是否處於取消狀態的物件,當需要由外部取消時,就呼叫CancellationTokenSourceCancel函式,此時與此CancellationTokenSource相連的CancellationTokenIsCancellationRequested屬性會變成True,當呼叫ThrowIfCancellationRequested函式後,就會拋出一個OperationCanceledException例外,此時Task會終止執行,於執行期間所發生的所有例外會被記錄下來,封裝成一個AggreateException,所有例外會被放在其InnerExceptions屬性中,當呼叫了TaskResult、或是Wait時,這個AggreateException例外就會被拋出,此時如果是處於cancel狀態的話,Task.IsCancelled屬性會變成True,此例以此來判斷是否是因為取消而導致Task終止。
另一種非例外的寫法如下:
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
 
            var ctoken = cts.Token;
            Task t1 = new Task(() =>
            {
                for (int v = 0; v < 10; v++)
                {
                    if (ctoken.IsCancellationRequested)
                        return;
                    Thread.Sleep(1000);
                    Console.WriteLine(v);
                }
            }, ctoken);
            t1.Start();
            Thread.Sleep(2000);
            cts.Cancel();
            t1.Wait();
            if (t1.IsCanceled)
                Console.WriteLine("cancel");
            Console.ReadLine();
            cts.Dispose();
        }
    }
}
 
這種寫法有個缺點,那就是Task.IsCanceled不會是True值,這有可能讓你無從得知Task是完成了還是取消了。
請注意,取消動作在根本上必須是一個合作形式,因為一旦脫開這個模式,Task自身就無法在取消前進行清除動作,所以正確且完善的寫法應該如下:
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
 
            var ctoken = cts.Token;
            Task t1 = new Task(() =>
            {
                for (int v = 0; v < 10; v++)
                {
                    if (ctoken.IsCancellationRequested)
                    {
                        //clean up.
                        ctoken.ThrowIfCancellationRequested();
                    }
                    Thread.Sleep(1000);
                    Console.WriteLine(v);
                }
            }, ctoken);
            t1.Start();
            Thread.Sleep(2000);
            cts.Cancel();
            try
            {
                t1.Wait();
            }
            catch (Exception ex)
            {
                if (t1.IsCanceled)
                    Console.WriteLine("cancel");
            }
            Console.ReadLine();
            cts.Dispose();
        }
    }
}
 
或許你與我一樣,會對Token可傳入Task,但無法於Taskdelegate中存取的情況感到疑惑,就架構上來說,以下的程式碼似乎是比較合理的。
 
Task t1 = new Task(() =>
            {
                for (int v = 0; v < 10; v++)
                {
                    if (Task.Current.CancelToken.IsCancellationRequested)
                    {
                        //clean up.
                        Task.Current.CancelToken.ThrowIfCancellationRequested();
                    }
                    Thread.Sleep(1000);
                    Console.WriteLine(v);
                }
            }, ctoken);
 
 
不過Beta 2移除了Task.Current屬性,原因是Task Library Team認為,Token大多數情況一定會在delegate可以存取的範圍內(使用Lambda Expression的情況下),如果不是的話,也可以透過state參數傳入。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApplication9
{
    class Program
    {
        static void ThreadFunc(object state)
        {
            CancellationToken token = (CancellationToken)state;
            for (int v = 0; v < 10; v++)
            {
                if (token.IsCancellationRequested)
                {
                    //clean up.
                    token.ThrowIfCancellationRequested();
                }
                Thread.Sleep(1000);
                Console.WriteLine(v);
            }
        }
 
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
 
            Task t1 = new Task(ThreadFunc, cts.Token,cts.Token);
            t1.Start();
            Thread.Sleep(2000);
            cts.Cancel();
            try
            {
                t1.Wait();
            }
            catch (Exception ex)
            {
                if (t1.IsCanceled)
                    Console.WriteLine("cancel");
            }
            Console.ReadLine();
            cts.Dispose();
        }
    }
}
 
 
呃,我必須承認這真的有點怪就是了,看來他們很喜歡黏巴達..........
 
 
Task Status of Planed & Un planed Exit
 
     截至目前為止,我們已經遇到Task結束時的三種情況,一是正常結束、二是產生例外、三是透過Cancel機制,這三種情況都會反應在Task.Status屬性上,其可能值如下表:
 
說明
Created
Task已經建立,但未呼叫Start
WaitingForActivation
Task已排入排程,但尚未執行(一般我們建立的Task不會有此狀態,只有ContinueWith所產生的Task才會有此狀態,容後再述)
WaitingToRun
Task已排入排程,等待執行中。
Running
Task執行中。
WaitingForChildrenToComplete
Task正等待子Task結束。
RanToCompletion
Task已經正常執行完畢。
Canceled
Task已被取消。
Faulted
Task執行中發生未預期例外。
除了Status屬性外,Task還提供了另外三個屬性來判定Task狀態。
屬性
說明
IsCompleted
Task已經正常執行完畢。
IsFaulted
Task執行中發生未預期例外。
IsCanceled
Task已被取消。