Attente synchrone d'une opération asynchrone, et pourquoi Wait () gèle-t-il le programme ici

318

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

Malgré avoir passé plusieurs jours à étudier des articles MSDN sur le modèle asynchrone basé sur les tâches (TAP), asynchroniser et attendre, je suis toujours un peu confus au sujet de certains des détails les plus fins.

J'écris un enregistreur pour les applications du Windows Store et je souhaite prendre en charge la journalisation asynchrone et synchrone. Les méthodes asynchrones suivent le TAP, les méthodes synchrones doivent masquer tout cela et ressembler et fonctionner comme des méthodes ordinaires.

Il s'agit de la méthode principale de la journalisation asynchrone:

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. L'ensemble du programme se bloque 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.

Version 3:

Hmm .. Task.RunSynchronouslysemble 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();
}

Cela marche. Donc, 2 et 3 ne sont pas les bons outils. Mais 1? Quel est le problème avec 1 et quelle est la différence avec 4? Qu'est-ce qui fait que 1 cause un gel? Y a-t-il un problème avec l'objet de tâche? Y a-t-il un blocage non évident?

Sebastian Negraszus
la source
Avez-vous de la chance d'avoir une explication ailleurs? Les réponses ci-dessous ne fournissent vraiment aucun aperçu. J'utilise en fait .net 4.0 et non 4.5 / 5, donc je ne peux pas utiliser certaines des opérations mais je rencontre les mêmes problèmes.
2013
3
@amadib, ver.1 et 4 ont été expliqués dans [réponses fournies. Ver.2 anв 3 essayez de recommencer la tâche déjà commencée. Postez votre question. On ne sait pas comment vous pouvez avoir des problèmes asynchrones / en attente de .NET 4.5 sur .NET 4.0
Gennady Vanin Геннадий Ванин
1
La version 4 est la meilleure option pour Xamarin Forms. Nous avons essayé le reste des options et n'avons pas fonctionné et nous avons connu des blocages dans tous les cas
Ramakrishna
Merci! La version 4 a fonctionné pour moi. Mais fonctionne-t-il toujours de manière asynchrone? Je suppose que oui parce que le mot-clé async est là.
sshirley

Réponses:

189

L' awaitintérieur de votre méthode asynchrone tente de revenir au thread d'interface utilisateur.

Étant donné que le thread d'interface utilisateur est occupé à attendre la fin de la tâche, vous avez un blocage.

Déplacer l'appel asynchrone pour Task.Run()résoudre le problème.
Étant donné que l'appel asynchrone s'exécute maintenant sur un thread de pool de threads, il n'essaie pas de revenir au thread d'interface utilisateur, et tout fonctionne donc.

Alternativement, vous pouvez appeler StartAsTask().ConfigureAwait(false)avant d'attendre l'opération interne pour la faire revenir au pool de threads plutôt qu'au thread d'interface utilisateur, en évitant complètement le blocage.

SLaks
la source
9
+1. Voici une autre explication - Attendez, et l'interface utilisateur, et les blocages! Oh mon!
Alexei Levenkov
13
C'est ConfigureAwait(false)la solution appropriée dans ce cas. Puisqu'il n'a pas besoin d'appeler les rappels dans le contexte capturé, il ne devrait pas. Étant une méthode API, elle doit la gérer en interne, plutôt que de forcer tous les appelants à sortir du contexte de l'interface utilisateur.
Servy
@Servy Je demande depuis que vous avez mentionné ConfigureAwait. J'utilise .net3.5 et j'ai dû supprimer configure configure cos car il n'était pas disponible dans la bibliothèque asynchrone que j'utilisais. Comment puis-je écrire le mien ou existe-t-il une autre façon d'attendre mon appel asynchrone? Parce que ma méthode se bloque aussi. Je n'ai pas Task mais pas Task.Run. Cela devrait probablement être une question en soi.
flexxxit
@flexxxit: Vous devez utiliser Microsoft.Bcl.Async.
SLaks
48

Appeler du asynccode à partir d'un 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 chacun awaitet utilisé pour reprendre la méthode.

Donc, si cela est appelé dans un contexte d'interface utilisateur, une fois l'opération awaitterminée, la asyncméthode essaie de ressaisir ce contexte pour continuer l'exécution. Malheureusement, le code utilisant Wait(ou Result) bloquera un thread dans ce contexte, la asyncméthode ne peut donc pas se terminer.

Les directives pour éviter cela sont les suivantes:

  1. Utilisez ConfigureAwait(continueOnCapturedContext: false)autant que possible. Cela permet à vos asyncméthodes de continuer à s'exécuter sans avoir à ressaisir le contexte.
  2. Utilisez asynctout le chemin. Utilisez awaitau lieu de Resultou Wait.

Si votre méthode est naturellement asynchrone, vous (probablement) ne devriez pas exposer un wrapper synchrone .

Stephen Cleary
la source
J'ai besoin d'exécuter une tâche asynchrone dans un catch () qui ne prend pas en charge asynccomment je pourrais faire cela et empêcher un incendie et oublier la situation.
Zapnologica
1
@Zapnologica: awaitest pris en charge dans les catchblocs à partir de VS2015. Si vous êtes sur une version plus ancienne, vous pouvez affecter l'exception à une variable locale et faire le awaitbloc après le catch .
Stephen Cleary
5

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 le thread d'interface utilisateur

pixel
la source
0

Avec un petit contexte de synchronisation personnalisé, la fonction de synchronisation peut attendre la fin de la fonction asynchrone, sans créer de blocage. 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
codefox
la source