SynchroNr.3 Pora wejść głębiej w to, jak async i await działa. Po oczywiście pisanie teraz kodu jest łatwiejsze, ale wciąż nie unikniesz problemu z synchronizacją działania w naszej aplikacji.
To jest jedno z tych zagadnienia, co w ogóle robi : ConfigureAwait i dlaczego Visual Studio czasem mi sugeruje, bym ustawił to na false.
Co może być przerażające, gdy zdaje sobie sprawę, że ten mechanizm działa inaczej w zależności od tego, czym nasza aplikacja jest.
Czy jest to konsola? Czy jest to aplikacja WPF, Windows Forms?
Czy jest to aplikacja Web, czyli ASP.NET Core? Czy jest to aplikacja na telefon?
Dawno temu napisałem taki wpis : https://cezarywalenciuk.pl/blog/programing/aysnc-await-wpf-ui
Jego esencją jest ten screen.
Jaka magia się dzieje dokładnie tutaj. Aby to, zrozumieć musimy zgłębić wiele zagadnienie. W następnym wpisie omówimy także Awaitables, aby rozjaśnić sprawę.
W tym wpisie zgłębimy mechanizmy planowania zadań przez naszą aplikację. Ktoś to musi wszystkiego pilnować.
W .NET istnieją aż trzy warstwy , które mogą to robić:
- SychronizactionContext
- TaskScheluder
- ThreadPool
Zacznijmy od SychronizationContext
SychronizactionContext
Mistyczna klasa SychronizactionContext wie "jak" i "gdzie" zakolejkować nasze asynchroniczne "zadanie". On więc jest tym kontrolerem asynchronicznych zadań.
Nasze zadania też po skończeniu swojej pracy chcą wrócić do jakiegoś głównego kontekstu aplikacji. Oczywiście czasami tego nie chcemy i właśnie dlatego ustawiamy ConfigureAwait na false. (o tym później)
Wiem, że brzmi to skomplikowanie, ale wierz mi, definicja SychronizactionContext na MSDN jest jeszcze lepsza.
Każde określenie "await" przechwytuje pewien kontekst synchronizacyjny i używa go, aby powrócić do swojego stanu, który go wywołał.
Mówiłem wcześniej, że każda aplikacja będzie miała swój kontekst i swoje mechanizmy.
Na przykład aplikacja Windows Form korzysta z Control.BeginInvoke. WPF ma Dispatcher.BeginInvoke. Obie te metody pozwalają na wykonanie kodu w kontekście wywoływanego wątku z innego wątku.
SychronizactionContext stara się być przedmiotem takiej operacji. Pomyśl o niej jako klasie abstrakcyjnej, która ma ten sam cel (lokalizowania wywoływanego kontekstu), ale działa trochę inaczej w zależności od Frameworku.
SychronizactionContext posiada w sobie wirtualne metody. Skoncentrujmy się na metodzie "Post" na razie. Post akceptuje delegatę, która jest odpowiedzialna za określenie "gdzie" i "kiedy" uruchomić tą delegatę, czyli wskaźnik do metody.
Domyślna implementacja SynchornizationContext.Post używa ThreadPool.QueueUserWorkItem
public class SychronizationContext
{
public virtual void Post(SendOrPostCallback d, Object state)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}
}
Inna domyślna metoda "Send", wykonuje to samo co metoda Post tylko synchronicznie . Dla domyślnego przykładu ten kod jest banalny.
public class SychronizationContext
{
public virtual void Send(SendOrPostCallback d, Object state)
{
d(state);
}
}
Natomiast dla WindowForm implementacja tej metody Post jest zupełnie inna. Będzie ona znajdować się w klasie WindowsFormsSynchronizationContext. Jej implementacja Post wygląda tak :
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable
{
/// <include file='doc\WindowsFormsSynchronizationContext.uex' path='docs/doc[@for="WindowsFormsSynchronizationContext.Post"]/*' />
public override void Post(SendOrPostCallback d, Object state) {
Debug.Assert(controlToSendTo != null, "Should always have the marshaling control by this point");
if (controlToSendTo != null) {
controlToSendTo.BeginInvoke(d, new object[] { state });
}
}
/// <include file='doc\WindowsFormsSynchronizationContext.uex' path='docs/doc[@for="WindowsFormsSynchronizationContext.CreateCopy"]/*' />
public override SynchronizationContext CreateCopy() {
return new WindowsFormsSynchronizationContext(controlToSendTo, DestinationThread);
}
}
Jak widzisz, Window Form korzysta z BeginInvoke.
A tak wygląda metoda Send w Windows Forms.
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable
{
public override void Send(SendOrPostCallback d, Object state) {
Thread destinationThread = DestinationThread;
if (destinationThread == null || !destinationThread.IsAlive) {
throw new InvalidAsynchronousStateException(SR.GetString(SR.ThreadNoLongerValid));
}
Debug.Assert(controlToSendTo != null, "Should always have the marshaling control by this point");
if (controlToSendTo != null) {
controlToSendTo.Invoke(d, new object[] { state });
}
}
}
WPF natomiast ma DispatcherSynchronizationContext.
WPF korzysta z Dispatcher.BeginInvoke.
Jego metoda Post wygląda tak:
public sealed class DispatcherSynchronizationContext : SynchronizationContext
{
/// <summary>
/// Asynchronously invoke the callback in the SynchronizationContext.
/// </summary>
public override void Post(SendOrPostCallback d, Object state)
{
// Call BeginInvoke with the cached priority. Note that BeginInvoke
// preserves the behavior of passing exceptions to
// Dispatcher.UnhandledException unlike InvokeAsync. This is
// desireable because there is no way to await the call to Post, so
// exceptions are hard to observe.
_dispatcher.BeginInvoke(_priority, d, state);
}
}
A tak jego metoda Send
public sealed class DispatcherSynchronizationContext : SynchronizationContext
{
/// <summary>
/// Synchronously invoke the callback in the SynchronizationContext.
/// </summary>
public override void Send(SendOrPostCallback d, Object state)
{
// Call the Invoke overload that preserves the behavior of passing
// exceptions to Dispatcher.UnhandledException.
if(BaseCompatibilityPreferences.GetInlineDispatcherSynchronizationContextSend() && _dispatcher.CheckAccess())
{
// Same-thread, use send priority to avoid any reentrancy.
_dispatcher.Invoke(DispatcherPriority.Send, d, state);
}
else
{
// Cross-thread, use the cached priority.
_dispatcher.Invoke(_priority, d, state);
}
}
}
Oczywiście możesz czuć się teraz zagubiony. Po co w ogóle "Dispatcher.BeginInvoke" istnieje w WPF i co do ma to do "async i await" oraz planowania zadań asynchronicznych.
Wytłumaczmy raz jeszcze jedna rzecz. Jaki problem "SynchronizationContext" chce rozwiązać. Wiemy, że działanie "SynchronizationContext" będzie zależny od środowiska i słusznie.
W końcu SynchronizationContext reprezentuje obecnie środowisko, na którym nasz kod się uruchamia.W asynchronicznym programie, gdy wysyłamy delegate/zadanie do innego wątku musimy złapać obecne środowisko, na którym się znajdujemy umieścić go do obiektu "SynchronizationContext" i wsadzić go do obiektu Task.
Mamy więc kontekst środowiska i wrzucamy go do innego wątku. To jak inne wątki będą trzymać swoje kontekst, jest zależne od aplikacji. Co ciekawe są frameworki, które tego kontekstu nie wysyłają. ASP.NET CORE nie ma "SynchronizationContext".
Aplikacje konsolowe też nie mają "SynchronizationContext".
Istnieją moim zdaniem dwa powody. Wydajność i prostota aplikacji.
Kiedyś w ASP.NET istniał chwytacz (handler), który łapał każde zapytanie do twojej strony internetowej. On w sobie tworzył kolejkę, ponieważ każde zapytanie musiało czekać na inne wcześniejsze zapytania. Nic nie jest natychmiastowe.
Zapytania do serwera stały więc swojej kolejce. Aby zapytania się nie zgubiły i nie pomieszały, to musiał do nich utworzyć się KONTEKST.
Gdy dane zapytanie było gotowe do uruchomienia przez serwer, to wtedy wątek był pobierany z puli wątków i wchodził on w KONTEKST SWOJEGO ZAPYTANIA. Później on zaczął obsługę swojego zapytania HTTP do serwera i na przykład wyświetla Ci stronę ".aspx".
Oczywiście trzeba było wykonać dużo operacji, aby te ponowne wejście w kontekst zapytania trzymało się kupy. Obiekt "HttpContext.Current" musiał zawierać w sobie informację o obecnym wątku, jak i danej kulturze, w której został uruchomiony.
Teraz w ASP.NET CORE, gdy asynchroniczny chwytacz wznawia swoje zadanie -- wątek jest brany z puli wątków i wykonuje swoje zadanie...bez żadnego wchodzenia ponownie w kontekst swojej kreacji.
ASP.NET CORE nie ma kontekstu, gdyż usunęli oni potrzebę tworzenia go poprzez pozbycie się tej kolejki zapytań na początku całego tego procesu.
Potem programiści stwierdzi, że w sumie ten kontekst nigdzie nie jest potrzebny i operacje asynchroniczne mogą być wszędzie.
Kiedyś aplikacja ASP.NET MVC musiała blokować operację dla filtrów i pod akcji kontrolerów.
W ASP.NET CORE cały potok jest asynchroniczny.
Dotyczy to też operacji "async i await" . Te operacje asynchroniczne też nie mają kontekstu do powrotu. Nie dziw się wiec, gdy ASP.NET CORE nawołuje Cię do korzystania z asynchronicznych metod nawet po stronie stron ".cshtml" Razor.
Nie musisz też korzystać z "ConfigureAwait(false)", chociaż pamiętaj o nim, gdy piszesz bibliotekę, która może być używane nie tylko przez ASP.NET CORE.
Z ciekawości możesz też zobaczyć jak : ASPNETSychronizationContext został napisany w C#
https://referencesource.microsoft.com/#system.web/AspNetSynchronizationContext.cs
Mam nadzieje, że nie musisz przepisywać kodu wielowątkowego z ASP.NET do ASP.NET CORE, bo tutaj może być dużo pułapkę związanych z tym, że ASP.NET CORE nie jest blokowany przez request Context.
private HttpClient _client = new HttpClient();
async Task<List<string>> GetBothAsync(string url1, string url2)
{
var result = new List<string>();
var task1 = GetOneAsync(result, url1);
var task2 = GetOneAsync(result, url2);
await Task.WhenAll(task1, task2);
return result;
}
async Task GetOneAsync(List<string> result, string url)
{
var data = await _client.GetStringAsync(url);
result.Add(data);
}
W ASP.NET HttpClient wykona po kolei operacje asynchroniczne - w jednym wątku - jak w kolejce każda operacja HttpClient będzie czekała na wcześniejszą przez "Request Context".
W ASP.NET CORE obie operacje pobrania zawartości strony WWW przez HttpClient uruchomią się równocześnie, co spowoduje, że twoja lista napisów może wyglądać zupełnie inaczej.
Wracając do "SynchronizationContext". Określa on lokalizację "gdzie" twój kod ma wrócić, gdy wykona swoją operację.
Kiedy przechwytywanie obecnego "SynchronizationContex" jest potrzebne
Dlaczego ten mechanizm jest potrzebny? W sumie pokazałem ci, że w ASP.NET CORE takiego kontekstu nie ma, więc nie musisz sobie zawracać głowy.
W nim istnieje TaskScheduler, który operuje i tak na puli wątków, ale o tym później.
Przykładowo natomiast w WPF powiedzmy, że chcesz wysłać rezultat swojego działania do pola testowego i jego właściwości Text.
Jest jednak tutaj problem. Tylko wątek UI w WPF ma prawo do zmiany tej właściwości w tej kontrolce. Jeżeli spróbujesz coś takiego zrobić to dostaniesz wyjątek "InvalidOperationException".
Podobnie jest w Windows Forms. Kontrolki WPF i Windows Form mogą być modyfikowane tylko przez wątek, które je utworzył. Jest nim główny wątek interfejsu użytkownika.
public static void DoWork()
{
//W wątku UI
var sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate
{
// zrób coś w ThreadPool w innym wątku
sc.Post(delegate
{
// zrób prace na wątku (UI)
}, null);
});
}
Wygląda to skomplikowanie. Na szczęście "async i await" operują na SychronizationContext pod stołem bez naszej wiedzy.
await LetsDoSomethingAsync();
ContinueWithRestOfThisCode();
Ten kod tak naprawdę wygląda tak :
var task = LetsDoSomethingAsync();
var currentSyncContext = SynchronizationContext.Current;
task.ContinueWith(delegate
{
if (currentSyncContext == null) ContinueWithRestOfThisCode();
else currentSyncContext.Post(delegate { ContinueWithRestOfThisCode(); }, null);
}, TaskScheduler.Current);
Co my tutaj widzimy? Pobierasz kontekst swojej aplikacji, a po wykonaniu Task-a korzystamy z metody ContinueWith() i korzystając z tego kontekstu, wykonujemy delegatę, która zrobi coś na wątku UI.
Dla pewności szybko napisałem aplikację w WPF, aby to sprawdzić.
Przy okazji jak widzisz zdarzenia przycisku to jednym "async void", który można zaakceptować.
private async void Button_Click(object sender, RoutedEventArgs e)
{
var i = await LetsDoSomethingAsync();
ContinueWithRestOfThisCode(i);
}
private Task<int> LetsDoSomethingAsync()
{
Task.Delay(1000);
return Task.FromResult(1);
}
private void ContinueWithRestOfThisCode(int i)
{
ResultTextBlock.Text = i.ToString();
}
Z tego, co pamiętam, przed "async i await" pisanie aplikacji WPF wielowątkowych nie było aż takiego przyjemne i teraz wiem dlaczego.
Jak widzisz "async i await" zajmą się powrotem do kontekstu i w sumie mógłbyś nawet nie widzieć, że w WPF istnieje coś takiego jak wątek UI, bo z perspektywy takiego kodu wszystko wygląda prosto.
Niebezpieczeństwo WPF i Windows Forms " SychronizactionContext i .Result
Zapomnijmy na razie o "async i await". Wyobraź sobie sytuacje, w którym chcemy obsłużyć przycisk i pobrać asynchronicznie jakąś stronę internetową i wyświetlić jej zawartość.
https://postman-echo.com/delay/10 To adres testowy, który zwróci zawartość JSON dopiero 10 sekundach.
Jak pamiętasz operacje takie jak "Result" i "Wait" będą blokowały wątek główny, aż do wykonania operacji asynchronicznej . W tym wypadku też tak będzie.
public partial class MainWindow : Window
{
private HttpClient HttpClient = new HttpClient();
private void Button_Click(object sender, RoutedEventArgs e)
{
var result = HttpClient.GetStringAsync("https://postman-echo.com/delay/10").Result;
ResultTextBlock.Text = result;
}
public MainWindow()
{
InitializeComponent();
}
}
}
W WPF i Windows Form tym wątkiem główny jest także wątek UI, a więc cała aplikacja tymczasowo się zawiesi, aż do momentu pobrania strony internetowej.
To, co jest ciekawe przy takiej operacji "SychronizationContext" nie jest używany. Nie skorzystaliśmy tutaj z całej machiny "async i await".
Jest to nie miła sytuacja, ale z drugiej strony nie zrobisz tutaj deadlock-a. (o czym później)
Taki kod najlepiej poprawić na "async i await".
public partial class MainWindow : Window
{
private HttpClient HttpClient = new HttpClient();
private async void Button_Click(object sender, RoutedEventArgs e)
{
var result = await HttpClient.GetStringAsync("https://postman-echo.com/delay/10");
ResultTextBlock.Text = result;
}
public MainWindow()
{
InitializeComponent();
}
}
Jak zrobić deadlock
Bawienie się metodami, które blokują wątki główne jest rzeczywiście niebezpieczne i nie chodzi tutaj tylko o blokadę wątku UI.
Teraz wymieszałem kod synchroniczny z asynchronicznym. Jak jednak zrobiłem tutaj deadlock. Jak to możliwe, że aplikacja się zawiesza na wieczność.
private readonly static string _url = "https://postman-echo.com/delay/10";
private void Button_Click(object sender, RoutedEventArgs e)
{
ResultTextBlock.Text = "";
ResultTextBlock.Text += "Przed MyGetStringAsync()";
var result = MyGetStringAsync().Result;
ResultTextBlock.Text += "Napisze coś w międzyczasie";
ResultTextBlock.Text = result;
ResultTextBlock.Text += "Po MyGetStringAsync()";
}
public async Task<string> MyGetStringAsync()
{
using (HttpClient httpClient = new HttpClient())
{
return await httpClient.GetStringAsync(_url);
}
}
Otóż moja metoda MyGetStringAsync czeka na wątek główny UI, ale z drugiej strony ten sam wątek główny czeka na rezultat mojej metody.
Czyli jakiś cudem stworzyłem sytuacje, w której wątek główny czeka na mój rezultat, ale ten nigdy nie nastąpi, ponieważ moja metoda czeka, aż wątek główny przestanie czekać na mój rezultat.
Kto by pomyślał, że wywoływanie .Result czy Wait może rozwalić cały ten SynchronizationContext. Spróbujmy jednak wyjaśnić, co dokładnie się tutaj stało.
Wiemy, że główny wątek UI czeka na moją metodę asynchroniczną. Na co jednak dokładnie moja metoda asynchroniczna czeka?
SynchronizationContext jest w tym przykładzie używany i to jest problem. Próbuje do niego wrócić, ale nie mogę, bo jest zablokowany. SynchronizationContext wróciłby w tym wypadku do kodu w metodzie "Button_Click" tylko mi to nie jest potrzebne, bo korzystam z właściwości Result.
Pamiętasz ten przykład przed chwilą :
await LetsDoSomethingAsync();
ContinueWithRestOfThisCode();
Powstałą dziwna sytuacja wynikająca z miksowania synchronicznego i asynchronicznego kodu oraz czekania na synchronizację wątku głównego z moim innym wątkiem.
O co chodzi z ConfigureAwait(false)
Powiedzmy, że z jakiego powodu chcesz skorzystać z właściwości Result tak jak w poprzednim przykładzie, ale chcesz się uwolnić od tego deadlock-a.
Na pomoc przychodzi ConfigureAwait(), który
private void Button_Click(object sender, RoutedEventArgs e)
{
ResultTextBlock.Text = "";
ResultTextBlock.Text += "Przed MyGetStringAsync()";
var result = MyGetStringAsync().Result;
ResultTextBlock.Text += "Napisze coś w międzyczasie";
ResultTextBlock.Text = result;
ResultTextBlock.Text += "Po MyGetStringAsync()";
}
public async Task<string> MyGetStringAsync()
{
using (HttpClient httpClient = new HttpClient())
{
return await httpClient.GetStringAsync(_url)
.ConfigureAwait(false);
}
}
Dodaliśmy ConfigureAwait(false). Mówi on aplikacji, że nie będzie kontynuowana żadna operacja z nim związana.
Nasza maszyna stanów ma zignorować SychronzationContext, który został pobrany. Nie chcemy wracać do tego kontekstu.
Trzeba pamiętać, że to ustawienie trzeba ustawić z każdym wywołaniem "async i await". Przykładowo moja metoda "MyGetStrinAsync" nie interesuje się kontekstem wykonawczy "button_Click" i w tym wypadku ma to sens.
Gdyby jednak używał interfejsu użytkownika, to ta sytuacja byłaby inna.
.NET oczekuje od Ciebie takiej weryfikacji z każdym wywołaniem async i await.
Nie mamy już tutaj deadlocka.
Taki sam problem można odtworzyć w ASP.NET MVC (nie w ASP.NET CORE, pamiętaj tam nie ma kontekstu)
public class ExampleController : Controller
{
public string Get()
{
MyProxy proxy = new MyProxy();
var r = proxy.MyGetStringAsync(
new Uri("https://postman-echo.com/get")).Result;
return r;
}
}
public class MyProxy
{
public async Task<string> MyGetStringAsync(Uri uri)
{
using( var client = new HttpClient())
{
var value = await client.GetStringAsync(uri);
return value;
}
}
}
Znowu udało na się zrobić sytuacje, w której moja metoda MyGetStrinAsync() czeka na synchronizacje do wątku głównego, a on jest zajęty czekaniem.
Naprawiamy sytuacje używając ConfigureAwait(false)
public class ExampleController : Controller
{
public string Get()
{
MyProxy proxy = new MyProxy();
var r = proxy.MyGetStringAsync(
new Uri("https://postman-echo.com/get")).Result;
return r;
}
}
public class MyProxy
{
public async Task<string> MyGetStringAsync(Uri uri)
{
using (var client = new HttpClient())
{
var value = await client.GetStringAsync(uri)
.ConfigureAwait(false);
return value;
}
}
}
Ogólnie jednak najlepiej by było przyjąć jedną złotą zasadę :
Nie blokujemy Tasków, używając (Result lub Wait) i nie korzystamy potem z "async i await" głębiej w swojej aplikacji.
Kiedy używać ConfigureAwait(false)
Jeżeli piszemy kod na poziomie aplikacji, gdzie działamy z UI ,to wtedy kontekst synchronizacyjny jest nam potrzebny, a więc configureAwait tutaj nie chcemy.
private async void Button_Click(object sender, RoutedEventArgs e)
{
string result = "";
using (HttpClient httpClient = new HttpClient())
{
result = await httpClient.GetStringAsync(_url)
.ConfigureAwait(false);
}
ResultTextBlock.Text = result;
//BUM
}
private readonly static string _url = "https://postman-echo.com/delay/2";
Natomiast gdy tworzymy bibliotekę ".NET" i wiemy, że zawsze ona będzie, gdzie głęboko w stosie wykonawczym gdzie na pewno nie skorzystamy z kontrolek wątku UI, to ma to sens.
Nigdy nie wiem, czy ktoś nie skorzysta z naszej biblioteki używając "Result".
W ASP.NET CORE nawet nie musisz sobie głowy tym zwracać, bo nie ma tam kontekstu.
Istnieje płatna biblioteka ConfigureAwait.Fody, która za Ciebie podmieni konfigurację "async i await" na taką jaką trzeba. Oczywiście trzeba jedna za to zapłacić.
SynchronizationContext : co uczy
Jak widzisz, ile jest zamieszania z tym SynchronizationContext. Dlatego powinieneś też zrezygnować z korzystania metody ContinueWith, byś nie musiał pisać takich metod.
var task = LetsDoSomethingAsync();
var currentSyncContext = SynchronizationContext.Current;
task.ContinueWith(delegate
{
if (currentSyncContext == null) ContinueWithRestOfThisCode();
else currentSyncContext.Post(delegate { ContinueWithRestOfThisCode(); }, null);
}, TaskScheduler.Current);
(żartuje możesz to napisać lepiej)
string result = "";
using (HttpClient httpClient = new HttpClient())
{
result = await httpClient.GetStringAsync(_url)
.ContinueWith(pageHtml =>
{
//Twoj kod UI
return pageHtml.Result;
}
,TaskScheduler.FromCurrentSynchronizationContext());
}
(albo tak)
SynchronizationContext synchronizationContext = SynchronizationContext.Current;
string result = "";
using (HttpClient httpClient = new HttpClient())
{
result = await httpClient.GetStringAsync(_url)
.ContinueWith(pageHtml =>
{
synchronizationContext.Post(__ => {
//Twoj kod UI
}, null);
return pageHtml.Result;
});
}
ContinueWith nie dba o SynchronizationContext, więc tę obsługę musiałbyś napisać sam.
Istnieją przypadki, dla których chciałbyś taką obsługę mieć, ale nie zdarzy Ci się to często.
public void Do(Action job, Action onDone)
{
SynchronizationContext sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
job();
}
finally
{
sc.Post(__ => onDone(), null);
}
});
}
Możesz utworzyć swój kontekst synchronizujący.
Task.Run(async delegate
{
SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());
await MyGetStringAsync();
});
Może chciałbyś utworzyć kontekst, w który obsługujesz jeden wątek główny, w który zbierasz dane na temat innych wątków. Mógłbyś stworzyć swoją kolejkę.
public class MySynchronizationContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
Console.WriteLine("Sychronizacja się wykonuje");
base.Post(d, state);
}
}
To jest bardzo zaawansowany temat i łatwo można stworzyć deadlock.
TaskScheduler
Czym jest ten TaskScheduler. Wiemy, że gdy nie ma SynchronizationContext jak w przypadku ASP.NET CORE, to on właśnie jest używany najbardziej. Reprezentuje on drugą warstwę planowania działań asynchronicznych.
Domyślny TaskScheduler jest oparty na puli wątków, czyli o klasę "ThreadPool" którą omówiliśmy wcześniej.
Gdy Task jest planowany do wykonania, to trafia on do kolejki. TaskScheduler ma metodę QueueTask, który danego Taska umieszcza do kolejki wykonawczej.
TaskScheduler.Default zwróci Ci to siedzie pod nim i zazwyczaj jest pula wątków "ThreadPool".
TaskScheduler.Current pozwoli ci pobrać obecnie używaną instancje tej klasy. Nie ma tutaj metody SET więc nie możesz tak ustawić swojego "TaskScheduler".
Przy użyciu Task.Factory.StartNew masz opcję dodania swojego "TaskScheduler"
W "TaskScheduler" istnieje metoda FromCurrentSynchronizationContext, która tworzy TaskScheduler skojarzoną z bieżącym SynchronizationContext.
Tyle, jeśli chodzi o suchą dokumentację.
Co możemy zrobić z tym TaskScheduler?
var cesp = new ConcurrentExclusiveSchedulerPair();
W ConcurrentExclusiveSchedulerPair masz do dyspozycji dwa TaskSchedulery
- ConcurrentScheduler : Wykona równocześnie wiele operacji wielowątkowo. Możemy określić limit.
- ExclusiveScheduler : Wykona zadanie po kolei i pojedyńczo
Najlepiej skojarzyć te jako operacje do odczytu i do zapisu. Pomyśl, wielowątkowo możesz odczytywać jakąś informację i nie ma tutaj skutków ubocznych, ale zapewne nie chciałbyś, aby równocześnie zapisywać pewne informacje lub je zmieniać, gdy masz wielowątkowy wyścig szczurów.
Co więcej, te style zapisu i odczyty są zależne od siebie. Czyli nie chcemy odczytywać, gdy inne zadanie jest w trakcie zapisu.
Dlatego mamy taki podział.
var cesp = new ConcurrentExclusiveSchedulerPair();
Task.Factory.StartNew(() =>
{
Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler);
}, default, TaskCreationOptions.None,
cesp.ExclusiveScheduler).Wait();
Korzystając ze swojego TaskScheduler, możesz ograniczyć dla obu styli planowania ograniczenie do 8 poleceń na raz.
var cesp = new ConcurrentExclusiveSchedulerPair();
var con = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, 8)
.ConcurrentScheduler;
var exc = new ConcurrentExclusiveSchedulerPair(con).ExclusiveScheduler;
Jeśli z jakiegoś powodu nie możesz korzystać z "async i await" to możesz poprawić metodę ContinueWith w taki sposób. Oto praktyczne użycie "FormCurrentSynchronizationContext"
string result = "";
using (HttpClient httpClient = new HttpClient())
{
result = await httpClient.GetStringAsync(_url)
.ContinueWith(pageHtml =>
{
//Twoj kod UI
return pageHtml.Result;
}
,TaskScheduler.FromCurrentSynchronizationContext());
}
To wszystko, co powinieneś wiedzieć na temat "TaskScheduler"
Bonus : ExecutionContext
Istnieją jeszcze inne konteksty, które zazwyczaj nas nie obchodzą. Są to szczegóły implementacyjne, do których nie chcemy grzebać.
ExecutionContext jest pojemnikiem na te inne konteksty : Security Context , HostExcecutionContext, CallContext
Przechowują one dane na temat tego, jak asynchroniczne wywołania przebiegały.
Jeżeli nie chcesz takiego śledzenia to możesz skorzystać z wywołania :
ThreadPool.UnsafeQueueUserWorkItem
Jest on prawie wszędzie w .NET ze względu na bezpieczeństwo.
To tyle, jeżeli chodzi o ten wpis. Następnym razem skupimy się na wywoływaniu kolekcji Tasków i o tym, jak to zrobić z głową.