子工作(或 巢狀工作)是另一個工作的使用者委派中建立的 System.Threading.Tasks.Task 實例,稱為 父工作。 子任務可以是脫離的或連結的。 分離的子工作 是獨立於父工作執行的工作。 附加子工作 是一個巢狀工作,創建時使用 TaskCreationOptions.AttachedToParent 選項,並且其父代並未明確或預設禁止其進行附加。 工作可以建立任何數量的附加和分離的子工作,僅受系統資源限制。
下表列出這兩種子任務的基本差異。
類別 | 獨立執行的子工作 | 附加的子工作 |
---|---|---|
父系會等候子工作完成。 | 否 | 是的 |
父系會傳播子工作擲回的例外狀況。 | 否 | 是的 |
父系的狀態取決於子系的狀態。 | 否 | 是的 |
在大部分情況下,我們建議您使用獨立的子工作,因為它們與其他工作的關聯性較為簡單。 這就是為什麼在父工作內建立的工作通常是分離的,並且您必須明確指定 TaskCreationOptions.AttachedToParent 選項來建立連結的子工作。
獨立執行的子工作
雖然子工作是由父工作所建立,但預設與父工作無關。 在下列範例中,父工作會創建一個簡單的子工作。 如果您多次執行範例程式代碼,您可能會注意到範例的輸出與所示不同,而且每次執行程式碼時,輸出可能會變更。 這是因為父工作和子工作彼此獨立執行,子工作是獨立的工作。 此範例只會等候父工作完成,而且子工作可能不會在控制台應用程式終止之前執行或完成。
using System;
using System.Threading;
using System.Threading.Tasks;
public class Example4
{
public static void Main()
{
Task parent = Task.Factory.StartNew(() =>
{
Console.WriteLine("Outer task executing.");
Task child = Task.Factory.StartNew(() =>
{
Console.WriteLine("Nested task starting.");
Thread.SpinWait(500000);
Console.WriteLine("Nested task completing.");
});
});
parent.Wait();
Console.WriteLine("Outer has completed.");
}
}
// The example produces output like the following:
// Outer task executing.
// Nested task starting.
// Outer has completed.
// Nested task completing.
Imports System.Threading
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim parent = Task.Factory.StartNew(Sub()
Console.WriteLine("Outer task executing.")
Dim child = Task.Factory.StartNew(Sub()
Console.WriteLine("Nested task starting.")
Thread.SpinWait(500000)
Console.WriteLine("Nested task completing.")
End Sub)
End Sub)
parent.Wait()
Console.WriteLine("Outer task has completed.")
End Sub
End Module
' The example produces output like the following:
' Outer task executing.
' Nested task starting.
' Outer task has completed.
' Nested task completing.
如果子工作是由 Task<TResult> 物件表示,而不是 Task 物件,您可以藉由存取 Task<TResult>.Result 屬性,確保父工作會等候子工作完成,即使它是中斷連結的子工作也一樣。 Result 屬性會封鎖直到其工作完成為止,如下列範例所示。
using System;
using System.Threading;
using System.Threading.Tasks;
class Example3
{
static void Main()
{
var outer = Task<int>.Factory.StartNew(() =>
{
Console.WriteLine("Outer task executing.");
var nested = Task<int>.Factory.StartNew(() =>
{
Console.WriteLine("Nested task starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Nested task completing.");
return 42;
});
// Parent will wait for this detached child.
return nested.Result;
});
Console.WriteLine($"Outer has returned {outer.Result}.");
}
}
// The example displays the following output:
// Outer task executing.
// Nested task starting.
// Nested task completing.
// Outer has returned 42.
Imports System.Threading
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim parent = Task(Of Integer).Factory.StartNew(Function()
Console.WriteLine("Outer task executing.")
Dim child = Task(Of Integer).Factory.StartNew(Function()
Console.WriteLine("Nested task starting.")
Thread.SpinWait(5000000)
Console.WriteLine("Nested task completing.")
Return 42
End Function)
Return child.Result
End Function)
Console.WriteLine("Outer has returned {0}", parent.Result)
End Sub
End Module
' The example displays the following output:
' Outer task executing.
' Nested task starting.
' Detached task completing.
' Outer has returned 42
附加的子工作
與分離的子工作不同,連結的子工作會與父系緊密同步。 您可以使用工作建立語句中的 [TaskCreationOptions.AttachedToParent] 選項,將上一個範例中的分離子工作變更為連接子工作,如下列範例所示。 在這段程式碼中,附加的子任務會先於其父任務完成。 因此,每次執行程式代碼時,範例的輸出都會相同。
using System;
using System.Threading;
using System.Threading.Tasks;
public class Example
{
public static void Main()
{
var parent = Task.Factory.StartNew(() => {
Console.WriteLine("Parent task executing.");
var child = Task.Factory.StartNew(() => {
Console.WriteLine("Attached child starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completing.");
}, TaskCreationOptions.AttachedToParent);
});
parent.Wait();
Console.WriteLine("Parent has completed.");
}
}
// The example displays the following output:
// Parent task executing.
// Attached child starting.
// Attached child completing.
// Parent has completed.
Imports System.Threading
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim parent = Task.Factory.StartNew(Sub()
Console.WriteLine("Parent task executing")
Dim child = Task.Factory.StartNew(Sub()
Console.WriteLine("Attached child starting.")
Thread.SpinWait(5000000)
Console.WriteLine("Attached child completing.")
End Sub, TaskCreationOptions.AttachedToParent)
End Sub)
parent.Wait()
Console.WriteLine("Parent has completed.")
End Sub
End Module
' The example displays the following output:
' Parent task executing.
' Attached child starting.
' Attached child completing.
' Parent has completed.
您可以使用附加的子任務來建立異步操作的緊密協同圖形。
不過,只有在父工作不禁止附加子工作時,子工作才能附加至其父工作。 父任務可以透過在父任務的類別建構函式或 TaskCreationOptions.DenyChildAttach 方法中指定 TaskFactory.StartNew 選項,明確地防止子任務附加到它們。 父工作會默默地防止透過呼叫 Task.Run 方法創建的子工作附加至它們。 下列範例說明這點。 這與上一個範例相同,不同之處在於父工作是藉由呼叫 Task.Run(Action) 方法而不是 TaskFactory.StartNew(Action) 方法所建立。 因為子任務無法附加到父任務,因此範例的輸出是無法預測的。 由於 Task.Run 多載的預設工作建立選項包含 TaskCreationOptions.DenyChildAttach。因此,此範例的功能等同於「獨立子工作」一節中的第一個範例。
using System;
using System.Threading;
using System.Threading.Tasks;
public class Example2
{
public static void Main()
{
var parent = Task.Run(() => {
Console.WriteLine("Parent task executing.");
var child = Task.Factory.StartNew(() => {
Console.WriteLine("Attached child starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completing.");
}, TaskCreationOptions.AttachedToParent);
});
parent.Wait();
Console.WriteLine("Parent has completed.");
}
}
// The example displays output like the following:
// Parent task executing.
// Parent has completed.
// Attached child starting.
Imports System.Threading
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim parent = Task.Run(Sub()
Console.WriteLine("Parent task executing.")
Dim child = Task.Factory.StartNew(Sub()
Console.WriteLine("Attached child starting.")
Thread.SpinWait(5000000)
Console.WriteLine("Attached child completing.")
End Sub, TaskCreationOptions.AttachedToParent)
End Sub)
parent.Wait()
Console.WriteLine("Parent has completed.")
End Sub
End Module
' The example displays output like the following:
' Parent task executing.
' Parent has completed.
' Attached child starting.
子工作中的例外狀況
如果分離的子任務拋出例外狀況,則必須在父任務中直接捕捉或處理該例外狀況,就像處理任何非巢狀任務一樣。 如果附加的子任務拋出例外狀況,則例外狀況會自動傳播至父任務,並傳回等候或嘗試存取任務 Task<TResult>.Result 屬性的工作執行緒。 因此,藉由使用附屬子任務,您可以在呼叫線程對 Task.Wait 的一次呼叫中集中處理所有例外狀況。 如需詳細資訊,請參閱 例外狀況處理。
取消和子任務
工作取消是合作式的。 也就是說,若要成為可取消的, 每個附加或獨立的子任務都必須監控取消令牌的狀態。 如果您想使用一個取消要求來取消父項及其所有子項,請將相同的令牌作為參數傳遞給所有任務,並在每個任務中提供回應該要求的邏輯。 如需詳細資訊,請參閱 工作取消 和 如何取消工作及其子任務。
父母取消時
如果父任務在啟動子任務之前被取消,那麼子任務將永遠不會啟動。 如果父任務在子任務啟動後取消,除非子任務有自己的取消邏輯,否則子任務會繼續執行直到完成。 如需詳細資訊,請參閱 任務取消。
當分離的子任務取消時
如果脫離的子任務使用傳遞至父任務的相同令牌自行取消,且父任務不會等待子任務,則不會傳播任何例外狀況,因為該例外狀況被視為良性合作的取消行為。 此行為與任何最上層工作的行為相同。
附屬子任務取消時
當附加的子任務使用傳遞給其父任務的相同令牌來取消自身時,會將 TaskCanceledException 傳播至 AggregateException內的連接執行緒。 您必須等候父工作,以便除了透過附加子工作圖表傳播的所有錯誤例外狀況之外,您還可以處理所有良性例外狀況。
如需詳細資訊,請參閱 例外狀況處理。
防止子工作附加至其父任務
子工作擲回的未處理例外狀況會傳播至父工作。 您可以使用此功能來觀察從一個根任務的所有子任務的例外狀況,而不是遍歷任務的樹狀結構。 不過,當父工作不預期會有其他程式碼的附件時,例外傳播可能會有問題。 例如,請考慮一個從 Task 物件呼叫第三方函式庫組件的應用程式。 如果第三方函式庫元件同樣建立 Task 物件,並指定 TaskCreationOptions.AttachedToParent 將其附加到父任務,則在子任務中出現的任何未處理的例外將傳播到父任務。 這可能會導致主要應用程式中發生非預期的行為。
若要防止子工作附加至其父工作,請在建立父系 TaskCreationOptions.DenyChildAttach 或 Task 物件時指定 Task<TResult> 選項。 當任務嘗試連結至其父任務,而父任務指定 TaskCreationOptions.DenyChildAttach 選項時,子任務將無法連結至父任務,並會像未指定 TaskCreationOptions.AttachedToParent 選項那樣執行。
當子工作未能及時完成時,您可能會考慮防止子工作附加至其父工作。 因為父任務要等到所有子任務完成後才會結束,因此運行時間較長的子任務可能會導致整個應用程式效能不佳。 如需範例示範如何藉由防止子任務附加至其父任務來改善應用程式性能,請參閱 如何:防止子任務附加至其父任務。