W mojej aplikacji Metro C # / XAML jest przycisk, który uruchamia długotrwały proces. Tak więc, zgodnie z zaleceniami, używam async / await, aby upewnić się, że wątek interfejsu użytkownika nie zostanie zablokowany:
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
await GetResults();
}
private async Task GetResults()
{
// Do lot of complex stuff that takes a long time
// (e.g. contact some web services)
...
}
Czasami rzeczy dziejące się w GetResults wymagały dodatkowego wkładu użytkownika, zanim będzie można je kontynuować. Dla uproszczenia powiedzmy, że użytkownik musi po prostu kliknąć przycisk „kontynuuj”.
Moje pytanie brzmi: jak mogę zawiesić wykonanie GetResults tak, aby czekało na zdarzenie, takie jak kliknięcie innego przycisku?
Oto brzydki sposób na osiągnięcie tego, czego szukam: moduł obsługi zdarzenia dla przycisku kontynuuj ”ustawia flagę ...
private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
_continue = true;
}
... a GetResults okresowo sonduje to:
buttonContinue.Visibility = Visibility.Visible;
while (!_continue) await Task.Delay(100); // poll _continue every 100ms
buttonContinue.Visibility = Visibility.Collapsed;
Ankieta jest wyraźnie okropna (zajęte oczekiwanie / marnowanie cykli) i szukam czegoś opartego na wydarzeniach.
Jakieś pomysły?
Tak przy okazji, w tym uproszczonym przykładzie jednym rozwiązaniem byłoby oczywiście podzielenie GetResults () na dwie części, wywołanie pierwszej części z przycisku Start i drugiej części z przycisku Kontynuuj. W rzeczywistości rzeczy dziejące się w GetResults są bardziej złożone i różne typy danych wejściowych użytkownika mogą być wymagane w różnych punktach wykonywania. Tak więc rozbicie logiki na wiele metod byłoby nietrywialne.
ManualResetEvent(Slim)
nie wydaje się wspieraćWaitAsync()
.async
nie oznacza „działa w innym wątku” czy coś w tym rodzaju. Oznacza po prostu „możesz użyćawait
w tej metodzie”. W tym przypadku blokowanie wewnątrzGetResults()
faktycznie zablokowałoby wątek interfejsu użytkownika.await
samo w sobie nie gwarantuje, że zostanie utworzony kolejny wątek, ale powoduje, że wszystko inne po instrukcji działa jako kontynuacjaTask
lub oczekiwanie, które wywołujeszawait
. Częściej niż nie, to jest jakiś rodzaj operacji asynchronicznych, które mogłyby być zakończenie IO, lub coś, co jest w innym wątku.SemaphoreSlim.WaitAsync
nie tylko wypychaWait
wątek na wątek puli wątków.SemaphoreSlim
ma odpowiednią kolejkęTask
s, które są używane do implementacjiWaitAsync
.Kiedy masz niezwykłą rzecz, którą musisz
await
włączyć, często najłatwiejszą odpowiedzią jestTaskCompletionSource
(lubasync
na podstawie jakiegoś prymitywu z włączoną funkcjąTaskCompletionSource
).W takim przypadku twoja potrzeba jest dość prosta, więc możesz po prostu użyć
TaskCompletionSource
bezpośrednio:Logicznie rzecz biorąc,
TaskCompletionSource
jest jak anasync
ManualResetEvent
, z tą różnicą, że możesz „ustawić” zdarzenie tylko raz, a zdarzenie może mieć „wynik” (w tym przypadku go nie używamy, więc ustawiamy wynik nanull
).źródło
Oto klasa użyteczności, której używam:
A oto jak z tego korzystam:
źródło
new Task(() => { });
zostałby natychmiast ukończony?Prosta klasa pomocnika:
Stosowanie:
źródło
example.YourEvent
?Idealnie, nie . Chociaż z pewnością możesz zablokować wątek asynchroniczny, jest to marnotrawstwo zasobów i nie jest idealne.
Rozważmy kanoniczny przykład, w którym użytkownik idzie na obiad, podczas gdy przycisk czeka na kliknięcie.
Jeśli zatrzymałeś kod asynchroniczny podczas oczekiwania na dane wejściowe od użytkownika, po prostu marnujesz zasoby, gdy ten wątek jest wstrzymany.
To powiedziawszy, lepiej jest, jeśli w operacji asynchronicznej ustawisz stan, który musisz utrzymywać, do punktu, w którym przycisk jest włączony i „czekasz” na kliknięcie. W tym momencie Twoja
GetResults
metoda się zatrzymuje .Wtedy, gdy przycisk zostanie kliknięty, na podstawie stanu, które są przechowywane, zaczynasz kolejny asynchroniczne zadanie kontynuowania prac.
Ponieważ
SynchronizationContext
zostanie przechwycony w programie obsługi zdarzeń, który wywołujeGetResults
(kompilator zrobi to w wyniku użyciaawait
używanego słowa kluczowego i faktu, że SynchronizationContext.Current nie powinien mieć wartości null, biorąc pod uwagę, że jesteś w aplikacji interfejsu użytkownika), można użyćasync
/await
tak:ContinueToGetResultsAsync
to metoda, która nadal daje wyniki w przypadku naciśnięcia przycisku. Jeśli przycisk nie jest wciśnięty, program obsługi zdarzeń nic nie robi.źródło
GetResults
zwraca aTask
.await
po prostu mówi „uruchom zadanie, a kiedy zadanie zostanie wykonane, kontynuuj kod po tym”. Biorąc pod uwagę, że istnieje kontekst synchronizacji, wywołanie jest kierowane z powrotem do wątku interfejsu użytkownika, ponieważ jest przechwytywane wawait
.await
to nie to samo coTask.Wait()
, w najmniejszym stopniu.Wait()
. Ale kod wGetResults()
będzie działał w wątku interfejsu użytkownika tutaj, nie ma innego wątku. Innymi słowy, tak,await
zasadniczo uruchamia zadanie, tak jak mówisz, ale tutaj to zadanie działa również w wątku interfejsu użytkownika.await
a następnie kod po nimawait
, nie ma blokowania. Reszta kodu jest przenoszona z powrotem w kontynuacji i planowana przezSynchronizationContext
.Stephen Toub opublikował te
AsyncManualResetEvent
zajęcia na swoim blogu .źródło
Z rozszerzeniami reaktywnymi (Rx.Net)
Możesz dodać Rx za pomocą Nuget Package System.Reactive
Badana próbka:
źródło
Używam własnej klasy AsyncEvent dla oczekiwanych wydarzeń.
Aby zadeklarować zdarzenie w klasie, które wywołuje zdarzenia:
Aby podnieść wydarzenia:
Aby zapisać się na wydarzenia:
źródło