共用方式為


工作平行處理原則 (工作平行程式庫)

更新:2011 年 3 月

工作平行程式庫 (TPL) 顧名思義就是以「工作」(Task) 的概念為基礎。 「工作平行處理原則」(Task Parallelism) 是指同時執行一個或多個獨立工作。 工作表示一項非同步作業,在某種程度上與建立新的執行緒或 ThreadPool 工作項目類似,只是抽象程度更高。 工作主要提供兩項優點:

  • 更有效率且更靈活地使用系統資源。

    在幕後,工作會排入已用演算法 (例如攀登演算法) 加強的 ThreadPool 中的佇列中,這些演算法會判斷可發揮最大輸送量的執行緒數目,並根據此數字進行調整。 這樣會使得工作變得相當輕便,而且您可以建立許多工作來啟用細部的平行處理原則。 為了配合這一點,我們採用眾所周知的工作竊取演算法來提供負載平衡。

  • 比使用執行緒或工作項目提供更多的程式設計控制能力。

    工作和以工作為中心建置的架構提供了一組豐富的 API,可支援等候、取消、接續、穩固的例外狀況處理、詳細狀態和自訂排程等各式各樣的作業。

基於這兩個理由,在 .NET Framework 4 中,工作是撰寫多執行緒、非同步和平行程式碼時較好的 API。

隱含建立和執行工作

Parallel.Invoke 方法有便利的方式可讓您同時執行任何數目的任意陳述式。 只要為每個工作項目傳入 Action 委派即可。 若要建立這些委派,使用 Lambda 運算式是最簡單的方式。 Lambda 運算式可以呼叫具名方法,或提供程式碼內嵌。 下列範例說明如何使用基本 Invoke 呼叫,建立並啟動兩項同時執行的工作。

注意事項注意事項

本文件使用 Lambda 運算式來定義 TPL 中的委派。如果您不太熟悉 C# 或 Visual Basic 中的 Lambda 運算式,請參閱 PLINQ 和 TPL 中的 Lambda 運算式

Parallel.Invoke(Sub() DoSomeWork(), Sub() DoSomeOtherWork())
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
注意事項注意事項

Invoke 在幕後建立的 Task 執行個體數目,不一定會等於所提供的委派數目。TPL 可採用各種不同的最佳化方式,尤其是有大量委派時。

如需詳細資訊,請參閱 HOW TO:使用 Parallel.Invoke 執行平行作業

若要進一步控制工作執行,或是從工作傳回值,您必須更明確地使用 Task 物件。

明確建立和執行工作

工作是以 System.Threading.Tasks.Task 類別表示。 傳回值的工作是以繼承自 TaskSystem.Threading.Tasks.Task<TResult> 類別表示。 工作物件會處理基礎結構細節,並提供可供呼叫端執行緒在整個工作存留期存取的方法和屬性。 例如,您可以隨時存取工作的 Status 屬性,看看工作是否已開始執行、已執行完畢、已取消或已擲回例外狀況。 狀態是以 TaskStatus 列舉表示。

當您建立工作時,您會指定使用者委派給工作,這個使用者委派會封裝此工作會執行的程式碼。 委派可以以具名委派、匿名方法或 Lambda 運算式的形式呈現。 Lambda 運算式可以包含具名方法的呼叫,如下列範例所示。

        ' Create a task and supply a user delegate by using a lambda expression.
        Dim taskA = New Task(Sub() Console.WriteLine("Hello from taskA."))

        ' Start the task.
        taskA.Start()

        ' Output a message from the joining thread.
        Console.WriteLine("Hello from the joining thread.")

        ' Output:
        ' Hello from the joining thread.
        ' Hello from taskA. 

            // Create a task and supply a user delegate by using a lambda expression.
            var taskA = new Task(() => Console.WriteLine("Hello from taskA."));

            // Start the task.
            taskA.Start();

            // Output a message from the joining thread.
            Console.WriteLine("Hello from the calling thread.");


            /* Output:
             * Hello from the joining thread.
             * Hello from taskA. 
             */

您也可以使用 StartNew 方法直接以一道作業建立並啟動工作。 如果不需要將建立和排程步驟分開,則這是建立並啟動工作較好的方式,如下列範例所示。

' Better: Create and start the task in one operation.
Dim taskA = Task.Factory.StartNew(Sub() Console.WriteLine("Hello from taskA."))

' Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.")
// Create and start the task in one operation.
var taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from taskA."));

// Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.");

