wake-up-neo.net

Warten Sie synchron auf einen asynchronen Vorgang und warum friert Wait () das Programm hier ein

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

Obwohl ich mehrere Tage damit verbracht habe, MSDN-Artikel über das Task-basierte Asynchronous Pattern (TAP), async und wartend zu studieren, 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 der TAP, die synchronen Methoden sollten all dies ausblenden und wie gewöhnliche 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 zwar richtig aus, funktioniert aber nicht. Das gesamte Programm bleibt für immer stehen.

Version 2 :

Hmm .. Vielleicht wurde die Aufgabe nicht gestartet?

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

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

Version 3:

Hmm .. Task.RunSynchronously klingt vielversprechend.

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

Dies 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. Also sind 2 und 3 die falschen Werkzeuge. Aber 1? Was ist los mit 1 und was ist der Unterschied zu 4? Was bewirkt, dass ich einfriere? Gibt es ein Problem mit dem Aufgabenobjekt? Gibt es einen nicht offensichtlichen Stillstand?

271

Die Variable await in Ihrer asynchronen Methode versucht, zum UI-Thread zurückzukehren.

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

Durch Verschieben des asynchronen Aufrufs nach Task.Run() wird das Problem gelöst.
Da der async-Aufruf jetzt in einem Threadpool-Thread ausgeführt wird, versucht er nicht, zum UI-Thread zurückzukehren, und alles funktioniert daher.

Alternativ können Sie StartAsTask().ConfigureAwait(false) aufrufen, bevor Sie auf den inneren Vorgang warten, um ihn zum Thread-Pool statt zum UI-Thread zurückzukehren, wodurch der Deadlock vollständig vermieden wird. 

162
SLaks

Das Aufrufen von async-Code aus synchronem Code kann ziemlich schwierig sein.

Ich erkläre die vollständige Gründe für diesen Deadlock in meinem Blog . Kurz gesagt, es gibt einen "Kontext", der standardmäßig am Anfang jeder await gespeichert und zum Fortsetzen der Methode verwendet wird.

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

Die Richtlinien, um dies zu vermeiden, sind:

  1. Verwenden Sie ConfigureAwait(continueOnCapturedContext: false) so oft wie möglich. Dadurch können Ihre async-Methoden mit der Ausführung fortfahren, ohne den Kontext erneut eingeben zu müssen.
  2. Verwenden Sie async vollständig. Verwenden Sie await anstelle von Result oder Wait.

Wenn Ihre Methode natürlich asynchron ist, sollte Sie sollten (wahrscheinlich) keinen synchronen Wrapper verfügbar machen .

45
Stephen Cleary

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

4
pixel

Mit einem kleinen benutzerdefinierten Synchronisationskontext kann die Synchronisierungsfunktion auf die Beendigung der Asynchronfunktion warten, ohne einen Deadlock zu erstellen. 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
0
codefox