'await' działa, ale wywołanie task.Result zawiesza się / zakleszcza

126

Mam następujące cztery testy, a ostatni zawiesza się po uruchomieniu. Dlaczego to się dzieje:

[Test]
public void CheckOnceResultTest()
{
    Assert.IsTrue(CheckStatus().Result);
}

[Test]
public async void CheckOnceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceResultTest()
{
    Assert.IsTrue(CheckStatus().Result); // This hangs
    Assert.IsTrue(await CheckStatus());
}

private async Task<bool> CheckStatus()
{
    var restClient = new RestClient(@"https://api.test.nordnet.se/next/1");
    Task<IRestResponse<DummyServiceStatus>> restResponse = restClient.ExecuteTaskAsync<DummyServiceStatus>(new RestRequest(Method.GET));
    IRestResponse<DummyServiceStatus> response = await restResponse;
    return response.Data.SystemRunning;
}

Używam tej metody rozszerzenia dla restsharp RestClient :

public static class RestClientExt
{
    public static Task<IRestResponse<T>> ExecuteTaskAsync<T>(this RestClient client, IRestRequest request) where T : new()
    {
        var tcs = new TaskCompletionSource<IRestResponse<T>>();
        RestRequestAsyncHandle asyncHandle = client.ExecuteAsync<T>(request, tcs.SetResult);
        return tcs.Task;
    }
}
public class DummyServiceStatus
{
    public string Message { get; set; }
    public bool ValidVersion { get; set; }
    public bool SystemRunning { get; set; }
    public bool SkipPhrase { get; set; }
    public long Timestamp { get; set; }
}

Dlaczego ostatni test się zawiesza?

Johan Larsson
źródło
7
Należy unikać zwracania void z metod asynchronicznych. Jest to tylko dla wstecznej kompatybilności z istniejącymi programami obsługi zdarzeń, głównie w kodzie interfejsu. Jeśli twoja metoda asynchroniczna nic nie zwraca, powinna zwrócić Task. Miałem wiele problemów z MSTest i void returning async.
ghord
2
@ghord: MSTest w ogóle nie obsługuje async voidmetod testów jednostkowych; po prostu nie będą działać. Jednak NUnit tak. Powiedział, że zgadzam się z ogólną zasadą preferując async Tasknad async void.
Stephen Cleary
@StephenCleary Tak, chociaż było to dozwolone w wersji beta VS2012, co powodowało różnego rodzaju problemy.
ghord

Odpowiedzi:

88

Napotkasz standardową sytuację zakleszczenia, którą opisuję na moim blogu i w artykule MSDN : asyncmetoda próbuje zaplanować kontynuację w wątku, który jest blokowany przez wywołanie Result.

W tym przypadku Twój SynchronizationContextjest używany przez NUnit do wykonywania async voidmetod testowych. async TaskZamiast tego spróbuję użyć metod testowych.

Stephen Cleary
źródło
4
zmiana na async Zadanie zadziałało, teraz muszę kilka razy przeczytać zawartość twoich linków, ty sir.
Johan Larsson
@MarioLopez: Rozwiązaniem jest użycie „ asyncdo końca” (jak wspomniano w moim artykule MSDN). Innymi słowy - jak stwierdza tytuł mojego posta na blogu - „nie blokuj kodu asynchronicznego”.
Stephen Cleary
1
@StephenCleary a co, jeśli muszę wywołać metodę asynchroniczną wewnątrz konstruktora? Konstruktorzy nie mogą być asynchroniczni.
Raikol Amaro,
1
@StephenCleary W prawie wszystkich twoich odpowiedziach na SO i w twoich artykułach, wszystko, o czym kiedykolwiek widziałem, mówisz, to zastąpienie Wait()metodą wywoływania async. Ale wydaje mi się, że to przesuwa problem na wyższy poziom. W pewnym momencie trzeba czymś zarządzać synchronicznie. Co się stanie, jeśli moja funkcja jest celowo synchroniczna, ponieważ zarządza długo działającymi wątkami roboczymi Task.Run()? Jak mam czekać, aż to się skończy bez zakleszczenia w moim teście NUnit?
void.pointer
1
@ void.pointer: At some point, something has to be managed synchronously.- wcale. W przypadku aplikacji interfejsu użytkownika punktem wejścia może być program async voidobsługi zdarzeń. W przypadku aplikacji serwerowych punktem wejścia może być async Task<T>akcja. Zaleca się używanie asyncobu, aby uniknąć blokowania gwintów. Test NUnit może być synchroniczny lub asynchroniczny; jeśli async, zrób to async Taskzamiast async void. Jeśli jest synchroniczny, nie powinien mieć znaku, SynchronizationContextwięc nie powinno być impasu.
Stephen Cleary
223

