Dość często ludzie sądzą, że oczekiwanie jest dokonywane przez kilka wątków. W rzeczywistości wszystko odbywa się za pomocą jednego wątku.
Rzeczą, która bardzo mi pomogła zrozumieć async-await, jest this interview with Eric Lippert about async-await. Gdzieś w środku porównuje async z kucharzem, który musi poczekać, aż się zagotuje. Zamiast nic nie robić, rozgląda się, czy nie jest jeszcze coś do zrobienia, jak krojenie cebuli. Jeśli to się skończy, a woda nadal się nie zagotuje, sprawdza, czy jest coś innego do zrobienia, i tak dalej, dopóki nie będzie miał nic do roboty poza czekaniem. W takim przypadku wraca do pierwszej rzeczy, na którą czekał.
Jeśli twoja procedura wywoła daną funkcję, jesteśmy pewni, że gdzieś w tej nieoczekiwanej funkcji jest wezwanie do funkcji, której nie można oczekiwać, w przeciwnym razie funkcja nie byłaby oczekiwana. W rzeczywistości twój kompilator ostrzeże cię, jeśli zapomnisz poczekać gdzieś w swojej oczekiwanej funkcji.
Jeśli twoja funkcja wywoła inną, oczekiwaną funkcję, wątek wchodzi do tej innej funkcji i zaczyna robić rzeczy w tej funkcji i przechodzi w inne funkcje, dopóki nie spotka się z oczekiwaniem.
Zamiast czekać na wyniki, wątek idzie w górę w stosie wywołań, aby sprawdzić, czy istnieją inne fragmenty kodu, które może przetworzyć, dopóki nie poczeka. Podejdź jeszcze raz do stosu wywołań, przetwórz, aż poczekasz, itd. Gdy wszyscy oczekują, wątek oczekuje na dno i poczekaj, aż to się skończy.
Ma to tę zaletę, że jeśli osoba wywołująca twoją nieoczekiwaną funkcję nie potrzebuje rezultatu twojej funkcji, ale może robić inne rzeczy zanim wynik będzie potrzebny, te inne rzeczy mogą być wykonane przez wątek zamiast czekania wewnątrz twoja funkcja.
Wezwanie natychmiast bez czekania na wynik będzie wyglądać następująco:
private async Task MyFunction()
{
Task<ReturnType>taskA = SomeFunctionAsync(...)
// I don't need the result yet, I can do something else
DoSomethingElse();
// now I need the result of SomeFunctionAsync, await for it:
ReturnType result = await TaskA;
// now you can use object result
}
Należy zauważyć, że w tym scenariuszu wszystko odbywa się przez jeden wątek. Dopóki twoja nić ma coś do zrobienia, będzie zajęty.
Dodawanie. To nieprawda, że zaangażowany jest tylko jeden wątek. Każdy wątek, który nie ma nic do roboty, może kontynuować przetwarzanie kodu po oczekiwaniu. Jeśli zaznaczysz identyfikator wątku, zobaczysz, że ten identyfikator można zmienić po oczekiwaniu. Kontynuowany wątek ma ten sam numer context
, co oryginalny wątek, więc możesz działać tak, jakby był to oryginalny wątek. Nie trzeba sprawdzać pod kątem InvokeRequired
, nie trzeba używać muteksów lub sekcji krytycznych. Dla twojego kodu jest to tak, jakby w grę wchodził jeden wątek.
Link do artykułu w końcu tej odpowiedzi wyjaśniono nieco więcej o kontekście wątku
zobaczysz awaitable funkcje głównie tam, gdzie jakiś inny proces musi robić rzeczy, podczas gdy Twój wątek prostu musi poczekaj, aż skończy się druga rzecz, np. wysyłając dane przez Internet, zapisując plik, komunikując się z bazą danych itp.
Jednak czasami trzeba wykonać ciężkie obliczenia i chcesz, aby twój wątek był wolny coś innego, na przykład reagowanie na dane wprowadzone przez użytkownika. W tej beczce możesz rozpocząć akcję, tak jakbyś zadzwonił do funkcji asynchronicznej.
Task<ResultType> LetSomeoneDoHeavyCalculations(...)
{
DoSomePreparations()
// start a different thread that does the heavy calculations:
var myTask = Task.Run(() => DoHeavyCalculations(...))
// now you are free to do other things
DoSomethingElse();
// once you need the result of the HeavyCalculations await for it
var myResult = await myTask;
// use myResult
...
}
Teraz inny wątek wykonuje ciężkie obliczenia, podczas gdy twój wątek może wykonywać inne czynności. Gdy zacznie się czekać, twój rozmówca może robić rzeczy, dopóki nie zacznie czekać. W efekcie twoja nić będzie dość swobodnie reagować na dane wprowadzone przez użytkownika. Jednak będzie tak tylko w przypadku, gdy wszyscy będą czekać. Podczas gdy twój wątek jest zajęty robieniem rzeczy, których wątek nie może reagować na dane wprowadzone przez użytkownika. Dlatego zawsze upewnij się, że jeśli uważasz, że wątek UI musi zrobić trochę zajęty przetwarzanie że zajmuje trochę czasu korzystania Task.Run i niech inny wątek to zrobić
Kolejny artykuł, który pomógł mi: Async-Await by the brilliant explainer Stephen Cleary
'Task.Run' pozwala oczekiwać na metodę synchroniczną. Jeśli twoja metoda jest już asynchroniczna i zwraca zadanie, nie potrzebujesz tego. –
Dwa fragmenty są * nie * równoważne. Pierwsza linia nie czeka na zakończenie funkcji 'LongProcess()', po prostu wywoła zadanie potomne i powróci. Aby były one równoważne, musisz użyć 'await LongProcess()' –
@PanagiotisKanavos Not true. Kod dokładnie taki, jaki został opublikowany, użyje przeciążenia 'Func' 'Task.Run', więc * będzie * czekało na zakończenie' LongProcess'. –
Luaan