c# - ejemplos - Esperando sincrónicamente una operación asíncrona, y por qué Wait() congela el programa aquí
programacion asincrona c# ejemplos (4)
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();
}
trabajando muy bien y no bloqueando el hilo de la interfaz de usuario
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 asíncrono basado en tareas (TAP), async 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 el sincronizado. Los métodos asíncronos siguen el TAP, los síncronos deben ocultar todo esto y lucir y funcionar como métodos comunes.
Este es el método central 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 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 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é es lo que hace que se congele? ¿Hay algún problema con el objeto de tarea? ¿Hay un punto muerto no obvio?
Por favor ayúdame a entender.
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 interbloqueo. Aquí hay un pequeño ejemplo de 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
La await
dentro de su método asíncrono está intentando volver al subproceso de la interfaz de usuario.
Dado que el subproceso de la interfaz de usuario está ocupado esperando que se complete la tarea completa, tiene un interbloqueo.
Mover la llamada asíncrona a Task.Run()
resuelve el problema.
Debido a que la llamada asíncrona ahora se está ejecutando en un subproceso de grupo de subprocesos, no intenta volver al subproceso de la interfaz de usuario y, por lo tanto, 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 la interfaz de usuario, evitando por completo el interbloqueo.
Llamar a un código async
desde un 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 await
y se utiliza para reanudar el método.
Entonces, si esto se llama en un contexto de IU, cuando la await
completa, el método async
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 método async
no se puede completar.
Las pautas para evitar esto son:
- Use
ConfigureAwait(continueOnCapturedContext: false)
tanto como sea posible. Esto permite que sus métodosasync
continúen ejecutándose sin tener que volver a ingresar al contexto. - Utilice
async
todo el camino. Utiliceawait
lugar deResult
oWait
.
Si su método es naturalmente asíncrono, entonces (probablemente) no debería exponer un envoltorio síncrono .