Tworząc nowe zadania (wątki) za pomocą TPL, możemy przekazać parametry AttachedToParent lub DenyChildAttach. Określają one, czy wątek powinien być podłączony do rodzica czy nie. W dzisiejszym wpisie postaram wyjaśnić się, czym one różnią się.
Parametry definiują relację wątku z nadrzędnym wątkiem. Jeśli wątek A, tworzy kolejny wątek B, wtedy za pomocą powyższych wartości możemy określić relacje wątku B z A.
Spróbujmy zatem wyjaśnić jak ta relacja wpływa na zachowanie wątków.
Jeśli wątek B(podrzędny) jest podłączony do rodzica (AttachedToParent), to wątek A (macierzysty) zawsze będzie czekał na wykonanie B. Innymi słowy, wątek A nie zostanie uznany za zakończony, dopóki nie skończy działania wątek B. Najpierw uruchomimy kod, z opcją DenyChildAttach:
var parent = Task.Factory.StartNew( () => { Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("B"); }, TaskCreationOptions.DenyChildAttach); }); parent.Wait(); Console.WriteLine("Parent thread finished."); Console.ReadLine();
Na ekranie prawdopodobnie zobaczymy następującą sekwencję:
1. Parent thread finished.
2. B
Wątek macierzysty (parent), nie czeka na skończenie wątku B. Wywołanie Result albo Wait, przestanie blokować jak tylko wykona się wątek główny, bez wątków zagnieżdżonych.
Zmieńmy teraz na AttachedToParent:
var parent = Task.Factory.StartNew( () => { Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("B"); }, TaskCreationOptions.AttachedToParent); }); parent.Wait(); Console.WriteLine("Parent thread finished."); Console.ReadLine();
Kolejność oczywiście będzie odwrotna, a mianowicie:
1. B
2. Parent thread finished.
Kolejna różnica wynika z obsługi błędów. Rozważmy następujący kod:
var parent = Task.Factory.StartNew( () => { Task.Factory.StartNew(() => { Thread.Sleep(1000); throw new Exception("B"); }, TaskCreationOptions.AttachedToParent); }); parent.Wait(); Console.WriteLine("Parent thread finished."); Console.ReadLine();
Wywołanie Task.Wait (powyższy przykład), albo Task.Result spowoduje wychwycenie wyjątku wyrzuconego przez podrzędne wątki. W przypadku DetachedChild, musielibyśmy napisać obsługę błędu w ciele wątku macierzystego ponieważ Wait\Result wyrzuci wyłącznie wyjątek, jeśli był on wyrzucony bezpośrednio w wątku macierzystym.
Kolejna różnica to anulowanie wątków. Zacznijmy od przypadku, kiedy wątek podrzędny typu Attached, anuluje wykonanie:
var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var parent = Task.Factory.StartNew( () => { Task.Factory.StartNew(() => { Thread.Sleep(1000); token.ThrowIfCancellationRequested(); }, token, TaskCreationOptions.AttachedToParent, TaskScheduler.Default); }, token); Thread.Sleep(5); parent.Wait(token); parent.Wait(); Console.WriteLine(parent.Status); Console.ReadLine();
W takim przypadku, wyjątek (TaskCancellationException) zostanie przekazany macierzystemu wątkowi, co oznacza, że Wait na głównym wątku wyrzuci TaskCancellationException.
Nieco inaczej sytuacja wygląda w przypadku wątków Detached:
var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var parent = Task.Factory.StartNew( () => { Task.Factory.StartNew(() => { Thread.Sleep(1000); token.ThrowIfCancellationRequested(); }, token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); }, token); Thread.Sleep(5); tokenSource.Cancel(); parent.Wait(token); Console.WriteLine(parent.Status); Console.ReadLine();
Jeśli wątek macierzysty kończy się przed anulowaniem wątku podrzędnego, wtedy TaskCancellationException nie zostanie wyrzucony przy wykonywaniu Wait. Wynika to z faktu przedstawionego na początku wpisu – wątek macierzysty nie czeka na wątki podrzędne, zatem taka informacja jest po prostu niedostępna.
Jeśli wątek podrzędny zostanie anulowany przed zakończeniem się wątku macierzystego, to TaskCancellationException zostanie wyrzucony, pomimo tego, że jest to wątek Detached:
var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var parent = Task.Factory.StartNew( () => { Task.Factory.StartNew(() => { token.ThrowIfCancellationRequested(); }, token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); Thread.Sleep(100); }, token); tokenSource.Cancel(); parent.Wait(token); Console.WriteLine(parent.Status); Console.ReadLine();
Mam nadzieję, że powyższe przykłady były przydatne i rozjaśniły temat, a nie jeszcze bardzo skomplikowały go 🙂