379 Stimmen

Synchrones Warten auf eine asynchrone Operation, 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-based Asynchronous Pattern (TAP), async und await zu studieren, bin ich immer noch etwas verwirrt über einige der feineren Details.

Ich schreibe einen Logger für Windows Store Apps, und ich möchte sowohl asynchrone als auch 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 Hauptmethode 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 korrekt 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();
}

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, 2 und 3 sind die falschen Werkzeuge. Aber 1? Was ist falsch an 1 und was ist der Unterschied zu 4? Warum verursacht 1 ein Einfrieren? Gibt es ein Problem mit dem Aufgabenobjekt? Gibt es eine nicht offensichtliche Blockierung?

230voto

SLaks Punkte 832502

En await innerhalb 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, kommt es zu einem Deadlock.

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

Alternativ können Sie auch StartAsTask().ConfigureAwait(false) bevor der innere Vorgang abgewartet wird, damit er zum Thread-Pool und nicht zum UI-Thread zurückkehrt, wodurch die Blockierung vollständig vermieden wird.

54voto

Stephen Cleary Punkte 402664

Aufruf von async Code von synchronem Code zu unterscheiden, kann ziemlich knifflig sein.

Ich erkläre die vollständige Gründe für diese Blockade in meinem Blog . Kurz gesagt, es gibt einen "Kontext", der standardmäßig zu Beginn jeder Sitzung gespeichert wird. await und verwendet, um die Methode fortzusetzen.

Wenn dies also in einem UI-Kontext aufgerufen wird, wenn die await vervollständigt, wird die async Methode versucht, wieder in diesen Kontext einzutreten, um die Ausführung fortzusetzen. Leider kann Code, der Wait (oder Result ) wird einen Thread in diesem Kontext blockieren, so dass die async Methode nicht abgeschlossen werden kann.

Die Leitlinien, um dies zu vermeiden, sind:

  1. Utilice ConfigureAwait(continueOnCapturedContext: false) so viel wie möglich. Dies ermöglicht Ihrem async Methoden weiter ausgeführt werden können, ohne dass der Kontext erneut eingegeben werden muss.
  2. Utilice async den ganzen Weg. Verwenden Sie await anstelle von Result o Wait .

Wenn Ihre Methode natürlich asynchron ist, dann Sie sollten (wahrscheinlich) keinen synchronen Wrapper bereitstellen .

7voto

pixel Punkte 8064

Das habe ich getan

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 hervorragend und blockiert nicht den UI-Thread

0voto

codefox Punkte 120

Mit einem kleinen benutzerdefinierten Synchronisierungskontext kann die sync-Funktion auf die Fertigstellung der asynchronen Funktion warten, ohne eine Blockade zu verursachen. Hier ist ein kleines Beispiel für eine WinForms-Anwendung.

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

0voto

ALEX-74-DE Punkte 11

Für mich eigentlich die beste funktionierende Lösung:

AsyncMethod(<params>).ConfigureAwait(true).GetAwaiter().GetResult();

Funktioniert auch bei UI-Content ohne Blockier- und Dispatcher-Probleme und auch von CTOR's.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X