.NET 4.0 任務(Task)

風靈使發表於2018-05-14

隨著 .NET 4.0的到來,她與以前各版本的一個明顯差別就是並行功能的增強,以此來適應這個多核的世界。於是引入了一個新概念—任務,作為支援並行運算的重要組成部分,同時,也作為對執行緒池的一個補充和完善。從所周知,使用執行緒池有兩個明顯的缺點,那就是一旦把我們要執行的任務放進去後,什麼時候執行完成,以及執行完成後需要返回值,我們都無法通過內建的方式而得知。由於任務(Task)的推出,使得我們對並行程式設計變得簡單,而且不用關心底層是怎麼實現的,由於比執行緒池更靈活,如果能掌握好Task,對於寫出高效的並行程式碼非常有幫助。

一、新建任務

System.Threading.Tasks名稱空間下,有兩個新類,Task及其泛型版本Task<TResult>,這兩個類是用來建立任務的,如果執行的程式碼不需要返回值,請使用Task,若需要返回值,請使用Task<TResult>

建立任務的方式有兩種,一種是通過Task.Factory.StartNew方法來建立一個新任務,如:

Task task = Task.Facotry.StartNew(()=>Console.WriteLine(“Hello, World!”));//此行程式碼執行後,任務就開始執行

另一種方法是通過Task類的建構函式來建立一個新任務,如:

Task task = new Task(()=>Console.WriteLine(“Hello, World!”));//此處只把要完成的工作交給任務,但任務並未開始

task.Start();//呼叫Start方法後,任務才會在將來某個時候開始執行。

同時,我們可以呼叫Wait方法來等待任務的完成或者呼叫IsCompleted屬性來判斷任務是否完成。需要說明的是,兩種建立任務的方法都可以配合TaskCreationOptions列舉來實現我們對任務執行的行為具體控制, 同時,這兩種建立方式允許我們傳遞一個TaskCreationOptions物件來取消正在執行中的任務,請看任務的取消。

二、任務的取消

這世界唯一不變的就是變化,當外部條件發生變化時,我們可能會取消正在執行的任務。對於.NET 4.0之前,.NET並未提供一個內建的解決方案來取消執行緒池中正在執行的程式碼,但在.NET 4.0中,我們有了Cooperative Cancellation模式,這使得取消正在執行的任務變得非常簡單。如下所示:

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

    namespace TaskDemo
    {
        class Program
        {
            static void Main()
            {
                CancellationTokenSource cts = new CancellationTokenSource();
                Task t = new Task(() => LongRunTask(cts.Token));
                t.Start();
                Thread.Sleep(2000);
                cts.Cancel();
                Console.Read();
            }

            static void LongRunTask(CancellationToken token)
            {

                 //此處方法模擬一個耗時的工作
                for (int i = 0; i < 1000; i++)
                {
                    if (!token.IsCancellationRequested)
                    {
                        Thread.Sleep(500);
                        Console.Write(".");
                    }
                    else
                    {
                        Console.WriteLine("任務取消");
                        break;
                    }
                }
            }
        }
    }

三、任務的異常機制

在任務執行過程中產生的未處理異常,任務會把它暫時隱藏起來,裝進一個集合中。當我們呼叫Wait方法或者Result屬性時,任務會丟擲一個AggregateException異常。我們可以通過呼叫AggregateException物件的只讀屬性InnerExceptions來得到一個ReadOnlyCollection<Exception>物件,它才是儲存丟擲異常的集合,它的第一個元素就是最初丟擲的異常。同樣的,AggregateException物件的InnerException屬性也會返回最初丟擲的異常。

值得重視的是,由於任務的隱藏機制的特點,一旦產生異常後,如果我們不呼叫相應的方法或者屬性檢視異常,我們也無法判斷是否有異常產生(Task不會主動丟擲異常)。當Task物件被GC回收時,Finalize方法會查檢是否有未處理的異常,如果不幸剛才好有,則Finalize方法會將此AggregateException再度丟擲,如果再不幸,我們沒有捕獲處理這個異常,則我們的程式會立即中止執行。如果發生這樣的事情,會是多麼大的災難啊!

