Esperando sincrónicamente una operación asíncrona, y por qué Wait() congela el programa aquí


Prefacio : Estoy buscando una explicación, no solo una solución. Ya conozco la solución.

A pesar de haber pasado varios días estudiando los artículos de MSDN sobre el Patrón Asincrónico basado en Tareas (TAP), async y await, todavía estoy un poco confundido sobre algunos de los detalles más finos.

Estoy escribiendo un registrador para aplicaciones de la tienda Windows, y quiero admitir el registro asíncrono y síncrono. Los métodos asíncronos siguen el TAP, los síncronos deben ocultar todo esto, y mirar y trabajar como métodos ordinarios.

Este es el método principal de registro asíncrono:

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

Ahora el método síncrono correspondiente...

Versión 1:

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

Esto parece correcto, pero no funciona. Todo el programa se congela para siempre.

Versión 2:

Hmm.. Tal vez la tarea no se inició?

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

Esto lanza InvalidOperationException: Start may not be called on a promise-style task.

Versión 3:

Hmm.. Task.RunSynchronously sonidos prometedor.

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

Esto lanza InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.

Versión 4 (la solución):

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

Esto funciona. Por lo tanto, 2 y 3 son las herramientas equivocadas. Pero 1? ¿Qué hay de malo con 1 y cuál es la diferencia con 4? ¿Qué hace que 1 cause una congelación? ¿Hay algún problema con el objeto task? ¿Hay un punto muerto no obvio?

Por favor ayúdame a entender.

4 answers

El await dentro de su método asíncrono está tratando de volver al subproceso de la interfaz de usuario.

Dado que el subproceso de interfaz de usuario está ocupado esperando que se complete toda la tarea, tiene un punto muerto.

Mover la llamada asincrónica a Task.Run() resuelve el problema.
Debido a que la llamada asincrónica ahora se está ejecutando en un subproceso de grupo de subprocesos, no intenta volver al subproceso de interfaz de usuario, y por lo tanto todo funciona.

Alternativamente, puedes llamar a StartAsTask().ConfigureAwait(false) antes de esperar la operación interna para hacerla venir volver al grupo de subprocesos en lugar del subproceso de interfaz de usuario, evitando el bloqueo por completo.

 153
Author: SLaks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-01 12:42:50

Llamar a async código desde código síncrono puede ser bastante complicado.

Explico las razones completas para este punto muerto en mi blog . En resumen, hay un "contexto" que se guarda por defecto al principio de cada await y se usa para reanudar el método.

Así que si esto se llama en un contexto UI, cuando el await se completa, el método async intenta volver a entrar en ese contexto para continuar ejecutando. Desafortunadamente, el código que usa Wait (o Result) bloqueará un hilo en ese contexto, así que el método async no puede completarse.

Las pautas para evitar esto son:

  1. Use ConfigureAwait(continueOnCapturedContext: false) tanto como sea posible. Esto permite que sus métodos async continúen ejecutándose sin tener que volver a entrar en el contexto.
  2. Use async todo el camino. Utilice await en lugar de Result o Wait.

Si su método es naturalmente asíncrono, entonces usted (probablemente) no debería exponer un wrapper síncrono.

 45
Author: Stephen Cleary,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-08 08:55:34

Esto es lo que hice

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

Funciona muy bien y no bloquea el hilo de la interfaz de usuario

 4
Author: pixel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-25 23:34:24

Con un pequeño contexto de sincronización personalizado, la función de sincronización puede esperar a que se complete la función asíncrona, sin crear un punto muerto. Aquí hay un pequeño ejemplo para la aplicación 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
Author: codefox,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-27 13:27:06