'Warten' funktioniert, aber Task aufrufen. Ergebnis hängt / Deadlocks


126

Ich habe die folgenden vier Tests und der letzte hängt, wenn ich ihn ausführe. Warum passiert das:

[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;
}

Ich benutze diese Erweiterungsmethode für 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; }
}

Warum hängt der letzte Test?


7
Sie sollten vermeiden, von asynchronen Methoden void zurückzugeben. Dies dient nur der Abwärtskompatibilität mit vorhandenen Ereignishandlern, hauptsächlich im Schnittstellencode. Wenn Ihre asynchrone Methode nichts zurückgibt, sollte sie Task zurückgeben. Ich hatte zahlreiche Probleme mit MSTest und konnte keine asynchronen Tests zurückgeben.
Ghord

2
@ghord: MSTest unterstützt überhaupt keine async voidUnit-Test-Methoden. Sie werden einfach nicht funktionieren. NUnit tut dies jedoch. Trotzdem stimme ich dem allgemeinen Prinzip des Vorzugs async Taskvor async void.
Stephen Cleary

@StephenCleary Ja, obwohl es in Betas von VS2012 erlaubt war, was alle Arten von Problemen verursachte.
Ghord

Antworten:


88

Sie stoßen auf die Standard-Deadlock-Situation, die ich in meinem Blog und in einem MSDN-Artikel beschreibe : Die asyncMethode versucht, ihre Fortsetzung für einen Thread zu planen, der durch den Aufruf von blockiert wird Result.

In diesem Fall SynchronizationContextwird von NUnit async voidTestmethoden ausgeführt. Ich würde async Taskstattdessen versuchen, Testmethoden zu verwenden.


4
Der Wechsel zu asynchron hat funktioniert. Jetzt muss ich den Inhalt Ihrer Links ein paar Mal lesen, ty sir.
Johan Larsson

@MarioLopez: Die Lösung besteht darin, "den asyncganzen Weg" zu verwenden (wie in meinem MSDN-Artikel angegeben). Mit anderen Worten - wie der Titel meines Blogposts besagt - "Blockiere keinen asynchronen Code".
Stephen Cleary

1
@StephenCleary Was ist, wenn ich eine asynchrone Methode in einem Konstruktor aufrufen muss? Konstruktoren können nicht asynchron sein.
Raikol Amaro

1
@StephenCleary In fast allen Ihren Antworten auf SO und in Ihren Artikeln ist alles, worüber Sie jemals sprechen, das Ersetzen Wait()durch das Erstellen der aufrufenden Methode async. Aber für mich scheint dies das Problem in den Vordergrund zu rücken. Irgendwann muss etwas synchron verwaltet werden. Was ist, wenn meine Funktion absichtlich synchron ist, weil sie lange laufende Arbeitsthreads mit verwaltet Task.Run()? Wie warte ich darauf, bis der Vorgang abgeschlossen ist, ohne dass mein NUnit-Test blockiert?
void.pointer

1
@ void.pointer: At some point, something has to be managed synchronously.- überhaupt nicht. Bei UI-Apps kann der Einstiegspunkt ein async voidEreignishandler sein. Bei Server-Apps kann der Einstiegspunkt eine async Task<T>Aktion sein. Es ist vorzuziehen, asyncfür beide zu verwenden, um das Blockieren von Threads zu vermeiden. Ihr NUnit-Test kann synchron oder asynchron sein. Wenn asynchron, machen Sie es async Taskstatt async void. Wenn es synchron ist, sollte es keine haben, SynchronizationContextalso sollte es keinen Deadlock geben.
Stephen Cleary

222

Erfassen eines Werts über eine asynchrone Methode:

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

Synchrones Aufrufen einer asynchronen Methode

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

Durch die Verwendung von Task.Run treten keine Deadlock-Probleme auf.


15
-1, um die Verwendung von async voidUnit-Test-Methoden zu fördern und Garantien für den gleichen Thread zu entfernen, die SynchronizationContextvon dem zu testenden System bereitgestellt werden .
Stephen Cleary

68
@StephenCleary: Es gibt keine "Ermutigung" für asynchrone Leere. Es wird lediglich ein gültiges c # -Konstrukt verwendet, um das Deadlock-Problem zu lösen. Das obige Snippet ist eine unverzichtbare und einfache Lösung für das Problem des OP. Bei Stackoverflow geht es um Lösungen für Probleme, nicht um ausführliche Eigenwerbung.
Herman Schönfeld

81
@StephenCleary: Ihre Artikel artikulieren die Lösung nicht wirklich (zumindest nicht klar) und selbst wenn Sie eine Lösung hätten, würden Sie solche Konstrukte indirekt verwenden. Meine Lösung verwendet keine expliziten Kontexte. Na und? Der Punkt ist, meine funktioniert und es ist ein Einzeiler. Ich brauchte keine zwei Blog-Beiträge und Tausende von Wörtern, um das Problem zu lösen. HINWEIS: Ich verwende nicht einmal asynchrone Leere , daher weiß ich nicht genau, worum es Ihnen geht. Sehen Sie "asynchrone Leere" irgendwo in meiner prägnanten und richtigen Antwort?
Herman Schönfeld

15
@HermanSchoenfeld, wenn Sie das Warum zum Wie hinzufügen , glaube ich, dass Ihre Antwort sehr davon profitieren würde.
Ironstone13

19
Ich weiß, dass dies etwas spät ist, aber Sie sollten es .GetAwaiter().GetResult()stattdessen verwenden, .Resultdamit Exceptiones nicht verpackt wird.
Camilo Terevinto

15

Sie können Deadlocks vermeiden ConfigureAwait(false), die dieser Zeile hinzugefügt werden:

IRestResponse<DummyServiceStatus> response = await restResponse;

=>

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

Ich habe diese Fallstricke in meinem Blog-Beitrag Fallstricke von Async / Warten beschrieben


9

Sie blockieren die Benutzeroberfläche mithilfe der Task.Result-Eigenschaft. In der MSDN-Dokumentation haben sie klar erwähnt, dass

"Die Result- Eigenschaft ist eine blockierende Eigenschaft. Wenn Sie versuchen, vor Abschluss der Aufgabe darauf zuzugreifen, wird der aktuell aktive Thread blockiert, bis die Aufgabe abgeschlossen ist und der Wert verfügbar ist. In den meisten Fällen sollten Sie mit Await auf den Wert zugreifen oder warten, anstatt direkt auf die Unterkunft zuzugreifen. "

Die beste Lösung für dieses Szenario wäre , sowohl das Warten als auch das Asynchronisieren aus den Methoden zu entfernen und nur die Aufgabe zu verwenden, bei der Sie das Ergebnis zurückgeben. Es wird Ihre Ausführungssequenz nicht durcheinander bringen.


3

Wenn Sie keine Rückrufe erhalten oder das Steuerelement auflegt, müssen Sie nach dem Aufrufen der asynchronen Funktion service / API Context so konfigurieren, dass ein Ergebnis für denselben aufgerufenen Kontext zurückgegeben wird.

Verwenden TestAsync().ConfigureAwait(continueOnCapturedContext: false);

Dieses Problem tritt nur in Webanwendungen auf, nicht jedoch in static void main.


ConfigureAwaitVermeidet Deadlocks in bestimmten Szenarien, indem sie nicht im ursprünglichen Thread-Kontext ausgeführt werden.
Davidcarr
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.