354 votes

Attente synchrone d'une opération asynchrone, et pourquoi Wait() fige le programme ici

Préface : 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 await, je suis encore un peu perdu sur certains détails plus fins.

J'écris un enregistreur pour Windows Store Apps, et je veux prendre en charge l'enregistrement asynchrone et synchrone. Les méthodes asynchrones suivent le TAP, les méthodes synchrones devraient cacher tout cela, et ressembler et fonctionner comme des méthodes ordinaires.

Il s'agit de la méthode de base 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 fige indéfiniment.

Version 2 :

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

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

Cela fonctionne. 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 ? Pourquoi 1 provoque-t-il un blocage ? Y a-t-il un problème avec l'objet tâche ? Y a-t-il un blocage non évident ?

0 votes

Avez-vous réussi à obtenir une explication ailleurs ? Les réponses ci-dessous n'apportent pas vraiment d'éclaircissement. J'utilise en fait .net 4.0 et non 4.5/5, je ne peux donc pas utiliser certaines des opérations mais je rencontre les mêmes problèmes.

3 votes

@amadib, les ver.1 et 4 ont été expliqués dans les réponses [rpvided]. Les ver.2 et 3 essaient de recommencer une tâche déjà commencée. Postez votre question. Il n'est pas clair comment vous pouvez avoir des problèmes d'async/await dans .NET 4.5 sur .NET 4.0.

2 votes

La version 4 est la meilleure option pour Xamarin Forms. Nous avons essayé les autres options, mais elles n'ont pas fonctionné et nous avons rencontré des blocages dans tous les cas.

214voto

SLaks Points 391154

El await à l'intérieur de votre méthode asynchrone essaie de revenir au thread de l'interface utilisateur.

Puisque le thread de l'interface utilisateur est occupé à attendre que la tâche entière se termine, vous avez un blocage.

Déplacer l'appel asynchrone vers Task.Run() résout le problème.
Comme l'appel asynchrone s'exécute maintenant sur un thread de pool, il n'essaie pas de revenir sur le thread de l'interface utilisateur, et tout fonctionne donc.

Alternativement, vous pouvez appeler StartAsTask().ConfigureAwait(false) avant d'attendre l'opération interne pour qu'elle revienne au pool de threads plutôt qu'au thread de l'interface utilisateur, ce qui évite complètement le blocage.

11 votes

+1. Voici une explication supplémentaire - L'attente, et l'interface utilisateur, et les blocages ! Oh là là !

14 votes

El ConfigureAwait(false) est la solution appropriée dans ce cas. Puisqu'elle n'a pas besoin d'appeler les callbacks dans le contexte capturé, elle ne devrait pas le faire. Comme il s'agit d'une méthode API, elle devrait le gérer en interne, plutôt que de forcer tous les appelants à sortir du contexte de l'interface utilisateur.

0 votes

@Servy Je vous pose la question puisque vous avez mentionné ConfigureAwait. J'utilise .net3.5 et j'ai dû supprimer ConfigureAwait car il n'était pas disponible dans la bibliothèque asynchrone que j'utilisais. Comment puis-je écrire le mien ou existe-t-il un autre moyen d'attendre mon appel asynchrone ? Car ma méthode se bloque aussi. Je n'ai pas de Task mais pas de Task.Run. Cela devrait probablement être une question à part entière.

54voto

Stephen Cleary Points 91731

Appel à async du code synchrone peut être assez délicat.

J'explique le toutes les raisons de cette impasse sur mon blog . En bref, il y a un "contexte" qui est sauvegardé par défaut au début de chaque opération de l await et utilisé pour reprendre la méthode.

Ainsi, si cette fonction est appelée dans un contexte d'interface utilisateur, lorsque la fonction await s'achève, le async tente de réintégrer ce contexte pour poursuivre son exécution. Malheureusement, le code utilisant Wait (ou Result ) bloquera un thread dans ce contexte, de sorte que l'option async ne peut pas se terminer.

Les directives pour éviter cela sont les suivantes :

  1. Utilisez ConfigureAwait(continueOnCapturedContext: false) autant que possible. Cela permet à votre async de poursuivre l'exécution des méthodes sans avoir à entrer à nouveau dans le contexte.
  2. Utilisez async jusqu'au bout. Utilisez await au lieu de Result o Wait .

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

0 votes

J'ai besoin d'exécuter une tâche async dans un catch() qui ne prend pas en charge async comment faire pour éviter une situation de feu et d'oubli.

2 votes

@Zapnologica : await est pris en charge dans catch à partir de VS2015. Si vous êtes sur une version antérieure, vous pouvez assigner l'exception à une variable locale et faire l'opération suivante await après le bloc catch .

7voto

pixel Points 2223

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 parfaitement et ne bloque pas le thread de l'interface utilisateur

0voto

codefox Points 110

Avec un petit contexte de synchronisation personnalisé, la fonction sync peut attendre l'achèvement de la fonction async, sans créer d'impasse. Voici un petit exemple pour une 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

-1voto

ALEX-74-DE Points 11

Pour moi, c'est la meilleure solution :

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

Fonctionne également sur UI-Content sans problèmes de blocage et de répartiteur, et aussi de CTOR.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X