Synchrones Warten auf eine asynchrone Operation und warum friert Wait () das Programm hier ein

318

Vorwort : Ich suche eine Erklärung, nicht nur eine Lösung. Ich kenne die Lösung bereits.

Obwohl ich mehrere Tage damit verbracht habe, MSDN-Artikel über das aufgabenbasierte asynchrone Muster (TAP) zu studieren, asynchron zu sein und zu warten, bin ich immer noch etwas verwirrt über einige der feineren Details.

Ich schreibe einen Logger für Windows Store Apps und möchte sowohl die asynchrone als auch die synchrone Protokollierung unterstützen. Die asynchronen Methoden folgen dem TAP, die synchronen sollten all dies verbergen und wie normale Methoden aussehen und funktionieren.

Dies ist die Kernmethode der asynchronen Protokollierung:

private async Task WriteToLogAsync(string text)
{
    StorageFolder folder = ApplicationData.Current.LocalFolder;
    StorageFile file = await folder.CreateFileAsync("log.log",
        CreationCollisionOption.OpenIfExists);
    await FileIO.AppendTextAsync(file, text,
        Windows.Storage.Streams.UnicodeEncoding.Utf8);
}

Nun die entsprechende synchrone Methode ...

Version 1 :

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Wait();
}

Das sieht richtig aus, funktioniert aber nicht. Das ganze Programm friert für immer ein.

Version 2 :

Hmm .. Vielleicht wurde die Aufgabe nicht gestartet?

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Start();
    task.Wait();
}

Das wirft InvalidOperationException: Start may not be called on a promise-style task.

Version 3:

Hmm .. Task.RunSynchronouslyklingt vielversprechend.

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.RunSynchronously();
}

Das wirft InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.

Version 4 (die Lösung):

private void WriteToLog(string text)
{
    var task = Task.Run(async () => { await WriteToLogAsync(text); });
    task.Wait();
}

Das funktioniert. 2 und 3 sind also die falschen Werkzeuge. Aber 1? Was ist los mit 1 und was ist der Unterschied zu 4? Was bringt 1 zum Einfrieren? Gibt es ein Problem mit dem Aufgabenobjekt? Gibt es einen nicht offensichtlichen Stillstand?

Sebastian Negraszus
quelle
Hast du Glück, woanders eine Erklärung zu bekommen? Die Antworten unten geben wirklich keinen Einblick. Ich verwende tatsächlich .net 4.0, nicht 4.5 / 5, daher kann ich einige der Vorgänge nicht verwenden, aber es treten dieselben Probleme auf.
Amadib
3
@amadib, Version 1 und 4 wurden in den Antworten erläutert. Ver.2 und 3 versuchen, die bereits gestartete Aufgabe erneut zu starten. Stellen Sie Ihre Frage. Es ist unklar, wie Sie .NET 4.5 asynchrone / wartende Probleme unter .NET 4.0 haben können
Gennady Vanin Геннадий Ванин
1
Version 4 ist die beste Option für Xamarin Forms. Wir haben den Rest der Optionen ausprobiert und nicht in allen Fällen funktioniert und Deadlocks erlebt
Ramakrishna
Vielen Dank! Version 4 hat bei mir funktioniert. Aber läuft es immer noch asynchron? Ich gehe davon aus, weil das asynchrone Schlüsselwort vorhanden ist.
Sshirley

Antworten:

189

In awaitIhrer asynchronen Methode wird versucht, zum UI-Thread zurückzukehren.

Da der UI-Thread damit beschäftigt ist, auf den Abschluss der gesamten Aufgabe zu warten, liegt ein Deadlock vor.

Durch Verschieben des asynchronen Aufrufs wird Task.Run()das Problem behoben .
Da der asynchrone Aufruf jetzt in einem Thread-Pool-Thread ausgeführt wird, wird nicht versucht, zum UI-Thread zurückzukehren, und daher funktioniert alles.

Alternativ können Sie aufrufen, StartAsTask().ConfigureAwait(false)bevor Sie auf die innere Operation warten, damit sie zum Thread-Pool und nicht zum UI-Thread zurückkehrt, wodurch der Deadlock vollständig vermieden wird.

SLaks
quelle
9
+1. Hier ist noch eine Erklärung - Warten und Benutzeroberfläche und Deadlocks! Oh mein!
Alexei Levenkov
13
Das ConfigureAwait(false)ist die geeignete Lösung in diesem Fall. Da die Rückrufe im erfassten Kontext nicht aufgerufen werden müssen, sollte dies nicht der Fall sein. Als API-Methode sollte sie intern behandelt werden, anstatt alle Aufrufer zu zwingen, den UI-Kontext zu verlassen.
Servy
@Servy Ich frage, seit Sie ConfigureAwait erwähnt haben. Ich verwende .net3.5 und musste configure await entfernen, da es in der von mir verwendeten asynchronen Bibliothek nicht verfügbar war. Wie schreibe ich meine eigenen oder gibt es eine andere Möglichkeit, auf meinen asynchronen Anruf zu warten? Weil meine Methode auch hängt. Ich habe keine Aufgabe, aber keine Aufgabe. Dies sollte wahrscheinlich eine Frage für sich sein.
Flexxxit
@flexxxit: Du solltest verwenden Microsoft.Bcl.Async.
SLaks
48

