'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?

Johan Larsson
quelle
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.

Stephen Cleary
quelle
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.

Herman Schönfeld
quelle
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

Vladimir
quelle
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.

Dunkler Ritter
quelle
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.

Mayank Pandit
quelle
ConfigureAwaitVermeidet Deadlocks in bestimmten Szenarien, indem sie nicht im ursprünglichen Thread-Kontext ausgeführt werden.
Davidcarr