Task 會公開靜態 Factory 屬性,這個屬性會傳回預設的 TaskFactory 執行個體,讓您以 Task.Factory.StartNew(…) 的形式呼叫這個方法。 另外,在這個範例中,每個工作都屬於 System.Threading.Tasks.Task<TResult> 型別,因此都具有包含計算結果的公用 Result 屬性。 這些工作會以非同步方式執行,且可能以任何順序完成。 如果在計算完成之前就存取 Result 屬性,則這個屬性會封鎖執行緒,直到有值為止。

Dim taskArray() = {Task(Of Double).Factory.StartNew(Function() DoComputation1()),
                   Task(Of Double).Factory.StartNew(Function() DoComputation2()),
                   Task(Of Double).Factory.StartNew(Function() DoComputation3())}


Dim results() As Double
ReDim results(taskArray.Length)
For i As Integer = 0 To taskArray.Length
    results(i) = taskArray(i).Result
Next
Task<double>[] taskArray = new Task<double>[]
   {
       Task<double>.Factory.StartNew(() => DoComputation1()),

       // May be written more conveniently like this:
       Task.Factory.StartNew(() => DoComputation2()),
       Task.Factory.StartNew(() => DoComputation3())                
   };

double[] results = new double[taskArray.Length];
for (int i = 0; i < taskArray.Length; i++)
    results[i] = taskArray[i].Result;

如需詳細資訊,請參閱 HOW TO:傳回工作的值

當您使用 Lambda 運算式建立工作的委派時,可以存取原始程式碼中該時間點可見的所有變數。 不過,在某些情況下 (特別是在迴圈內),Lambda 擷取的變數不是預期的變數。 它只會擷取最後的值,而不是在每次反覆運算後變動的值。 您可以透過工作建構函式提供狀態物件給工作,存取每次反覆運算的值,如下列範例所示:


    Class MyCustomData

        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Sub TaskDemo2()
        ' Create the task object by using an Action(Of Object) to pass in custom data
        ' in the Task constructor. This is useful when you need to capture outer variables
        ' from within a loop. 
        ' As an experiement, try modifying this code to capture i directly in the lamda,
        ' and compare results.
        Dim taskArray() As Task
        ReDim taskArray(10)
        For i As Integer = 0 To taskArray.Length - 1
            taskArray(i) = New Task(Sub(obj As Object)
                                        Dim mydata = CType(obj, MyCustomData)
                                        mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId
                                        Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
                                                          mydata.Name, mydata.CreationTime, mydata.ThreadNum)
                                    End Sub,
            New MyCustomData With {.Name = i, .CreationTime = DateTime.Now.Ticks}
            )
            taskArray(i).Start()
        Next

    End Sub


       class MyCustomData
       {
        public long CreationTime;
        public int Name;
        public int ThreadNum;
        }

    void TaskDemo2()
    {
        // Create the task object by using an Action(Of Object) to pass in custom data
        // in the Task constructor. This is useful when you need to capture outer variables
        // from within a loop. As an experiement, try modifying this code to 
        // capture i directly in the lambda, and compare results.
        Task[] taskArray = new Task[10];

        for(int i = 0; i < taskArray.Length; i++)
        {
            taskArray[i] = new Task((obj) =>
                {
                                        MyCustomData mydata = (MyCustomData) obj;
                                        mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                        Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
                                                          mydata.Name, mydata.CreationTime, mydata.ThreadNum)
                },
            new MyCustomData () {Name = i, CreationTime = DateTime.Now.Ticks}
            );
            taskArray[i].Start();
        }
    }

這個狀態會做為引數傳遞給工作委派,而且可經由 AsyncState 屬性從工作物件存取。 此外,在某些情況下,透過建構函式傳入資料可能會獲得一些效能上的好處。

工作 ID

每個工作都會得到一個整數 ID,這個 ID 負責在應用程式定義域中唯一識別該工作,且可以經由 Id 屬性來存取。 當要在 Visual Studio 偵錯工具的 [平行堆疊] 和 [平行工作] 視窗中檢視工作資訊時,這個 ID 很有用。 ID 是採延遲建立的方式,也就是說,要等到收到要求後才會建立 ID。因此,每次執行程式時,工作都可能會有不同的 ID。 如需在偵錯工具中檢視工作 ID 的詳細資訊,請參閱 使用平行堆疊視窗

工作建立選項

大多數建立工作的 API 都會提供用來接受 TaskCreationOptions 參數的多載。 指定其中一個選項,即等於在指示工作排程器要如何在執行緒集區上排定工作。 下表列出各種工作建立選項。

項目

說明

None

未指定選項時的預設選項。 排程器會使用其預設的啟發式來排定工作。

PreferFairness

指定排定工作時,應該讓較早建立的工作較早執行,並讓較晚建立的工作較晚執行。

LongRunning