Das Aufrufen von asyncCode aus synchronem Code kann sehr schwierig sein.

Die vollständigen Gründe für diesen Deadlock erkläre ich in meinem Blog . Kurz gesagt, es gibt einen "Kontext", der standardmäßig am Anfang jedes Kontextes gespeichert awaitund zum Fortsetzen der Methode verwendet wird.

Wenn dies in einem UI-Kontext aufgerufen wird await, asyncversucht die Methode nach Abschluss des Vorgangs, diesen Kontext erneut einzugeben, um die Ausführung fortzusetzen. Leider blockiert Code mit Wait(oder Result) einen Thread in diesem Kontext, sodass die asyncMethode nicht abgeschlossen werden kann.

Die Richtlinien, um dies zu vermeiden, sind:

  1. Verwenden Sie ConfigureAwait(continueOnCapturedContext: false)so viel wie möglich. Auf diese Weise können Ihre asyncMethoden weiter ausgeführt werden, ohne den Kontext erneut eingeben zu müssen.
  2. Verwenden Sie den asyncganzen Weg. Verwenden Sie awaitanstelle von Resultoder Wait.

Wenn Ihre Methode von Natur aus asynchron ist, sollten Sie (wahrscheinlich) keinen synchronen Wrapper verfügbar machen .

Stephen Cleary
quelle
Ich muss eine Async-Aufgabe in einem catch () ausführen, die nicht unterstützt, asyncwie ich dies tun und ein Feuer verhindern und eine Situation vergessen würde.
Zapnologica
1
@Zapnologica: awaitwird ab catchVS2015 blockweise unterstützt . Wenn Sie eine ältere Version verwenden, können Sie die Ausnahme einer lokalen Variablen zuweisen und awaitden Block nach dem Catch ausführen .
Stephen Cleary
5

Hier ist was ich getan habe

private void myEvent_Handler(object sender, SomeEvent e)
{
  // I dont know how many times this event will fire
  Task t = new Task(() =>
  {
    if (something == true) 
    {
        DoSomething(e);  
    }
  });
  t.RunSynchronously();
}

funktioniert super und blockiert nicht den UI-Thread

Pixel
quelle
0

In einem kleinen benutzerdefinierten Synchronisationskontext kann die Synchronisierungsfunktion auf den Abschluss der Asynchronisierungsfunktion warten, ohne einen Deadlock zu verursachen. Hier ist ein kleines Beispiel für die WinForms-App.

Imports System.Threading
Imports System.Runtime.CompilerServices

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        SyncMethod()
    End Sub

    ' waiting inside Sync method for finishing async method
    Public Sub SyncMethod()
        Dim sc As New SC
        sc.WaitForTask(AsyncMethod())
        sc.Release()
    End Sub

    Public Async Function AsyncMethod() As Task(Of Boolean)
        Await Task.Delay(1000)
        Return True
    End Function

End Class

Public Class SC
    Inherits SynchronizationContext

    Dim OldContext As SynchronizationContext
    Dim ContextThread As Thread

    Sub New()
        OldContext = SynchronizationContext.Current
        ContextThread = Thread.CurrentThread
        SynchronizationContext.SetSynchronizationContext(Me)
    End Sub

    Dim DataAcquired As New Object
    Dim WorkWaitingCount As Long = 0
    Dim ExtProc As SendOrPostCallback
    Dim ExtProcArg As Object

    <MethodImpl(MethodImplOptions.Synchronized)>
    Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
        Interlocked.Increment(WorkWaitingCount)
        Monitor.Enter(DataAcquired)
        ExtProc = d
        ExtProcArg = state
        AwakeThread()
        Monitor.Wait(DataAcquired)
        Monitor.Exit(DataAcquired)
    End Sub

    Dim ThreadSleep As Long = 0

    Private Sub AwakeThread()
        If Interlocked.Read(ThreadSleep) > 0 Then ContextThread.Resume()
    End Sub

    Public Sub WaitForTask(Tsk As Task)
        Dim aw = Tsk.GetAwaiter

        If aw.IsCompleted Then Exit Sub

        While Interlocked.Read(WorkWaitingCount) > 0 Or aw.IsCompleted = False
            If Interlocked.Read(WorkWaitingCount) = 0 Then
                Interlocked.Increment(ThreadSleep)
                ContextThread.Suspend()
                Interlocked.Decrement(ThreadSleep)
            Else
                Interlocked.Decrement(WorkWaitingCount)
                Monitor.Enter(DataAcquired)
                Dim Proc = ExtProc
                Dim ProcArg = ExtProcArg
                Monitor.Pulse(DataAcquired)
                Monitor.Exit(DataAcquired)
                Proc(ProcArg)
            End If
        End While

    End Sub

     Public Sub Release()
         SynchronizationContext.SetSynchronizationContext(OldContext)
     End Sub

End Class
Codefox
quelle