Mam wielowarstwową aplikację .Net 4,5 wywołującą metodę przy użyciu nowych async
i C #await
słów kluczowych, które po prostu się zawieszają i nie rozumiem, dlaczego.
Na dole mam metodę asynchroniczną, która rozszerza nasze narzędzie bazy danych OurDBConn
(w zasadzie opakowanie dla bazowego DBConnection
i DBCommand
obiektów):
public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
string connectionString = dataSource.ConnectionString;
// Start the SQL and pass back to the caller until finished
T result = await Task.Run(
() =>
{
// Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
using (var ds = new OurDBConn(connectionString))
{
return function(ds);
}
});
return result;
}
Następnie mam metodę asynchroniczną średniego poziomu, która wywołuje to, aby uzyskać wolno działające sumy:
public static async Task<ResultClass> GetTotalAsync( ... )
{
var result = await this.DBConnection.ExecuteAsync<ResultClass>(
ds => ds.Execute("select slow running data into result"));
return result;
}
Wreszcie mam metodę UI (akcję MVC), która działa synchronicznie:
Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);
// do other stuff that takes a few seconds
ResultClass slowTotal = asyncTask.Result;
Problem w tym, że na zawsze wisi na ostatniej linii. Robi to samo, gdy dzwonięasyncTask.Wait()
. Jeśli uruchomię wolną metodę SQL bezpośrednio, zajmie to około 4 sekund.
Spodziewam się zachowania, kiedy to nastąpi asyncTask.Result
, jeśli nie zostanie zakończone, powinno poczekać, aż nastąpi, a gdy to się stanie, powinno zwrócić wynik.
Jeśli przejdę przez debuger, instrukcja SQL kończy się i funkcja lambda kończy się, ale return result;
wierszGetTotalAsync
nie zostanie osiągnięta.
Masz pojęcie, co robię źle?
Jakieś sugestie, gdzie muszę to zbadać, aby to naprawić?
Czy to może być gdzieś impas, a jeśli tak, czy istnieje bezpośredni sposób, aby go znaleźć?
SynchronizationContext
.async
/await
.To jest klasyczny
async
scenariusz mieszanego impasu, jak opisuję na moim blogu . Jason dobrze to opisał: domyślnie „kontekst” jest zapisywany w każdym miejscuawait
i używany do kontynuowaniaasync
metody. Ten „kontekst” jest aktualny,SynchronizationContext
chyba że nim jestnull
, w którym to przypadku jest aktualnyTaskScheduler
. Gdyasync
metoda próbuje kontynuować, najpierw ponownie wchodzi do przechwyconego „kontekstu” (w tym przypadku ASP.NETSynchronizationContext
). ASP.NETSynchronizationContext
zezwala tylko na jeden wątek w kontekście naraz, aw kontekście istnieje już wątek - wątek zablokowanyTask.Result
.Istnieją dwie wskazówki, które pozwolą uniknąć tego impasu:
async
całą drogę w dół. Wspomniałeś, że „nie możesz” tego zrobić, ale nie jestem pewien, dlaczego nie. ASP.NET MVC na .NET 4.5 z pewnością może obsługiwaćasync
akcje i nie jest to trudna zmiana.ConfigureAwait(continueOnCapturedContext: false)
jak najwięcej. Zastępuje to domyślne zachowanie wznawiania w przechwyconym kontekście.źródło
ConfigureAwait(false)
gwarantuje, że obecna funkcja zostanie wznowiona w innym kontekście?async
działania bez zerwania sposobu, w jaki działa to po stronie klienta. Z pewnością planuję jednak zbadać tę opcję w dłuższej perspektywie.ConfigureAwait(false)
drzewa wywołań rozwiązałoby problem OP.async
ogóle nie wpływa na stronę klienta. Wyjaśniam to w innym poście na blogu,async
Nie zmienia protokołu HTTP .async
że „rośnie” w bazie kodu. Jeśli metoda kontrolera może zależeć od operacji asynchronicznych, powinna zwrócić metodę klasy bazowejTask<ActionResult>
. Przenoszenie dużego projektu doasync
jest zawsze niewygodne, ponieważ mieszanieasync
i synchronizacja kodu jest trudne i skomplikowane. Czystyasync
kod jest znacznie prostszy.Byłem w tej samej sytuacji zakleszczenia, ale w moim przypadku wywołanie metody asynchronicznej z metody synchronizacji działa dla mnie:
czy to dobre podejście, jakiś pomysł?
źródło
Aby dodać do zaakceptowanej odpowiedzi (za mało przedstawiciela, aby skomentować), miałem ten problem podczas blokowania używania
task.Result
, zdarzenie chociaż każdyawait
poniżej miałConfigureAwait(false)
, jak w tym przykładzie:Problem faktycznie dotyczył kodu biblioteki zewnętrznej. Metoda biblioteki asynchronicznej próbowała kontynuować w kontekście wywoływania synchronizacji, bez względu na to, jak skonfigurowałem oczekiwanie, co doprowadziło do zakleszczenia.
Dlatego odpowiedzią było zrolowanie własnej wersji kodu biblioteki zewnętrznej
ExternalLibraryStringAsync
, tak aby miała pożądane właściwości kontynuacji.zła odpowiedź ze względów historycznych
Po wielu bólu i udręce znalazłem rozwiązanie ukryte w tym poście na blogu (Ctrl-f oznacza „impas”). Obraca się wokół używania
task.ContinueWith
zamiast gołegotask.Result
.Przykład wcześniejszego zakleszczenia:
Uniknij impasu w ten sposób:
źródło
Task
i nie udostępniasz wywołującemu żadnych środków na określenie, kiedy faktycznie nastąpi mutacja zwróconego obiektu.GetFooSynchronous
metody?Task
zamiast blokować.szybka odpowiedź: zmień tę linię
do
czemu? nie powinieneś używać .result do uzyskania wyniku zadań wewnątrz większości aplikacji z wyjątkiem aplikacji konsolowych, jeśli to zrobisz, twój program zawiesi się, gdy się tam dostanie
możesz również wypróbować poniższy kod, jeśli chcesz użyć .Result
źródło