Attente synchrone pour une opération asynchrone, et pourquoi Wait () bloque le programme ici

Preface : je cherche une explication, pas seulement une solution. Je connais déjà la solution.

bien que j'aie passé plusieurs jours à étudier les articles de MSDN sur le modèle asynchrone basé sur les tâches (TAP), async et wait, je suis encore un peu confus au sujet de certains détails plus fins.

j'écris un logger pour les applications Windows Store, et je veux prendre en charge la journalisation asynchrone et synchrone. L'asynchrone les méthodes suivent le robinet, les synchrones devraient cacher tout cela, et regarder et travailler comme des méthodes ordinaires.

C'est le cœur de la méthode asynchrone de la journalisation:

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

maintenant la méthode synchrone correspondante...

Version 1 :

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

cela semble correct, mais cela ne fonctionne pas. Tout le programme est figé pour toujours.

Version 2 :

Hmm.. Peut-être que la tâche n'a pas commencé?

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

cela jette InvalidOperationException: Start may not be called on a promise-style task.

de la Version 3:

Hmm.. Task.RunSynchronously semble prometteur.

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

cela jette 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 (la solution):

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

ça marche. Donc, 2 et 3 sont les mauvais outils. Mais 1? Qu'est-ce qui ne va pas avec 1 et quelle est la différence avec 4? Qu'est-ce qui fait que 1 provoque un gel? Est-il un problème avec la tâche de l'objet? Y a-t-il une impasse non évidente?

Aidez-moi à comprendre.

257
demandé sur Gennady Vanin Геннадий Ванин 2013-01-23 20:56:39

4 réponses

le await à l'intérieur de votre méthode asynchrone essaie de revenir au fil UI.

puisque le fil UI est occupé à attendre que la tâche complète, vous avez une impasse.

déplacer l'appel async vers Task.Run() résout le problème.

Parce que l'appel async tourne maintenant sur un thread pool, il n'essaie pas de revenir au thread UI, et tout fonctionne donc.

alternativement, vous pouvez appeler StartAsTask().ConfigureAwait(false) avant d'attendre l'opération intérieure pour la faire revenir à la masse de fil plutôt que le fil UI, en évitant l'impasse entièrement.

153
répondu SLaks 2018-07-01 12:42:50

appeler async le code du code synchrone peut être assez délicat.

j'explique les raisons complètes de cette impasse sur mon blog . En bref, il y a un "contexte" qui est enregistré par défaut au début de chaque await et pour reprendre la méthode.

donc si cela est appelé dans un contexte D'UI, lorsque la await est terminée, la méthode async essaie de revenir dans ce contexte pour continuer exécuter. Malheureusement, le code utilisant Wait (ou Result ) va bloquer un thread dans ce contexte, de sorte que la méthode async ne peut pas être complète.

Les lignes directrices pour éviter ce sont:

  1. utiliser ConfigureAwait(continueOnCapturedContext: false) autant que possible. Cela permet à vos méthodes async de continuer à exécuter sans avoir à entrer de nouveau dans le contexte.
  2. Utiliser async tout le chemin. Utilisez await au lieu de Result ou Wait .

si votre méthode est naturellement asynchrone, alors vous (probablement) ne devriez pas exposer une enveloppe synchrone .

45
répondu Stephen Cleary 2017-03-08 08:55:34

voici ce que j'ai fait

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();
}

fonctionne très bien et ne bloque pas thread d'INTERFACE utilisateur

4
répondu pixel 2016-10-25 23:34:24

avec un petit contexte de synchronisation personnalisé, la fonction de synchronisation peut attendre l'achèvement de la fonction async, sans créer d'impasse. Voici un petit exemple pour L'application WinForms.

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
répondu codefox 2016-09-27 13:27:06