為了避免這種不幸的發生,我們可以通過註冊TaskScheduler類的靜態UnobservedTaskException事件來處理這種未被處理的異常,避免程式的崩潰。

四、任務啟動任務

任務的強大與靈活之一是,當我們完成一個任務時,可以自動開始一個新任務的執行。如下所示:

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

    namespace TaskDemo
    {
        public class AutoTask
        {
            static void Main()
            {
                Task task = new Task(() => { Thread.Sleep(5000); Console.WriteLine("Hello,"); Thread.Sleep(5000); });
                task.Start();
                Task newTask = task.ContinueWith(t => Console.WriteLine("World!"));
                Console.Read();
            }
        }
    }

對於ContinueWith方法,我們可以配合TaskContinuationOptions列舉,得到更多我們想要的行為。

五、子任務

任務是支援父子關係的,即在一個任務中建立新任務。如下所示:

    using System;
    using System.Threading.Tasks;

    namespace TaskDemo
    {
        class ChildTask
        {
            static void Main()
            {
                Task parant = new Task(() =>
                {
                    new Task(() => Console.WriteLine("Hello")).Start();
                    new Task(() => Console.WriteLine(",")).Start();
                    new Task(() => Console.WriteLine("World")).Start();
                    new Task(() => Console.WriteLine("!")).Start();
                });
                parant.Start();
                Console.ReadLine();
            }
        }
    }

值得注意的是,以上程式碼中所示的子任務的呼叫並不是以程式碼的出現先後為順序來呼叫的。

六、任務工廠

在某些情況下,我們會遇到建立大量的任務,而恰好這些任務共用某個狀態引數(如CancellationToken),為了避免大量的呼叫任務的構造器和一次又一次的引數傳遞,我們可以使用任務工廠來為我們處理這種大量建立工作。如下程式碼所示:

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

namespace TaskDemo
{
    public class FactoryOfTask
    {
        static void Main()
        {
            Task parent = new Task(() =>
            {
                CancellationTokenSource cts = new CancellationTokenSource();
                TaskFactory tf = new TaskFactory(cts.Token);
                var childTask = new[]
                {
                 tf.StartNew(()=>ConcreteTask(cts.Token)),
                 tf.StartNew(()=>ConcreteTask(cts.Token)),
                 tf.StartNew(()=>ConcreteTask(cts.Token))
                };

                Thread.Sleep(5000);//此處睡眠等任務開始一定時間後才取消任務
                cts.Cancel();
            }
            );

            parent.Start();//開始執行任務
            Console.Read();
        }

        static void ConcreteTask(CancellationToken token)
        {
            while (true)
            {
                if (!token.IsCancellationRequested)
                {
                    Thread.Sleep(500);
                    Console.Write(".");
                }
                else
                {
                    Console.WriteLine("任務取消");
                    break;
                }
            }
        }
    }
}

七、任務排程程式

任務的排程通過排程程式來實現的,目前,.NET 4.0內建兩種任務排程程式:執行緒池任務排程程式(thread pool task scheduler)同步上下文任務排程程式(synchronization context task scheduler)。預設情況下,應用程式使用執行緒池任務排程程式呼叫執行緒池的工作執行緒來完成任務,如受計算限制的非同步操作。同步上下文任務排程程式通常使用UI執行緒來完成與Windows FormsWindows Presentation Foundation(WPF)以及SilverLight應用程式相關的任務。

可喜的是,.NET 4.0 提供了TaskScheduler抽象類供開發人員繼承來實現自定義任務排程程式的開發,有興趣的同學可以試試。

八、總結

任務給了我們更多的方便性、靈活性的同時,也帶來了比執行緒池更多的資源消耗。如果想減少資源消耗,請直接使用執行緒池QueueUserWorkItem方法效果會更好;如果想要更多的控制與靈活性,任務(Task)是不二的選擇。這個要我們開發者自己去斟酌了。

相關文章