Dołączone i odłączone zadania podrzędne
Zadanie podrzędne (lub zagnieżdżone zadanie) jest wystąpieniem System.Threading.Tasks.Task utworzonym w delegacie użytkownika innego zadania, nazywanego zadaniem nadrzędnym . Podrzędne zadanie może być odłączone lub dołączone. odłączone zadanie potomne to zadanie, które wykonuje się niezależnie od zadania nadrzędnego. Zadanie podrzędne dołączone jest zagnieżdżonym zadaniem utworzonym z opcją TaskCreationOptions.AttachedToParent, której nadrzędny element nie zabrania jawnie ani domyślnie jego dołączania. Zadanie może utworzyć dowolną liczbę dołączonych i odłączonych zadań podrzędnych, co ogranicza jedynie dostępność zasobów systemowych.
W poniższej tabeli wymieniono podstawowe różnice między dwoma rodzajami zadań podrzędnych.
Kategoria | Odłączone zadania podrzędne | Dołączone zadania podrzędne |
---|---|---|
Rodzic czeka na zakończenie zadań dzieci. | Nie. | Tak |
Rodzic propaguje wyjątki zgłaszane przez zadania podrzędne. | Nie. | Tak |
Status elementu nadrzędnego zależy od statusu elementu podrzędnego. | Nie. | Tak |
W większości scenariuszy zalecamy używanie odłączonych zadań podrzędnych, ponieważ ich relacje z innymi zadaniami są mniej złożone. Dlatego zadania utworzone wewnątrz zadań nadrzędnych są domyślnie odłączane i należy jawnie określić opcję TaskCreationOptions.AttachedToParent, aby utworzyć dołączone zadanie podrzędne.
Odłączone zadania podrzędne
Chociaż zadanie podrzędne jest tworzone przez zadanie nadrzędne, domyślnie jest niezależne od zadania nadrzędnego. W poniższym przykładzie zadanie nadrzędne tworzy jedno proste zadanie podrzędne. Jeśli uruchomisz przykładowy kod wiele razy, możesz zauważyć, że dane wyjściowe z przykładu różnią się od pokazanego, a także że dane wyjściowe mogą ulec zmianie za każdym razem, gdy uruchomisz kod. Dzieje się tak, ponieważ zadania nadrzędne i podrzędne są wykonywane niezależnie od siebie, a zadanie podrzędne jest zadaniem odłączonym. Przykład oczekuje tylko na ukończenie zadania nadrzędnego, a podrzędne zadanie może nie zostać wykonane lub ukończone przed zakończeniem działania aplikacji konsolowej.
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.
Jeśli zadanie podrzędne jest reprezentowane przez obiekt Task<TResult>, a nie obiekt Task, możesz upewnić się, że zadanie nadrzędne będzie czekać na ukończenie podrzędnego, uzyskując dostęp do właściwości Task<TResult>.Result zadania podrzędnego, nawet jeśli jest to odłączone zadanie podrzędne. Właściwość Result blokuje się do momentu zakończenia zadania, jak pokazano w poniższym przykładzie.
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
Dołączone zadania podrzędne
Zadania podrzędne, w przeciwieństwie do odłączonych, są ściśle synchronizowane z rodzicem. W poprzednim przykładzie można zmienić odłączone zadanie podrzędne na dołączone zadanie podrzędne przy użyciu opcji TaskCreationOptions.AttachedToParent w instrukcji tworzenia zadania, jak pokazano w poniższym przykładzie. W tym kodzie dołączone zadanie podrzędne zostanie ukończone przed zadaniem nadrzędnym. W rezultacie dane wyjściowe z przykładu są takie same przy każdym uruchomieniu kodu.
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.
Za pomocą dołączonych zadań podrzędnych można tworzyć ściśle synchronizowane wykresy operacji asynchronicznych.
Jednakże zadanie podrzędne może zostać dołączone do swojego elementu nadrzędnego tylko wtedy, gdy element nadrzędny nie zabrania dołączania zadań podrzędnych. Zadania nadrzędne mogą jawnie uniemożliwić dołączanie do nich zadań podrzędnych, określając opcję TaskCreationOptions.DenyChildAttach w konstruktorze klasy zadania nadrzędnego lub metodzie TaskFactory.StartNew. Zadania nadrzędne niejawnie uniemożliwiają dołączanie do nich zadań podrzędnych, jeśli są one tworzone przez wywołanie metody Task.Run. Poniższy przykład ilustruje to. Jest on identyczny z poprzednim przykładem, z tą różnicą, że zadanie nadrzędne jest tworzone przez wywołanie metody Task.Run(Action) zamiast metody TaskFactory.StartNew(Action). Ponieważ zadanie podrzędne nie jest w stanie dołączyć do zadania nadrzędnego, dane wyjściowe z przykładu są nieprzewidywalne. Ponieważ domyślne opcje tworzenia zadań dla przeciążeń Task.Run obejmują TaskCreationOptions.DenyChildAttach, ten przykład jest funkcjonalnym odpowiednikiem pierwszego przykładu w sekcji "Odłączone zadania podrzędne".
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.
Wyjątki w zadaniach podrzędnych
Jeśli odłączone zadania podrzędne zgłaszają wyjątki, te wyjątki muszą być zaobserwowane lub obsłużone bezpośrednio w zadaniu nadrzędnym, tak samo jak w przypadku każdego zadania niezagnieżdżonego. Jeśli dołączone zadanie podrzędne zgłasza wyjątek, wyjątek jest automatycznie propagowany do zadania nadrzędnego i z powrotem do wątku, który czeka lub próbuje uzyskać dostęp do właściwości Task<TResult>.Result zadania. W związku z tym, korzystając z dołączonych zadań podrzędnych, można obsługiwać wszystkie wyjątki w jednym miejscu podczas wywołania Task.Wait w wątku wywołującym. Aby uzyskać więcej informacji, zobacz Obsługa wyjątków.
Anulowanie i zadania podrzędne
Anulowanie zadania wymaga współpracy. Oznacza to, że aby można je było anulować, każde dołączone lub odłączone zadanie podrzędne musi monitorować stan tokenu anulowania. Jeśli chcesz anulować element nadrzędny i wszystkie jego elementy podrzędne przy użyciu jednego żądania anulowania, przekaż ten sam token jako argument do wszystkich zadań i podaj w każdym zadaniu logikę, aby odpowiedzieć na żądanie w każdym zadaniu. Aby uzyskać więcej informacji, zobacz Anulowanie zadania i Jak anulować zadanie i jego podzadania.
Kiedy rodzic anuluje
Jeśli element nadrzędny anuluje się przed uruchomieniem zadania podrzędnego, element podrzędny nigdy się nie uruchamia. Jeśli element nadrzędny anuluje się po tym, jak jego zadanie podrzędne już się rozpoczęło, podrzędne zadanie działa do ukończenia, chyba że ma własną logikę anulowania. Aby uzyskać więcej informacji, zobacz Anulowanie zadania.
Gdy odłączone zadanie podrzędne zostanie anulowane
Jeśli odłączone zadanie podrzędne anuluje się przy użyciu tego samego tokenu, który został przekazany do elementu nadrzędnego, a element nadrzędny nie czeka na zadanie podrzędne, nie jest propagowany wyjątek, ponieważ wyjątek jest traktowany jako łagodne anulowanie współpracy. To zachowanie jest takie samo jak w przypadku każdego zadania najwyższego poziomu.
Kiedy dołączone zadanie podrzędne jest anulowane
Gdy dołączone zadanie podrzędne anuluje się przy użyciu tego samego tokenu, który został przekazany do zadania nadrzędnego, TaskCanceledException jest propagowany do wątku łączącego wewnątrz AggregateException. Musisz poczekać na zadanie nadrzędne, aby można było obsługiwać wszystkie łagodne wyjątki oprócz wszystkich wyjątków błędów, które są propagowane za pomocą grafu dołączonych zadań podrzędnych.
Aby uzyskać więcej informacji, zobacz Obsługi błędów.
Zapobieganie przyłączeniu zadania podrzędnego do zadania nadrzędnego
Nieobsługiwany wyjątek rzucony przez zadanie podrzędne jest propagowany do zadania nadrzędnego. Tego zachowania można użyć do obserwowania wszystkich wyjątków zadań podrzędnych z jednego zadania głównego zamiast przeszukiwania drzewa zadań. Propagacja wyjątków może być jednak problematyczna, gdy zadanie nadrzędne nie oczekuje załącznika z innego kodu. Rozważmy na przykład aplikację, która wywołuje składnik biblioteki trzeciej strony z obiektu Task. Jeśli składnik biblioteki innej firmy tworzy również obiekt Task i określa TaskCreationOptions.AttachedToParent, aby dołączyć go do zadania nadrzędnego, wszelkie nieobsługiwane wyjątki występujące w zadaniu podrzędnym są propagowane do zadania nadrzędnego. Może to prowadzić do nieoczekiwanego zachowania w głównej aplikacji.
Aby zapobiec dołączaniu zadania podrzędnego do zadania nadrzędnego, określ opcję TaskCreationOptions.DenyChildAttach podczas tworzenia obiektu nadrzędnego Task lub Task<TResult>. Gdy zadanie próbuje dołączyć do elementu nadrzędnego, a element nadrzędny określa opcję TaskCreationOptions.DenyChildAttach, zadanie podrzędne nie będzie mogło dołączyć do elementu nadrzędnego i zostanie wykonane tak samo, jakby nie określono opcji TaskCreationOptions.AttachedToParent.
Możesz również uniemożliwić dołączanie zadania podrzędnego do zadania nadrzędnego, gdy zadanie podrzędne nie zostanie ukończone w odpowiednim czasie. Ponieważ zadanie nadrzędne nie zostanie zakończone, dopóki wszystkie zadania podrzędne nie zakończą się, długotrwałe zadanie podrzędne może spowodować, że ogólna aplikacja będzie działać nieprawidłowo. Przykład pokazujący, jak zwiększyć wydajność aplikacji, uniemożliwiając dołączanie zadania podrzędnego do zadania nadrzędnego, zobacz Jak zapobiec dołączaniu zadania podrzędnego do zadania nadrzędnego.
Zobacz też
- Programowanie równoległe
- równoległość danych