指定工作表示一項長時間執行的作業。

AttachedToParent

指定應該將工作建立為目前工作 (如果有的話) 的子系。 如需詳細資訊,請參閱巢狀工作和子工作

這些選項可以透過位元 OR 運算結合在一起。 下列範例顯示具有 LongRunningPreferFairness 選項的工作。


Dim task3 = New Task(Sub() MyLongRunningMethod(),
                        TaskCreationOptions.LongRunning Or TaskCreationOptions.PreferFairness)
task3.Start()
var task3 = new Task(() => MyLongRunningMethod(),
                    TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();

建立工作接續

Task.ContinueWith 方法和 Task<TResult>.ContinueWith 方法可讓您指定在「前項工作」(Antecedent Task) 完成時要啟動的工作。 接續工作的委派會收到前項工作的參考,藉此檢查前項工作的狀態。 此外,前項工作可以利用 Result 屬性,將使用者定義的值傳遞給接續工作,讓前項工作的輸出成為接續工作的輸入。 在下列範例中,會先由程式碼啟動 getData,當 getData 完成時會自動啟動 analyzeData,而當 analyzeData 完成時就會啟動 reportData。 getData 會產生位元組陣列做為結果,而這個結果陣列會傳入至 analyzeData 中。 analyzeData 會處理該陣列並傳回結果,這個結果的型別是從 Analyze 方法的傳回型別推斷而來。 reportData 會接受來自 analyzeData 的輸入並產生結果,這個結果的型別也是以類似方式推斷而來,且這個結果會放在 Result 屬性中讓程式取得。

        Dim getData As Task(Of Byte()) = New Task(Of Byte())(Function() GetFileData())
        Dim analyzeData As Task(Of Double()) = getData.ContinueWith(Function(x) Analyze(x.Result))
        Dim reportData As Task(Of String) = analyzeData.ContinueWith(Function(y As Task(Of Double)) Summarize(y.Result))

        getData.Start()

        System.IO.File.WriteAllText("C:\reportFolder\report.txt", reportData.Result)

            Task<byte[]> getData = new Task<byte[]>(() => GetFileData());
            Task<double[]> analyzeData = getData.ContinueWith(x => Analyze(x.Result));
            Task<string> reportData = analyzeData.ContinueWith(y => Summarize(y.Result));

            getData.Start();

            //or...
            Task<string> reportData2 = Task.Factory.StartNew(() => GetFileData())
                                        .ContinueWith((x) => Analyze(x.Result))
                                        .ContinueWith((y) => Summarize(y.Result));

            System.IO.File.WriteAllText(@"C:\reportFolder\report.txt", reportData.Result);



ContinueWhenAllContinueWhenAny 方法可讓您從多個工作繼續執行。 如需詳細資訊,請參閱接續工作HOW TO:使用接續鏈結多個工作

建立中斷連結的巢狀工作

當在工作內執行的使用者程式碼建立新的工作,卻未指定 AttachedToParent 選項時,新的工作不會以任何特殊方式與外層工作同步。 這類工作稱為「中斷連結的巢狀工作」(Detached Nested Task)。 下列範例顯示一個工作如何建立一個中斷連結的巢狀工作。

Dim outer = Task.Factory.StartNew(Sub()
                                      Console.WriteLine("Outer task beginning.")
                                      Dim child = Task.Factory.StartNew(Sub()
                                                                            Thread.SpinWait(5000000)
                                                                            Console.WriteLine("Detached task completed.")
                                                                        End Sub)
                                  End Sub)
outer.Wait()
Console.WriteLine("Outer task completed.")

' Output:
'     Outer task beginning.
'     Outer task completed.
'    Detached child completed.
            var outer = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Outer task beginning.");

                var child = Task.Factory.StartNew(() =>
                {
                    Thread.SpinWait(5000000);
                    Console.WriteLine("Detached task completed.");
                });

            });

            outer.Wait();
            Console.WriteLine("Outer task completed.");

            /* Output:
                Outer task beginning.
                Outer task completed.
                Detached task completed.

             */

請注意,外部工作不會等候巢狀工作完成。

建立子工作

當在工作內執行的使用者程式碼以 AttachedToParent 選項建立新的工作時,新的工作就是原始工作 (稱為父工作) 的子工作。 您可以使用 AttachedToParent 選項來呈現結構化工作平行處理原則,因為父工作會隱含等候所有子工作都完成。 下列範例顯示一個工作如何建立一個子工作:

Dim parent = Task.Factory.StartNew(Sub()
                                       Console.WriteLine("Parent task beginning.")
                                       Dim child = Task.Factory.StartNew(Sub()
                                                                             Thread.SpinWait(5000000)
                                                                             Console.WriteLine("Attached child completed.")
                                                                         End Sub,
                                                                         TaskCreationOptions.AttachedToParent)

                                   End Sub)
outer.Wait()
Console.WriteLine("Parent task completed.")

' Output:
'     Parent task beginning.
'     Attached child completed.
'     Parent task completed.
var parent = Task.Factory.StartNew(() =>
{
    Console.WriteLine("Parent task beginning.");

    var child = Task.Factory.StartNew(() =>
    {
        Thread.SpinWait(5000000);
        Console.WriteLine("Attached child completed.");
    }, TaskCreationOptions.AttachedToParent);

});

parent.Wait();
Console.WriteLine("Parent task completed.");

/* Output:
    Parent task beginning.
    Attached task completed.
    Parent task completed.
 */

如需詳細資訊,請參閱巢狀工作和子工作

等候工作完成

System.Threading.Tasks.Task 型別和 System.Threading.Tasks.Task<TResult> 型別提供 Task.WaitTask<TResult>.Wait 方法的數個多載,這些多載可讓您等候工作完成。 此外,靜態 Task.WaitAll 方法和 Task.WaitAny 方法的多載可讓您等候工作陣列中的任一工作或所有工作完成。

您通常會因為下列其中一個原因而等候工作完成:

  • 主執行緒需要有工作的最終計算結果才能完成。

  • 您必須處理可能會從工作擲回的例外狀況。

下列範例顯示不含例外處理的基本模式。

Dim tasks() =
{
    Task.Factory.StartNew(Sub() MethodA()),
    Task.Factory.StartNew(Sub() MethodB()),
    Task.Factory.StartNew(Sub() MethodC())
}

' Block until all tasks complete.
Task.WaitAll(tasks)

' Continue on this thread...
Task[] tasks = new Task[3]
{
    Task.Factory.StartNew(() => MethodA()),
    Task.Factory.StartNew(() => MethodB()),
    Task.Factory.StartNew(() => MethodC())
};

//Block until all tasks complete.
Task.WaitAll(tasks);

// Continue on this thread...

如需示範例外處理的範例,請參閱 HOW TO:處理工作擲回的例外狀況

有些多載可讓您指定逾時,有些多載則額外接受一個 CancellationToken 做為輸入參數,讓等候作業本身也可以取消 (不管是透過程式設計還是做為對使用者輸入的回應)。

等候某項工作完成時,也會隱含等待該工作之所有以 TaskCreationOptions AttachedToParent 選項建立的子系完成。 如果工作已完成,則 Task.Wait 會立即傳回。 Wait 方法會擲回工作所引發的任何例外狀況,即使 Wait 方法是在工作完成之後才呼叫也一樣。

如需詳細資訊,請參閱 HOW TO:等候一個或多個工作完成

處理工作中的例外狀況

當工作擲回一個或多個例外狀況時,這些例外狀況會包裝在一個 AggregateException 中。 接著該例外狀況就會回傳給與工作聯結的執行緒,這通常會是正在等候該工作完成的執行緒,或是嘗試存取該工作之 Result 屬性的執行緒。 這個行為有助於落實這樣的 .NET Framework 原則:即所有未處理的例外狀況預設都應該要讓處理序終止。 呼叫端程式碼可以藉由使用 WaitWaitAllWaitAny 方法,或是一個或多個工作上的 Result() 屬性,並將 Wait 方法置於 try-catch 區塊內,以處理例外狀況。

聯結的執行緒也可以藉由在工作被進行記憶體回收之前就存取 Exception 屬性,以處理例外狀況。 存取這個屬性,可防止未處理的例外狀況觸發當物件完成時,會讓處理序終止的例外狀況傳播行為。

如需例外狀況和工作的詳細資訊,請參閱例外處理 (工作平行程式庫)HOW TO:處理工作擲回的例外狀況

取消工作

Task 類別支援合作式取消,且與 .NET Framework 4 版中的兩個新類別:System.Threading.CancellationTokenSource 類別和 System.Threading.CancellationToken 類別完全整合。 System.Threading.Tasks.Task 類別中的許多建構函式都接受 CancellationToken 做為輸入參數。 許多 StartNew 多載也都接受 CancellationToken

您可以先建立語彙基元,之後再使用 CancellationTokenSource 類別來發出取消要求。 請將語彙基元當做引數傳遞給 Task,並同時在您的使用者委派 (負責完成回應取消要求的動作) 中參考同一個語彙基元。 如需詳細資訊,請參閱工作取消HOW TO:取消工作及其子系

TaskFactory 類別

