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.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 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?
fuente
Respuestas:
El
await
interior 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.fuente
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.Microsoft.Bcl.Async
.Llamar
async
có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
await
y se utiliza para reanudar el método.Entonces, si esto se llama en un contexto de UI, cuando se
await
completa, elasync
método intenta volver a ingresar ese contexto para continuar ejecutándose. Desafortunadamente, el código que usaWait
(oResult
) bloqueará un hilo en ese contexto, por lo que elasync
método no puede completarse.Las pautas para evitar esto son:
ConfigureAwait(continueOnCapturedContext: false)
tanto como sea posible. Esto permite suasync
métodos continúen ejecutándose sin tener que volver a ingresar al contexto.async
todo el camino. Use enawait
lugar deResult
oWait
.Si su método es naturalmente asíncrono, entonces (probablemente) no debería exponer un contenedor sincrónico .
fuente
async
cómo haría esto y evitaría una situación de incendio y olvido.await
se admite encatch
bloques a partir de VS2015. Si tiene una versión anterior, puede asignar la excepción a una variable local y hacer lo siguienteawait
después del bloque catch .Aquí esta lo que hice
funciona muy bien y no bloquea el hilo de la interfaz de usuario
fuente
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.
fuente