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

Prefacio : Estoy buscando una explicación, no solo una solución. Ya sé 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 aguardado, todavía estoy un poco confundido acerca de algunos de los detalles más finos.

Estoy escribiendo un registrador para Windows Store Apps, y quiero admitir el registro asincrónico y síncrono. Los métodos asíncronos siguen el TAP, los síncronos deben ocultar todo esto, y mirar y funcionar como métodos ordinarios.

Este es el método central 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 …

Versión 1 :

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

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

Versión 2 :

Hmm … ¿Tal vez la tarea no comenzó?

 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.RunSynchronously suena 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 equivocadas. Pero 1? ¿Qué pasa 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 de la tarea? ¿Hay un punto muerto no obvio?

Por favor, ayúdame a entender.

La await dentro de su método asincrónico está tratando de volver al hilo de la interfaz de usuario.

Como el hilo de la 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 se está ejecutando en un subproceso de grupo de subprocesos, no intenta volver al subproceso de interfaz de usuario, y todo funciona.

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

Llamar al código async desde un código sincrónico puede ser bastante complicado.

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

Entonces, si esto se llama en un contexto de interfaz de usuario, cuando se await , el método async intenta volver a ingresar en ese contexto para continuar la ejecución. Desafortunadamente, el código que usa Wait (o Result ) bloqueará un hilo en ese contexto, por lo que el método async no se puede completar.

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 ingresar al contexto.
  2. Use async todo el camino. Use await lugar de Result o Wait .

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

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 UI

Con un pequeño contexto de sincronización personalizada, la función de sincronización puede esperar la finalización de la función asíncrona, sin crear un interlocking. 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  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