Sincrónicamente esperando una operación asincrónica, y por qué Wait () congela el programa aquí

318

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 artículos de MSDN sobre el patrón asincrónico basado en tareas (TAP), asíncrono y esperar, todavía estoy un poco confundido acerca de algunos de los detalles más finos.

Estoy escribiendo un registrador para las aplicaciones de la Tienda Windows, y quiero admitir el registro asíncrono y sincrónico. Los métodos asincrónicos siguen el TAP, los síncronos deben ocultar todo esto, y verse y funcionar como los métodos ordinarios.

Este es el método principal de registro asincrónico:

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 sincrónico correspondiente ...

Version 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 arroja InvalidOperationException: Start may not be called on a promise-style task.

Versión 3:

Hmm ... Task.RunSynchronouslysuena prometedor.

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

Esto arroja 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. Entonces, 2 y 3 son las herramientas incorrectas. Pero 1? ¿Qué le pasa a 1 y cuál es la diferencia con 4? ¿Qué hace que 1 cause un congelamiento? ¿Hay algún problema con el objeto de tarea? ¿Hay un punto muerto no obvio?

Sebastian Negraszus
fuente
¿Alguna suerte obteniendo una explicación en otro lado? Las respuestas a continuación realmente no proporcionan información. En realidad estoy usando .net 4.0 no 4.5 / 5, así que no puedo usar algunas de las operaciones, pero me encuentro con los mismos problemas.
amadib
3
@amadib, ver.1 y 4 se explicaron en [respuestas rpvidedas. Ver.2 y 3 intente comenzar de nuevo la tarea ya iniciada. Publica tu pregunta. No está claro cómo puede tener .NET 4.5 async / esperar problemas en .NET 4.0
Gennady Vanin Геннадий Ванин 05 de
1
La versión 4 es la mejor opción para Xamarin Forms. Probamos el resto de las opciones y no trabajamos y experimentamos puntos muertos en todos los casos
Ramakrishna
¡Gracias! La versión 4 funcionó para mí. ¿Pero todavía se ejecuta de forma asincrónica? Supongo que sí porque la palabra clave asincrónica está ahí.
sshirley

Respuestas:

189

El awaitinterior de su método asincrónico está tratando de volver al hilo de la interfaz de usuario.

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

Mover la llamada asincrónica para Task.Run()resolver el problema.
Debido a que la llamada asíncrona ahora se ejecuta en un subproceso de grupo de subprocesos, no intenta volver al subproceso de la interfaz de usuario y, por lo tanto, todo funciona.

Alternativamente, puedes llamar StartAsTask().ConfigureAwait(false) antes de esperar la operación interna para que regrese al grupo de subprocesos en lugar del subproceso de la interfaz de usuario, evitando por completo el punto muerto.

SLaks
fuente
99
+1. Aquí hay una explicación más: ¡ Espera, interfaz de usuario y puntos muertos! ¡Oh mi!
Alexei Levenkov
13
La ConfigureAwait(false)es la solución adecuada en este caso. Como no tiene necesidad de llamar a las devoluciones de llamada en el contexto capturado, no debería hacerlo. Al ser un método API, debe manejarlo internamente, en lugar de obligar a todas las personas que llaman a salir del contexto de la interfaz de usuario.
Servy
@Servy Estoy preguntando desde que mencionaste ConfigureAwait. Estoy usando .net3.5 y tuve que eliminar la configuración en espera porque no estaba disponible en la biblioteca asincrónica que estaba usando. ¿Cómo escribo el mío o hay otra forma de esperar mi llamada asincrónica? Porque mi método también se cuelga. No tengo Tarea Pero no Tarea. Ejecutar. Esto debería ser una pregunta por sí solo.
flexxxit
@flexxxit: deberías usar Microsoft.Bcl.Async.
SLaks
48

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

Explico las razones completas de este punto muerto en mi blog . En resumen, hay un "contexto" que se guarda de forma predeterminada al comienzo de cada uno awaity se utiliza para reanudar el método.

Entonces, si esto se llama en un contexto de UI, cuando se awaitcompleta, el asyncmétodo intenta volver a ingresar ese contexto para continuar ejecutándose. Desafortunadamente, el código que usa Wait(o Result) bloqueará un hilo en ese contexto, por lo que el asyncmétodo no puede completarse.

Las pautas para evitar esto son:

  1. Use ConfigureAwait(continueOnCapturedContext: false)tanto como sea posible. Esto permite suasync métodos continúen ejecutándose sin tener que volver a ingresar al contexto.
  2. Usar asynctodo el camino. Use en awaitlugar de Resulto Wait.

Si su método es naturalmente asíncrono, entonces (probablemente) no debería exponer un contenedor sincrónico .

Stephen Cleary
fuente
Necesito ejecutar una tarea Async en un catch () que no admite asynccómo haría esto y evitaría una situación de incendio y olvido.
Zapnologica
1
@Zapnologica: awaitse admite en catchbloques a partir de VS2015. Si tiene una versión anterior, puede asignar la excepción a una variable local y hacer lo siguiente awaitdespués del bloque catch .
Stephen Cleary
5

Aquí esta 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

píxel
fuente
0

Con un pequeño contexto de sincronización personalizado, la función de sincronización puede esperar la finalización de 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
codefox
fuente