Pracuję nad projektem, który musi obsługiwać zarówno wersję asynchroniczną, jak i synchronizacyjną tej samej logiki / metody. Na przykład muszę mieć:
public class Foo
{
public bool IsIt()
{
using (var conn = new SqlConnection(DB.ConnString))
{
return conn.Query<bool>("SELECT IsIt FROM SomeTable");
}
}
public async Task<bool> IsItAsync()
{
using (var conn = new SqlConnection(DB.ConnString))
{
return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
}
}
}
Logika asynchronizacji i synchronizacji dla tych metod jest identyczna pod każdym względem, z wyjątkiem tego, że jedna jest asynchroniczna, a druga nie. Czy istnieje uzasadniony sposób uniknięcia naruszenia zasady DRY w tego rodzaju scenariuszu? Widziałem, że ludzie mówią, że możesz użyć GetAwaiter (). GetResult () na metodzie asynchronicznej i wywołać ją z metody synchronizacji? Czy ten wątek jest bezpieczny we wszystkich scenariuszach? Czy istnieje inny, lepszy sposób, aby to zrobić, czy też jestem zmuszony powielić logikę?
c#
.net
asynchronous
Marko
źródło
źródło
return Task.FromResult(IsIt());
)Odpowiedzi:
W swoim pytaniu zadałeś kilka pytań. Rozbiorę je nieco inaczej niż ty. Ale najpierw pozwól mi bezpośrednio odpowiedzieć na pytanie.
Wszyscy chcemy aparatu, który jest lekki, wysokiej jakości i tani, ale jak mówi przysłowie, można uzyskać maksymalnie dwa z tych trzech. Jesteś w tej samej sytuacji tutaj. Chcesz rozwiązania, które jest wydajne, bezpieczne i dzieli kod między ścieżkami synchronicznymi i asynchronicznymi. Dostaniesz tylko dwa z nich.
Pozwól mi wyjaśnić, dlaczego tak jest. Zaczniemy od tego pytania:
Pytanie to brzmi: „czy mogę współdzielić ścieżki synchroniczne i asynchroniczne, czyniąc ścieżkę synchroniczną po prostu synchronicznym oczekiwaniem w wersji asynchronicznej?”
Pozwól, że wyrażę się jasno w tej kwestii, ponieważ jest to ważne:
NALEŻY NATYCHMIAST PRZESTAĆ SKORZYSTAĆ Z JAKICHKOLWIEK PORAD OD TYCH LUDZI .
To bardzo zła rada. Synchroniczne pobieranie wyniku z zadania asynchronicznego jest bardzo niebezpieczne, chyba że masz dowody, że zadanie zakończyło się normalnie lub nieprawidłowo .
Powodem, dla którego jest to bardzo zła rada, jest, cóż, rozważ ten scenariusz. Chcesz kosić trawnik, ale ostrze kosiarki jest zepsute. Zdecydowałeś się przestrzegać tego przepływu pracy:
Co się dzieje? Śpisz wiecznie, ponieważ operacja sprawdzania poczty jest teraz uzależniona od czegoś, co dzieje się po jej nadejściu .
Jest to bardzo proste , aby dostać się do tej sytuacji, kiedy synchronicznie czekać na dowolnym zadaniu. To zadanie mogło mieć zaplanowaną pracę w przyszłości wątku, który teraz czeka , a teraz ta przyszłość nigdy nie nadejdzie, ponieważ na nią czekasz.
Jeśli wykonujesz asynchroniczne oczekiwanie, wszystko jest w porządku! Od czasu do czasu sprawdzasz pocztę, a podczas oczekiwania robisz kanapkę, płacisz podatki lub cokolwiek innego; wykonujesz pracę podczas oczekiwania.
Nigdy nie czekaj synchronicznie. Jeśli zadanie zostało wykonane, nie jest konieczne . Jeśli zadanie nie zostanie wykonane, ale zaplanowane uruchomienie z bieżącego wątku, jest nieefektywne, ponieważ bieżący wątek może obsługiwać inną pracę zamiast czekać. Jeśli zadanie nie zostanie wykonane, a harmonogram uruchomi się na bieżącym wątku, zawiesi się, aby synchronicznie czekać. Nie ma żadnego dobrego powodu, aby ponownie synchronicznie czekać, chyba że już wiesz, że zadanie zostało zakończone .
Więcej informacji na ten temat można znaleźć na stronie
https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
Stephen wyjaśnia scenariusz ze świata rzeczywistego znacznie lepiej niż potrafię.
Rozważmy teraz „inny kierunek”. Czy możemy współdzielić kod, czyniąc wersję asynchroniczną po prostu wykonując wersję synchroniczną w wątku roboczym?
Jest to prawdopodobnie i prawdopodobnie zły pomysł z następujących powodów.
Jest nieefektywny, jeśli operacja synchroniczna działa we / w z dużym opóźnieniem. To zasadniczo zatrudnia pracownika i sprawia, że ten pracownik śpi aż do wykonania zadania. Wątki są niesamowicie drogie . Domyślnie zużywają milion bajtów przestrzeni adresowej, zabierają czas, zajmują zasoby systemu operacyjnego; nie chcesz palić nici wykonując bezużyteczną pracę.
Operacja synchroniczna może nie zostać zapisana jako bezpieczna dla wątków.
Jest to bardziej rozsądna technika, jeśli praca z dużymi opóźnieniami jest związana z procesorem, ale jeśli tak, to prawdopodobnie nie chcesz po prostu przekazywać jej do wątku roboczego. Prawdopodobnie chcesz użyć biblioteki zadań równoległych do zrównoleglenia jej z jak największą liczbą procesorów, prawdopodobnie potrzebujesz logiki anulowania i nie możesz po prostu zmusić wersji synchronicznej do tego wszystkiego, ponieważ wtedy byłaby to już wersja asynchroniczna .
Dalsza lektura; ponownie Stephen wyjaśnia to bardzo wyraźnie:
Dlaczego nie skorzystać z Task.Run:
https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html
Więcej scenariuszy „rób i nie rób” dla Task.Run:
https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html
Co nam zatem pozostawia? Obie techniki udostępniania kodu prowadzą do impasu lub znacznej nieefektywności. Doszliśmy do wniosku, że musisz dokonać wyboru. Czy chcesz program, który jest wydajny i poprawny i zachwyca dzwoniącego, czy chcesz zaoszczędzić kilka naciśnięć klawiszy związanych z duplikowaniem niewielkiej ilości kodu między ścieżkami synchronicznymi i asynchronicznymi? Obawiam się, że nie dostaniesz obu.
źródło
Task.Run(() => SynchronousMethod())
w wersji asynchronicznej nie jest tym, czego chce OP?Dns.GetHostEntryAsync
jest implementowany w .NET iFileStream.ReadAsync
dla niektórych typów plików. System operacyjny po prostu nie zapewnia interfejsu asynchronicznego, więc środowisko wykonawcze musi je sfałszować (i nie jest specyficzne dla języka - powiedzmy, środowisko wykonawcze Erlang uruchamia całe drzewo procesów roboczych , z wieloma wątkami w każdym z nich, aby zapewnić nieblokujące wejścia / wyjścia dysku i nazwę rozkład).Trudno jest odpowiedzieć na to jedno uniwersalne rozwiązanie. Niestety nie ma prostego, idealnego sposobu na ponowne użycie kodu asynchronicznego i synchronicznego. Ale oto kilka zasad do rozważenia:
Query()
jedną iQueryAsync()
drugą) lub konfigurowaniem połączeń z różnymi ustawieniami. Więc nawet jeśli jest strukturalnie podobny, często występują wystarczające różnice w zachowaniu, aby zasługiwały na to, aby traktować je jako oddzielny kod o różnych wymaganiach. Zwróć uwagę na różnice między implementacjami metod Async i Sync w klasie File, na przykład: nie podejmuje się żadnych starań, aby używały tego samego koduTask.FromResult(...)
.Powodzenia.
źródło
Łatwy; niech synchroniczny wywoła asynchroniczny. Istnieje nawet przydatna metoda,
Task<T>
aby to zrobić:źródło