TaskFactory 類別提供靜態方法,這些方法會封裝在建立和啟動工作及接續工作時常用的一些模式。

Task 類別或 Task<TResult> 類別上會以靜態屬性的形式提供預設的 TaskFactory 供人存取。 您也可以直接執行個體化 TaskFactory 並指定各種選項,包括 CancellationTokenTaskCreationOptions 選項、TaskContinuationOptions 選項或 TaskScheduler。 您在建立工作 Factory 時指定的任何選項都會套用至此 Factory 建立的所有工作,除非工作是以 TaskCreationOptions 列舉建立,在此情況下,工作的選項會覆寫工作 Factory 的選項。

不含委派的工作

在某些情況下,您可能需要使用 Task 來封裝由外部元件 (而非您自己的使用者委派) 所執行的一些非同步作業。 如果作業是根據非同步程式設計模型 Begin/End 模式,您可以使用 FromAsync 方法。 如果不是根據這個模式,您可以使用 TaskCompletionSource<TResult> 物件將作業包裝在工作中,進而享有利用 Task 撰寫程式的一些優點,例如支援例外狀況傳播和接續。 如需詳細資訊,請參閱 TaskCompletionSource<TResult>

自訂排程器

大部分的應用程式或程式庫開發人員並不在意工作會在哪一個處理器上執行、工作會如何將自己的成品與其他工作同步,或是工作會如何排定在 System.Threading.ThreadPool 上。 他們只要求工作能夠在主機電腦上盡可能有效率地執行。 如果您需要進一步控制排程細節,工作平行程式庫可讓您設定預設工作排程器上的某些設定,甚至可讓您提供自訂的排程器。 如需詳細資訊,請參閱 TaskScheduler

相關資料結構

TPL 提供數個新的公用型別,這些型別在平行處理和序列處理情節中會很有用。 這些型別包括 System.Collections.Concurrent 命名空間中數個具備執行緒安全、快速和可擴充的集合類別,以及數個新的同步處理型別 (例如 SemaphoreLock 和 System.Threading.ManualResetEventSlim),這些型別在特定類型的工作負載上表現的比它們的前身更有效率。 .NET Framework 4 版中還有其他新的型別 (例如 System.Threading.BarrierSystem.Threading.SpinLock) 可提供舊版所沒有的功能。 如需詳細資訊,請參閱適用於平行程式設計的資料結構

自訂工作類型

建議您不要繼承自 System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult>, 請改用 AsyncState 屬性,建立其他資料或狀態與 TaskTask<TResult> 物件的關聯。 您也可以使用擴充方法,擴充 TaskTask<TResult> 類別的功能。 如需擴充方法的詳細資訊,請參閱擴充方法 (C# 程式設計手冊)擴充方法 (Visual Basic)

如果必須繼承自 TaskTask<TResult>,不可以使用 System.Threading.Tasks.TaskFactorySystem.Threading.Tasks.TaskFactory<TResult>System.Threading.Tasks.TaskCompletionSource<TResult> 類別建立自訂工作類型執行個體,因為這些類別只會建立 TaskTask<TResult> 物件。 此外,也不可以使用 TaskTask<TResult>TaskFactoryTaskFactory<TResult> 所提供的工作接續機制來建立自訂工作類型執行個體,因為這些機制也是只建立 TaskTask<TResult> 物件。

相關主題

標題

描述

接續工作

說明接續的運作方式。

巢狀工作和子工作

說明子工作與巢狀工作的差異。

工作取消

說明 Task 類別內建的取消支援。

例外處理 (工作平行程式庫)

說明並行執行緒上發生例外狀況時的處理方式。

HOW TO:使用 Parallel.Invoke 執行平行作業

說明如何使用 Invoke

HOW TO:傳回工作的值

說明如何從工作傳回值。

HOW TO:等候一個或多個工作完成

說明如何等候工作完成。

HOW TO:取消工作及其子系

說明如何取消工作。

HOW TO:處理工作擲回的例外狀況

說明如何處理工作擲回的例外狀況。

HOW TO:使用接續鏈結多個工作

說明如何在某項工作完成時執行另一項工作。

HOW TO:使用平行工作周遊二進位樹狀結構

說明如何使用工作,在二進位樹狀目錄中周遊。

資料平行處理原則 (工作平行程式庫)

說明如何使用 ForForEach 建立資料的平行迴圈。

以 .NET Framework 進行平行程式設計

.NET 平行程式設計的最上層節點。

請參閱

概念

以 .NET Framework 進行平行程式設計

變更記錄

日期

記錄

原因

2011 年 3 月

加入如何繼承自 TaskTask<TResult> 類別的資訊。

資訊加強。