cursores - ¿Cómo difiere la función de asincronización en espera de C#5.0 del TPL?
cursores en c# (7)
No veo la diferencia entre las nuevas características asíncronas de C # (y de VB) y la Biblioteca de tareas paralelas de .NET 4.0. Tomemos, por ejemplo, el código de Eric Lippert desde aquí :
async void ArchiveDocuments(List<Url> urls) {
Task archive = null;
for(int i = 0; i < urls.Count; ++i) {
var document = await FetchAsync(urls[i]);
if (archive != null)
await archive;
archive = ArchiveAsync(document);
}
}
Parece que la palabra clave await
está sirviendo dos propósitos diferentes. La primera aparición ( FetchAsync
) parece significar: "Si este valor se usa más adelante en el método y su tarea no se termina, espere hasta que se complete antes de continuar". La segunda instancia ( archive
) parece significar: "Si esta tarea aún no ha finalizado, espere ahora hasta que se complete". Si estoy equivocado, por favor corrígeme.
¿No podría ser escrito tan fácilmente así?
void ArchiveDocuments(List<Url> urls) {
for(int i = 0; i < urls.Count; ++i) {
var document = FetchAsync(urls[i]); // removed await
if (archive != null)
archive.Wait(); // changed to .Wait()
archive = ArchiveAsync(document.Result); // added .Result
}
}
He reemplazado el primero en await
con un Task.Result
donde realmente se necesita el valor, y el segundo await
con Task.Wait()
, donde realmente está ocurriendo la espera. La funcionalidad es (1)
ya implementada, y (2)
mucho más cerca semánticamente de lo que está sucediendo realmente en el código.
Me doy cuenta de que un método async
se reescribe como una máquina de estado, similar a los iteradores, pero tampoco veo qué beneficios trae. Cualquier código que requiera que funcione otro subproceso (como la descarga) requerirá otro subproceso, y cualquier código que no lo haga (como leer un archivo) aún podría utilizar el TPL para trabajar con un solo subproceso.
Obviamente me falta algo enorme aquí; ¿Alguien puede ayudarme a entender esto un poco mejor?
Anders lo resumió en una respuesta muy breve en la entrevista de Channel 9 Live que hizo. Lo recomiendo altamente
Las nuevas palabras clave Async y aguardar le permiten orquestar la concurrencia en sus aplicaciones. En realidad, no introducen ninguna concurrencia en su aplicación.
TPL y más específicamente Tarea es una forma en que puede usar para realizar operaciones al mismo tiempo. La nueva palabra clave async y await le permite componer estas operaciones concurrentes de forma "síncrona" o "lineal".
Por lo tanto, aún puede escribir un flujo de control lineal en sus programas mientras que la computación real puede o no ocurrir al mismo tiempo. Cuando el cálculo ocurre simultáneamente, aguardar y sincronizar le permiten componer estas operaciones.
Creo que el malentendido surge aquí:
Parece que la palabra clave await está sirviendo dos propósitos diferentes. La primera aparición (FetchAsync) parece significar: "Si este valor se usa más adelante en el método y su tarea no se termina, espere hasta que se complete antes de continuar". La segunda instancia (archivo) parece significar: "Si esta tarea aún no ha finalizado, espere ahora hasta que se complete". Si estoy equivocado, por favor corrígeme.
Esto es realmente completamente incorrecto. Ambos tienen el mismo significado.
En tu primer caso:
var document = await FetchAsync(urls[i]);
Lo que sucede aquí, es que el tiempo de ejecución dice "Comience a llamar a FetchAsync, luego regrese el punto de ejecución actual al hilo que llama a este método". Aquí no hay "espera"; en su lugar, la ejecución vuelve al contexto de sincronización de llamadas, y las cosas continúan agitándose. En algún momento en el futuro, la Tarea de FetchAsync se completará, y en ese punto, este código se reanudará en el contexto de sincronización del hilo de llamada, y se producirá la siguiente declaración (asignando la variable del documento).
La ejecución continuará hasta que la segunda espera la llamada, momento en el que sucederá lo mismo; si la Task<T>
(archivo) no está completa, la ejecución se lanzará al contexto de llamada; de lo contrario, se establecerá el archivo .
En el segundo caso, las cosas son muy diferentes: aquí, está bloqueando explícitamente, lo que significa que el contexto de sincronización de llamadas nunca tendrá la oportunidad de ejecutar ningún código hasta que se complete todo el método. De acuerdo, todavía hay asincronía, pero la asincronía está completamente contenida dentro de este bloque de código: ningún código fuera de este código pegado ocurrirá en este hilo hasta que se complete todo el código.
El problema aquí es que la firma de ArchiveDocuments
es engañosa. Tiene un retorno explícito de void
pero realmente el retorno es Task
. Para mí, vacío implica sincrónico ya que no hay forma de "esperar" que termine. Considere la firma alternativa de la función.
async Task ArchiveDocuments(List<Url> urls) {
...
}
Para mí, cuando está escrito de esta manera, la diferencia es mucho más obvia. La función ArchiveDocuments
no se completa sincrónicamente sino que finalizará más tarde.
Hay una gran diferencia
Wait()
bloquea, await
no bloquea. Si ejecuta la versión asincrónica de ArchiveDocuments()
en la secuencia de la interfaz gráfica de usuario, la GUI seguirá siendo receptiva mientras se ejecutan las operaciones de recuperación y archivado. Si usa la versión TPL con Wait()
, su GUI será bloqueada.
Tenga en cuenta que async
logra hacer esto sin introducir ningún subproceso: en el punto de await
, el control simplemente se devuelve al ciclo de mensajes. Una vez que se ha completado la tarea que se esperaba, el resto del método (continuación) se pone en cola en el bucle de mensajes y el hilo de la GUI continuará ejecutando ArchiveDocuments
donde se dejó.
La capacidad de convertir el flujo de control del programa en una máquina de estados es lo que hace que estas nuevas palabras clave sean interesantes. Piense en ello como ceder el control , en lugar de los valores.
Mira este video del canal 9 de Anders hablando de la nueva función.
La llamada a FetchAsync()
seguirá FetchAsync()
hasta que se complete (a menos que FetchAsync()
una instrucción dentro de las llamadas?) La clave es que el control se devuelve a la persona que llama (porque el método ArchiveDocuments
sí mismo se declara como async
). Entonces, la persona que llama puede continuar procesando la lógica de UI, responder a eventos, etc.
Cuando FetchAsync()
finaliza, interrumpe a la persona que llama para finalizar el ciclo. ArchiveAsync()
y bloquea, pero ArchiveAsync()
probablemente solo crea una nueva tarea, la inicia y devuelve la tarea. Esto permite que comience el segundo ciclo, mientras se procesa la tarea.
El segundo ciclo golpea FetchAsync()
y bloquea, devolviendo el control a la persona que llama. Cuando FetchAsync()
finaliza, nuevamente interrumpe a la persona que llama para continuar el proceso. A continuación, await archive
, que devuelve el control a la persona que llama hasta que se completa la Task
creada en el ciclo 1. Una vez que se completa esa tarea, la persona que llama nuevamente se interrumpe, y el segundo ciclo llama a ArchiveAsync()
, que obtiene una tarea iniciada y comienza el ciclo 3, repite ad nauseum .
La clave es devolver el control a la persona que llama mientras los levantadores pesados se están ejecutando.
La palabra clave await no introduce concurrencia. Es como la palabra clave yield, le dice al compilador que reestructure su código en lambda controlado por una máquina de estados.
Para ver cómo sería el código de espera sin ''aguardar'', consulte este excelente enlace: http://blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt-and-await.aspx