Oto sekwencja fragmentów kodu, których ostatnio użyłem, aby zilustrować różnicę i różne problemy przy użyciu rozwiązań asynchronicznych.
Załóżmy, że masz program obsługi zdarzeń w aplikacji opartej na GUI, który zajmuje dużo czasu, więc chciałbyś, aby był asynchroniczny. Oto logika synchroniczna, od której zaczynasz:
while (true) {
string result = LoadNextItem().Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
LoadNextItem zwraca zadanie, które ostatecznie da wynik, który chcesz sprawdzić. Jeśli bieżący wynik jest tym, którego szukasz, aktualizujesz wartość jakiegoś licznika w interfejsie użytkownika i wracasz z metody. W przeciwnym razie kontynuujesz przetwarzanie większej liczby elementów z LoadNextItem.
Pierwszy pomysł na wersję asynchroniczną: po prostu użyj kontynuacji! I na razie zignorujmy część zapętloną. Mam na myśli, co mogłoby się nie udać?
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
});
Świetnie, teraz mamy metodę, która nie blokuje! Zamiast tego ulega awarii. Wszelkie aktualizacje formantów interfejsu użytkownika powinny mieć miejsce w wątku interfejsu użytkownika, więc musisz to uwzględnić. Na szczęście istnieje opcja określenia, w jaki sposób powinny być zaplanowane kontynuacje, i jest domyślna tylko do tego:
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Świetnie, teraz mamy metodę, która się nie zawiesza! Zamiast tego po cichu zawodzi. Kontynuacje same w sobie są oddzielnymi zadaniami, których status nie jest powiązany ze statusem zadania poprzedzającego. Więc nawet jeśli LoadNextItem zakończy się niepowodzeniem, wywołujący zobaczy tylko zadanie, które zostało pomyślnie zakończone. OK, po prostu przekaż wyjątek, jeśli taki istnieje:
return LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
throw t.Exception.InnerException;
}
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Świetnie, teraz to faktycznie działa. Dla jednej pozycji. A co z tym zapętleniem. Okazuje się, że rozwiązanie odpowiadające logice oryginalnej wersji synchronicznej będzie wyglądało mniej więcej tak:
Task AsyncLoop() {
return AsyncLoopTask().ContinueWith(t =>
Counter.Value = t.Result,
TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
var tcs = new TaskCompletionSource<int>();
DoIteration(tcs);
return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
tcs.TrySetException(t.Exception.InnerException);
} else if (t.Result.Contains("target")) {
tcs.TrySetResult(t.Result.Length);
} else {
DoIteration(tcs);
}});
}
Lub, zamiast wszystkich powyższych, możesz użyć async, aby zrobić to samo:
async Task AsyncLoop() {
while (true) {
string result = await LoadNextItem();
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
}
Teraz jest o wiele ładniej, prawda?
Wait
połączenia w drugim przykładzie następnie dwa fragmenty będzie (w większości) równoważne.