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 Taskque 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/awaiten 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 claveasyncyawait; 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:
continuationrepresenta un cierre para la "iteración actual"previousrepresenta el queTaskcontiene 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.continuationse ejecuta por primera vez usandoprevious.ContinueWith(continuation)continuationcierre se actualizapreviouspara reflejar el estado de finalización de_ => GetWorkAsync()_ => GetWorkAsync()se completa, "continúa con"_previous.ContinueWith(continuation), es decir,continuationvolver a llamar al lambdapreviousse ha actualizado con el estado de_ => GetWorkAsync()modo quecontinuationse llama al lambda cuandoGetWorkAsync()regresa.La
continuationlambda siempre verifica el estado de_quitesto, si_quit == falseno hay más continuaciones, yTaskCompletionSourcese 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/awaitharí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.ConfigureAwaitlugar?)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 siawaitse 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, recurrirawaita 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