附加與中斷連結的子工作
「子工作」(或「巢狀工作」) 是 System.Threading.Tasks.Task 執行個體,它是在另一項工作 (稱為「父工作」) 的使用者委派中建立。 子工作可以中斷連結或附加。 「中斷連結的子工作」(detached child task) 是獨立於其父代而執行的工作。 「附加的子工作」(attached child task) 是巢狀工作,而且是使用 TaskCreationOptions.AttachedToParent 選項所建立,其父代並不明確或預設禁止它附加。 工作可能會建立任意數目的附加和中斷連結的子工作,只受限於系統資源。
下表列出這兩種子工作之間的基本差異。
類別 | 中斷連結的子工作 | 附加的子工作 |
---|---|---|
等候子工作完成的父系。 | No | Yes |
父系會傳播子工作擲回的例外狀況。 | No | Yes |
父系的狀態取決於子系的狀態。 | No | Yes |
在大部分情況下,我們建議您使用中斷連結的子工作,因為它們與其他工作的關聯性較不複雜。 這也就是為什麼在父工作中建立的工作會依預設中斷連結,且您必須明確指定 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("Outer task executing.");
var 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 Example
{
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 {0}.", 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 Example
{
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 的呼叫中,一次處理所有例外狀況。 如需詳細資訊,請參閱例外狀況處理。
取消和子工作
工作取消需要合作。 也就是說,若要能取消,每個附加或中斷連結的子工作必須監視取消語彙基元的狀態。 如果您想要使用一個取消要求來取消父系及其所有子系,您會將相同的語彙基元當做引數傳遞至所有工作,並在每個工作中提供邏輯以回應每個工作中的要求。 如需詳細資訊,請參閱工作取消和如何:取消工作及其子系。
當父系取消時
如果父系在其子工作啟動之前自行取消,則永遠不會啟動子系。 如果父系在子工作已經開始之後自行取消,則子系會執行到完成為止,除非它有自己的取消邏輯。 如需詳細資訊,請參閱 Task Cancellation。
當取消中斷連結的子工作時
如果中斷連結的子工作使用傳遞給父系的相同語彙基元來自行取消,且父系不等候子工作,則不會傳播任何例外狀況,因為例外狀況被視為良性合作取消。 此行為與任何最上層工作相同。
當取消附加的子工作時
當附加的子工作使用傳遞給父工作的相同語彙基元來自行取消時,TaskCanceledException 會傳播到 AggregateException 內部的聯結執行緒。 您必須等候父工作,以便處理所有良性的例外狀況,以及所有在附加的子工作的圖形間向上傳播的錯誤例外狀況。
如需詳細資訊,請參閱例外狀況處理。
防止子工作附加到其父系
子工作所擲回的未處理例外狀況會傳播到父工作。 您可以使用此行為來從一個根工作觀察所有子工作例外狀況,而不必周遊工作樹狀。 不過,當父工作不預期會有來自其他程式碼的附加時,例外狀況傳播可能會造成問題。 例如,假設應用程式從 Task 物件呼叫協力廠商程式庫元件。 如果協力廠商程式庫元件也會建立 Task 物件,並指定 TaskCreationOptions.AttachedToParent 將它附加至父工作,則在子工作中發生任何未處理例外狀況都會傳播到父系。 這可能會導致在主應用程式中非預期的行為。
若要防止子工作附加至其父工作,,當您建立父 Task 或 Task<TResult> 物件時,請指定 TaskCreationOptions.DenyChildAttach 選項。 當工作嘗試附加至其父系,而父系指定 TaskCreationOptions.DenyChildAttach 選項,則子工作將無法附加至父系,並且執行時就如同未指定 TaskCreationOptions.AttachedToParent 選項。
當子工作未及時完成時,您也可能想要防止子工作附加到其父系。 由於父工作會在所有子工作完成後才完成,因此長時間執行的子工作可能造成整個應用程式效能不佳。 如需示範如何藉由防止工作附加至其父工作以改善應用程式效能的範例,請參閱如何:防止子工作附加到其父系。