Después de lidiar con el patrón asíncrono / espera de C # por un tiempo, de repente me di cuenta de que realmente no sé cómo explicar lo que sucede en el siguiente código:
async void MyThread()
{
while (!_quit)
{
await GetWorkAsync();
}
}
GetWorkAsync()
se supone que devuelve un objeto de espera Task
que puede o no causar un cambio de hilo cuando se ejecuta la continuación.
No estaría confundido si la espera no estuviera dentro de un bucle. Naturalmente, esperaría que el resto del método (es decir, continuación) se ejecute potencialmente en otro hilo, lo cual está bien.
Sin embargo, dentro de un bucle, el concepto de "el resto del método" me resulta un poco confuso.
¿Qué sucede con "el resto del bucle" si el hilo se enciende en continuación vs. si no se cambia? ¿En qué hilo se ejecuta la siguiente iteración del bucle?
Mis observaciones muestran (no verificadas de manera concluyente) que cada iteración comienza en el mismo hilo (el original) mientras que la continuación se ejecuta en otro. ¿Puede esto ser realmente? En caso afirmativo, ¿es este un grado de paralelismo inesperado que debe tenerse en cuenta frente a la seguridad de subprocesos del método GetWorkAsync?
ACTUALIZACIÓN: Mi pregunta no es un duplicado, como lo sugieren algunos. El while (!_quit) { ... }
patrón de código es simplemente una simplificación de mi código real. En realidad, mi hilo es un ciclo de larga duración que procesa su cola de entrada de elementos de trabajo a intervalos regulares (cada 5 segundos por defecto). La verificación de la condición de abandono real tampoco es una simple verificación de campo como lo sugiere el código de muestra, sino más bien una verificación del controlador de eventos.
Respuestas:
De hecho, puedes verlo en Try Roslyn . Su método de espera se reescribe en
void IAsyncStateMachine.MoveNext()
la clase asíncrona generada.Lo que verás es algo como esto:
Básicamente, no importa en qué hilo estés; la máquina de estado puede reanudarse correctamente reemplazando su ciclo con una estructura if / goto equivalente.
Dicho esto, los métodos asíncronos no necesariamente se ejecutan en un hilo diferente. Vea la explicación de Eric Lippert "No es mágico" para explicar cómo puede trabajar
async/await
en un solo hilo.fuente
En primer lugar, Servy ha escrito un código en una respuesta a una pregunta similar, en la que se basa esta respuesta:
/programming/22049339/how-to-create-a-cancellable-task-loop
La respuesta de Servy incluye un
ContinueWith()
ciclo similar que utiliza construcciones TPL sin el uso explícito de las palabras claveasync
yawait
; así que para responder a su pregunta, considere cómo se vería su código cuando su ciclo se desenrolla usandoContinueWith()
Esto lleva algún tiempo para entenderlo, pero en resumen:
continuation
representa un cierre para la "iteración actual"previous
representa el queTask
contiene el estado de la "iteración anterior" (es decir, sabe cuándo finaliza la 'iteración' y se utiliza para iniciar la siguiente ...)GetWorkAsync()
devuelve aTask
, eso significaContinueWith(_ => GetWorkAsync())
que devolverá a,Task<Task>
por lo tanto, la llamadaUnwrap()
a obtener la 'tarea interna' (es decir, el resultado real deGetWorkAsync()
).Entonces:
Task.FromResult(_quit)
- su estado comienza comoTask.Completed == true
.continuation
se ejecuta por primera vez usandoprevious.ContinueWith(continuation)
continuation
cierre se actualizaprevious
para reflejar el estado de finalización de_ => GetWorkAsync()
_ => GetWorkAsync()
se completa, "continúa con"_previous.ContinueWith(continuation)
, es decir,continuation
volver a llamar al lambdaprevious
se ha actualizado con el estado de_ => GetWorkAsync()
modo quecontinuation
se llama al lambda cuandoGetWorkAsync()
regresa.La
continuation
lambda siempre verifica el estado de_quit
esto, si_quit == false
no hay más continuaciones, yTaskCompletionSource
se establece en el valor de_quit
, y todo se completa.En cuanto a su observación con respecto a la continuación que se ejecuta en un hilo diferente, eso no es algo que la palabra clave
async
/await
haría por usted, según este blog "Las tareas (todavía) no son hilos y la sincronización no es paralela" . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/Sugeriría que realmente vale la pena echar un vistazo más de cerca a su
GetWorkAsync()
método con respecto al enhebrado y la seguridad del hilo. Si sus diagnósticos revelan que se ha estado ejecutando en un subproceso diferente como consecuencia de su código asincrónico / de espera repetido, entonces algo dentro o relacionado con ese método debe estar causando que se cree un nuevo subproceso en otro lugar. (Si esto es inesperado, ¿tal vez hay un.ConfigureAwait
lugar?)fuente
SynchronizationContext
- eso es ciertamente importante ya que.ContinueWith()
usa el SynchronizationContext para despachar la continuación; de hecho, explicaría el comportamiento que está viendo siawait
se llama en un hilo ThreadPool o un hilo ASP.NET. Una continuación ciertamente podría ser enviada a un hilo diferente en esos casos. Por otro lado, recurrirawait
a un contexto de subproceso único, como un WPF Dispatcher o el contexto Winforms, debería ser suficiente para garantizar que la continuación ocurra en el original. hilo