W .NET 4.5 pojawiła się metoda Task.Run. Z przyzwyczajenia jednak przez długi czas używałem tylko Task.Factory.StartNew. Obie metody służą do stworzenia nowego wątku i natychmiastowego jego uruchomienia. Sposób wywołania wygląda bardzo podobnie:
var t1=Task.Run(()=>Method()); var t2 = Task.Factory.StartNew(Method);
Zajrzyjmy do zdekompilowanego kodu Task.Run:
[__DynamicallyInvokable] [MethodImpl(MethodImplOptions.NoInlining)] public static Task Run(Action action) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; return Task.InternalStartNew((Task) null, (Delegate) action, (object) null, new CancellationToken(), TaskScheduler.Default, TaskCreationOptions.DenyChildAttach, InternalTaskOptions.None, ref stackMark); }
Oznacza to, że Task.Run to nic innego jak:
var t2 = Task.Factory.StartNew(Method, CancellationToken.None,TaskCreationOptions.DenyChildAttach,TaskScheduler.Default);
Spróbujmy jednak rozszyfrować co powyższe parametry oznaczają. W przypadku CancellationToken.None sprawa jest oczywista – po prostu nie przekazujemy własnego tokena.
Następny parametr to TaskCreationOptions.DenyChildAttach. Opisałem go w poprzednim w poście w szczegółach – służy do tworzenia wątków macierzystych, które są po prostu niezależne od pozostałych wątków.
TaskScheduler.Default wymaga więcej uwagi. Często przekazuje się TaskScheduler.Default albo TaskScheduler.Current. Pierwszy z nich zwraca domyślny scheduler, drugi z kolei aktualny czyli ten ustawiony w danym wątku. Zaglądając do TaskScheduler.Current zobaczymy
public static TaskScheduler Current { get { Task currentTask = Task.InternalCurrent; if (currentTask != null) { return currentTask.ExecutingTaskScheduler; } else { return TaskScheduler.Default; } } }
Innymi słowy, jeśli aktualny wątek nie ma żadnego schedulera, wtedy TaskScheduler.Default zostanie zwrócony. Wynika z tego, że TaskScheduler.Current i TaskScheduler.Default mają różne wartości, wyłącznie gdy aktualny wątek ma już ustawiony jakiś scheduler.
Default oznacza, że następujący obiekt zostanie zwrócony:
private static TaskScheduler s_defaultTaskScheduler = new ThreadPoolTaskScheduler();
Klasa ThreadPoolTaskScheduler używa standardowej puli wątków zaimplementowanej w .NET. Domyślny scheduler, to ten operujący bezpośrednio na puli wątków .NET. Warto pamiętać, że ta pula używa wewnętrznie dwóch typów kolejek do przechowywania wątków. GlobalQueue zawiera referencje do wątków macierzystych (patrz poprzedni wpis). LocalQueue zawiera z kolei wątki stworzone w kontekście innych wątków. Wynika to z właściwości opisanych w poprzednim wpisie i z tego względu, LocalQueue jest powiązany z wątkiem macierzystym.
Kiedy zatem chcemy używać Default lub Current? Klasycznym jest odświeżanie interfejsu. Zwykle pierwszy wątek będzie wykonywał czasochłonną operację, a potem chcemy wykonać zadanie na wątku UI, w celu odświeżenia interfejsu. Przykład:
Task.Factory.StartNew(RunLongLastingOperation, CancellationToken.None, TaskCreationOptions.DenyChildAttach,TaskScheduler.Default). ContinueWith(UpdateUI, TaskScheduler.FromCurrentSynchronizationContext());
RunLongLastingOperation będzie wykonana na wątku z puli. Potem, UpdateUI użyje schedulera bazującego na kontekście synchronizacyjnym, czyli umieści zadanie na wątku UI. Następnie załóżmy, że w UpdateUI tworzymy kolejny wątek za pomocą TaskScheduler.Current:
private void UpdateUI() { Task.Factory.StartNew(AnotherOperation,CancellationToken.None, TaskCreationOptions.DenyChildAttach,TaskScheduler.Current); }
AnotherOperation w tym przypadku, zostanie wykonany na tym samym wątku co UpdateUI, czyli wątku UI. Jeśli chcemy mieć serie typu czasochłonna operacja, aktualizacja UI i znów czasochłonna operacja, nie możemy korzystać z TaskScheduler.Current bo w powyższym przypadku będzie to po prostu wątek UI. Z tego względu, musimy skorzystać z puli wątków czyli TaskScheduler.Default:
private void UpdateUI() { Task.Factory.StartNew(AnotherOperation,CancellationToken.None, TaskCreationOptions.DenyChildAttach,TaskScheduler.Default); }
Co nam daje zatem Task.Run? Tworzy on zawsze wątek na puli (TaskScheduler.Default), który jest niezależny od rodzica(DenyChildAttach). Jeśli chcemy stworzyć wątek, który coś robi w tle, wtedy Task.Run jest naturalnym wyborem. W przypadku Task.Factory.StartNew zostanie przekazany domyślnie TaskCreationOptions.None, co spowoduje, że wątek nie jest niezależny.
W praktyce, Task.Run prawdopodobnie powinien być najczęściej wykorzystywany. Z tego względu, twórcy .NET dodali skrót w formie Task.Run do najczęściej wykorzystywanych parametrów.
Dlaczego Task.Run powinien być najczęściej wykorzystywany? Dlaczego mielibyśmy chcieć tworzyć same wątki niezwiązane z wątkiem – rodzicem?