Uzyskanie wartości metodą asynchroniczną:

var result = Task.Run(() => asyncGetValue()).Result;

Synchroniczne wywoływanie metody asynchronicznej

Task.Run( () => asyncMethod()).Wait();

Dzięki użyciu Task.Run nie wystąpią żadne problemy z zakleszczeniem.

Herman Schoenfeld
źródło
15
-1 za zachęcanie do stosowania async voidmetod testów jednostkowych i usuwanie gwarancji tego samego wątku zapewnianych przez SynchronizationContexttestowany system.
Stephen Cleary
68
@StephenCleary: nie ma „zachęcającej” pustki asynchronicznej. Po prostu wykorzystuje prawidłową konstrukcję C # do rozwiązania problemu zakleszczenia. Powyższy fragment kodu jest nieodzownym i prostym obejściem problemu PO. Stackoverflow dotyczy rozwiązań problemów, a nie szczegółowej autopromocji.
Herman Schoenfeld
81
@StephenCleary: Twoje artykuły tak naprawdę nie opisują rozwiązania (przynajmniej nie jasno), a nawet gdybyś miał rozwiązanie, używałbyś takich konstrukcji pośrednio. Moje rozwiązanie nie wykorzystuje kontekstów, więc co z tego? Chodzi o to, że mój działa i jest jednoliniowy. Nie potrzebował dwóch postów na blogu i tysięcy słów, aby rozwiązać problem. UWAGA: Nawet nie używam async void , więc tak naprawdę nie wiem, o co ci chodzi. Czy widzisz „async void” gdziekolwiek w mojej zwięzłej i poprawnej odpowiedzi?
Herman Schoenfeld
15
@HermanSchoenfeld, jeśli dodasz dlaczego do sposobu , wierzę, że twoja odpowiedź przyniosłaby wiele korzyści.
ironstone13
19
Wiem, że to trochę późno, ale powinieneś używać .GetAwaiter().GetResult()zamiast .Result, aby żaden Exceptionnie był opakowany.
Camilo Terevinto
15

Możesz uniknąć zakleszczenia dodawania ConfigureAwait(false)do tej linii:

IRestResponse<DummyServiceStatus> response = await restResponse;

=>

IRestResponse<DummyServiceStatus> response = await restResponse.ConfigureAwait(false);

Opisałem tę pułapkę w moim wpisie na blogu Pułapki async / await

Vladimir
źródło
9

Blokujesz interfejs użytkownika za pomocą właściwości Task.Result. W dokumentacji MSDN wyraźnie wspomnieli, że

„Właściwość Result jest właściwością blokującą. Jeśli spróbujesz uzyskać do niej dostęp przed zakończeniem zadania, wątek, który jest aktualnie aktywny, zostanie zablokowany do czasu zakończenia zadania i udostępnienia wartości. W większości przypadków należy uzyskać dostęp do wartości za pomocą opcji Oczekiwanie lub poczekaj zamiast bezpośredniego dostępu do usługi. "

Najlepszym rozwiązaniem dla tego scenariusza byłoby usunięcie zarówno await, jak i async z metod i użycie tylko zadania, w którym zwracasz wynik. Nie zakłóci to sekwencji wykonywania.

Mroczny rycerz
źródło
3

Jeśli nie otrzymujesz żadnych wywołań zwrotnych lub kontrolka zawiesza się, po wywołaniu funkcji asynchronicznej usługi / API, musisz skonfigurować Context, aby zwracał wynik w tym samym wywołanym kontekście.

Posługiwać się TestAsync().ConfigureAwait(continueOnCapturedContext: false);

Będziesz mieć do czynienia z tym problemem tylko w aplikacjach internetowych, ale nie w static void main.

Mayank Pandit
źródło
ConfigureAwaitpozwala uniknąć zakleszczenia w niektórych scenariuszach, nie uruchamiając się w oryginalnym kontekście wątku.
davidcarr