Compartir a través de


Programación asincrónica con Async y Await (Visual Basic)

El modelo de programación asincrónica de tareas (TAP) proporciona una capa de abstracción sobre la codificación asincrónica típica. En este modelo, escribirá código como una secuencia de instrucciones, igual que de costumbre. La diferencia es que puede leer el código basado en tareas a medida que el compilador procesa cada instrucción y antes de empezar a procesar la siguiente instrucción. Para lograr este modelo, el compilador realiza muchas transformaciones para completar cada tarea. Algunas instrucciones pueden iniciar el trabajo y devolver un Task objeto que representa el trabajo en curso y el compilador debe resolver estas transformaciones. El objetivo de la programación asincrónica de tareas es habilitar el código que lee como una secuencia de instrucciones, pero se ejecuta en un orden más complicado. La ejecución se basa en la asignación de recursos externos y cuando se completan las tareas.

El modelo de programación asincrónica de tareas es análogo a cómo las personas proporcionan instrucciones para los procesos que incluyen tareas asincrónicas. En este artículo se usa un ejemplo con instrucciones para preparar el desayuno para mostrar cómo las Async palabras clave y Await facilitan la razón del código que incluye una serie de instrucciones asincrónicas. Las instrucciones para hacer un desayuno se pueden proporcionar como una lista:

  1. Verte una taza de café.
  2. Calentar una sartén y luego freír dos huevos.
  3. Cocine tres tortitas de patata.
  4. Tosta dos rebanadas de pan.
  5. Unta mantequilla y mermelada en la tostada.
  6. Vierta un vaso de jugo de naranja.

Si tiene experiencia con la cocina, es posible que complete estas instrucciones de forma asincrónica. Empiece a calentar la sartén y luego empiece a cocinar la masa de patata. Pones el pan en la tostadora y luego empiezas a cocinar los huevos. En cada paso del proceso, inicia una tarea y, a continuación, realiza la transición a otras tareas que están listas para su atención.

El desayuno de cocina es un buen ejemplo de trabajo asincrónico que no es paralelo. Una persona (o hilo) puede manejar todas las tareas. Una persona puede hacer el desayuno de forma asincrónica iniciando la siguiente tarea antes de que se complete la tarea anterior. Cada tarea de cocción progresa independientemente de si alguien está viendo activamente el proceso. En cuanto empieces a calentar la sartén para los huevos, puedes empezar a cocinar los hash browns. Después de que las croquetas de patata comiencen a cocinarse, puede poner el pan en la tostadora.

Para un algoritmo paralelo, necesitas varias personas que cocinen (o varios hilos). Una persona cocina los huevos, otra cocina las croquetas de patata, y así sucesivamente. Cada persona se centra en su tarea específica. Cada persona que está cocinando (o cada subproceso) se bloquea sincrónicamente esperando a que se complete la tarea actual: Tortitas de patata listo para voltear, pan listo para aparecer en tostadora, etc.

Diagrama que muestra instrucciones para preparar el desayuno como una lista de siete tareas secuenciales completadas en 30 minutos.

Tenga en cuenta la misma lista de instrucciones sincrónicas escritas como instrucciones de código de Visual Basic:

Sub Main()
    Dim cup As Coffee = PourCoffee()
    Console.WriteLine("coffee is ready")

    Dim eggs As Egg = FryEggs(2)
    Console.WriteLine("eggs are ready")

    Dim hashBrown As HashBrown = FryHashBrowns(3)
    Console.WriteLine("hash browns are ready")

    Dim toast As Toast = ToastBread(2)
    ApplyButter(toast)
    ApplyJam(toast)
    Console.WriteLine("toast is ready")

    Dim oj As Juice = PourOJ()
    Console.WriteLine("oj is ready")
    Console.WriteLine("Breakfast is ready!")
End Sub

Private Function PourOJ() As Juice
    Console.WriteLine("Pouring orange juice")
    Return New Juice()
End Function

Private Sub ApplyJam(toast As Toast)
    Console.WriteLine("Putting jam on the toast")
End Sub

Private Sub ApplyButter(toast As Toast)
    Console.WriteLine("Putting butter on the toast")
End Sub

Private Function ToastBread(slices As Integer) As Toast
    For slice As Integer = 0 To slices - 1
        Console.WriteLine("Putting a slice of bread in the toaster")
    Next
    Console.WriteLine("Start toasting...")
    Task.Delay(3000).Wait()
    Console.WriteLine("Remove toast from toaster")

    Return New Toast()
End Function

Private Function FryHashBrowns(patties As Integer) As HashBrown
    Console.WriteLine($"putting {patties} hash brown patties in the pan")
    Console.WriteLine("cooking first side of hash browns...")
    Task.Delay(3000).Wait()
    For patty As Integer = 0 To patties - 1
        Console.WriteLine("flipping a hash brown patty")
    Next
    Console.WriteLine("cooking the second side of hash browns...")
    Task.Delay(3000).Wait()
    Console.WriteLine("Put hash browns on plate")

    Return New HashBrown()
End Function

Private Function FryEggs(howMany As Integer) As Egg
    Console.WriteLine("Warming the egg pan...")
    Task.Delay(3000).Wait()
    Console.WriteLine($"cracking {howMany} eggs")
    Console.WriteLine("cooking the eggs ...")
    Task.Delay(3000).Wait()
    Console.WriteLine("Put eggs on plate")

    Return New Egg()
End Function

Private Function PourCoffee() As Coffee
    Console.WriteLine("Pouring coffee")
    Return New Coffee()
End Function

Si interpreta estas instrucciones como haría un ordenador, el desayuno tarda unos 30 minutos en prepararse. La duración es la suma de los tiempos de tarea individuales. El ordenador bloquea cada instrucción hasta que se completa todo el trabajo y, a continuación, continúa con la siguiente tarea. Este enfoque puede tardar mucho tiempo. En el ejemplo de desayuno, el método del ordenador crea un desayuno no satisfactorio. Las tareas posteriores de la lista sincrónica, como tostar el pan, no se inician hasta que se completen las tareas anteriores. Algunos alimentos se frían antes de que el desayuno esté listo para servir.

Si desea que el equipo ejecute instrucciones de forma asincrónica, debe escribir código asincrónico. Al escribir programas cliente, quiere que la interfaz de usuario responda a la entrada del usuario. La aplicación no debe inmovilizar toda la interacción al descargar datos de la web. Cuando escribes programas de servidor, no quieres bloquear hilos que podrían estar sirviendo otras peticiones. El uso de código sincrónico cuando existen alternativas asincrónicas daña la capacidad de escalar horizontalmente de forma menos costosa. Al final, los subprocesos bloqueados pasarán factura.

Las aplicaciones modernas correctas requieren código asincrónico. Sin soporte del lenguaje, la escritura de código asincrónico requiere callbacks, eventos de finalización u otros medios que pueden oscurecer la intención original del código. La ventaja del código sincrónico es la acción paso a paso que facilita el examen y la comprensión. Los modelos asincrónicos tradicionales le obligan a centrarse en la naturaleza asincrónica del código, no en las acciones fundamentales del código.

No bloquee, espere en su lugar

El código anterior resalta una práctica de programación desafortunada: Escritura de código sincrónico para realizar operaciones asincrónicas. El código impide que el subproceso actual realice cualquier otro trabajo. El código no interrumpe el subproceso mientras hay tareas en ejecución. El resultado de este modelo es similar a mirar en la tostadora después de poner el pan. Omite las interrupciones y no inicia otras tareas hasta que aparezca el pan. No sacas la mantequilla y la mermelada de la nevera. Podrías perderte ver un fuego comenzando en el fogón. Quieres tostar el pan y manejar otras tareas al mismo tiempo. Lo mismo sucede con tu código.

Puede empezar actualizando el código para que el subproceso no bloquee mientras se ejecutan las tareas. La palabra clave Await proporciona una manera de no bloqueo para iniciar una tarea y luego continuar la ejecución cuando se completa la tarea. Una versión asincrónica simple del código de desayuno tiene el siguiente aspecto:

Module AsyncBreakfastProgram
    Async Function Main() As Task
        Dim cup As Coffee = PourCoffee()
        Console.WriteLine("coffee is ready")

        Dim eggs As Egg = Await FryEggsAsync(2)
        Console.WriteLine("eggs are ready")

        Dim hashBrown As HashBrown = Await FryHashBrownsAsync(3)
        Console.WriteLine("hash browns are ready")

        Dim toast As Toast = Await ToastBreadAsync(2)
        ApplyButter(toast)
        ApplyJam(toast)
        Console.WriteLine("toast is ready")

        Dim oj As Juice = PourOJ()
        Console.WriteLine("oj is ready")
        Console.WriteLine("Breakfast is ready!")
    End Function

    Private Async Function ToastBreadAsync(slices As Integer) As Task(Of Toast)
        For slice As Integer = 0 To slices - 1
            Console.WriteLine("Putting a slice of bread in the toaster")
        Next
        Console.WriteLine("Start toasting...")
        Await Task.Delay(3000)
        Console.WriteLine("Remove toast from toaster")

        Return New Toast()
    End Function

    Private Async Function FryHashBrownsAsync(patties As Integer) As Task(Of HashBrown)
        Console.WriteLine($"putting {patties} hash brown patties in the pan")
        Console.WriteLine("cooking first side of hash browns...")
        Await Task.Delay(3000)
        For patty As Integer = 0 To patties - 1
            Console.WriteLine("flipping a hash brown patty")
        Next
        Console.WriteLine("cooking the second side of hash browns...")
        Await Task.Delay(3000)
        Console.WriteLine("Put hash browns on plate")

        Return New HashBrown()
    End Function

    Private Async Function FryEggsAsync(howMany As Integer) As Task(Of Egg)
        Console.WriteLine("Warming the egg pan...")
        Await Task.Delay(3000)
        Console.WriteLine($"cracking {howMany} eggs")
        Console.WriteLine("cooking the eggs ...")
        Await Task.Delay(3000)
        Console.WriteLine("Put eggs on plate")

        Return New Egg()
    End Function

    Private Function PourCoffee() As Coffee
        Console.WriteLine("Pouring coffee")
        Return New Coffee()
    End Function

    Private Function PourOJ() As Juice
        Console.WriteLine("Pouring orange juice")
        Return New Juice()
    End Function

    Private Sub ApplyJam(toast As Toast)
        Console.WriteLine("Putting jam on the toast")
    End Sub

    Private Sub ApplyButter(toast As Toast)
        Console.WriteLine("Putting butter on the toast")
    End Sub
End Module

El código actualiza los cuerpos de método originales de FryEggs, FryHashBrownsy ToastBread para devolver Task(Of Egg)objetos , Task(Of HashBrown)y Task(Of Toast) , respectivamente. Los nombres de método actualizados incluyen el sufijo "Async": FryEggsAsync, FryHashBrownsAsyncy ToastBreadAsync. La Main función devuelve el Task objeto, aunque no tiene una Return expresión, que es por diseño.

Nota:

El código actualizado aún no aprovecha las características clave de la programación asincrónica, lo que puede dar lugar a tiempos de finalización más cortos. El código procesa las tareas aproximadamente la misma cantidad de tiempo que la versión sincrónica inicial. Para ver las implementaciones completas del método, consulte la versión final del código más adelante en este artículo.

Vamos a aplicar el ejemplo de desayuno al código actualizado. El hilo no se bloquea mientras los huevos o las tortitas de patata se están cocinando, pero el código tampoco inicia otras tareas hasta que se completa el trabajo actual. Todavía pones el pan en la tostadora y mira a la tostadora hasta que el pan aparezca, pero ahora puedes responder a interrupciones. En un restaurante donde se realizan varios pedidos, el cocinero puede comenzar un nuevo pedido mientras que otro ya está cocinando.

En el código actualizado, el subproceso que trabaja en la tarea del desayuno no se bloquea mientras espera a que se termine cualquier tarea iniciada que esté sin finalizar. Para algunas aplicaciones, este cambio es todo lo que necesita. Puede permitir que la aplicación admita la interacción del usuario mientras se descargan datos desde la web. En otros escenarios, es posible que desee iniciar otras tareas mientras espera a que se complete la tarea anterior.

Iniciar tareas simultáneamente

Para la mayoría de las operaciones, desea iniciar varias tareas independientes inmediatamente. A medida que completes cada tarea, inicias otros trabajos que estén listos para empezar. Al aplicar esta metodología al ejemplo de desayuno, puede preparar el desayuno más rápidamente. También tiene todo listo cerca de la misma hora, por lo que podrá disfrutar de un desayuno caliente.

La Task clase y los tipos relacionados son clases que puede usar para aplicar este estilo de razonamiento a las tareas que están en curso. Este enfoque le permite escribir código que se parezca más a la forma en que se crea el desayuno en la vida real. Empiezas a cocinar los huevos, las patatas fritas ralladas y las tostadas al mismo tiempo. A medida que cada alimento requiere acción, diriges tu atención a esa tarea, realizas la acción y, a continuación, esperas a que haya algo más que requiera tu atención.

En tu código, inicias una tarea y retienes el objeto Task que representa el trabajo. Use el método Await en la tarea para retrasar la acción en el trabajo hasta que el resultado esté listo.

Aplique estos cambios al código de desayuno. El primer paso es almacenar las tareas de las operaciones cuando se inician, en lugar de usar la Await expresión :

Dim cup As Coffee = PourCoffee()
Console.WriteLine("Coffee is ready")

Dim eggsTask As Task(Of Egg) = FryEggsAsync(2)
Dim eggs As Egg = Await eggsTask
Console.WriteLine("Eggs are ready")

Dim hashBrownTask As Task(Of HashBrown) = FryHashBrownsAsync(3)
Dim hashBrown As HashBrown = Await hashBrownTask
Console.WriteLine("Hash browns are ready")

Dim toastTask As Task(Of Toast) = ToastBreadAsync(2)
Dim toast As Toast = Await toastTask
ApplyButter(toast)
ApplyJam(toast)
Console.WriteLine("Toast is ready")

Dim oj As Juice = PourOJ()
Console.WriteLine("Oj is ready")
Console.WriteLine("Breakfast is ready!")

Estas revisiones no ayudan a preparar el desayuno más rápido. La Await expresión se aplica a todas las tareas tan pronto como se inician. El siguiente paso es mover las expresiones de Await para las tortitas de patata y los huevos al final del método, antes de servir el desayuno:

Dim cup As Coffee = PourCoffee()
Console.WriteLine("Coffee is ready")

Dim eggsTask As Task(Of Egg) = FryEggsAsync(2)
Dim hashBrownTask As Task(Of HashBrown) = FryHashBrownsAsync(3)
Dim toastTask As Task(Of Toast) = ToastBreadAsync(2)

Dim toast As Toast = Await toastTask
ApplyButter(toast)
ApplyJam(toast)
Console.WriteLine("Toast is ready")
Dim oj As Juice = PourOJ()
Console.WriteLine("Oj is ready")

Dim eggs As Egg = Await eggsTask
Console.WriteLine("Eggs are ready")
Dim hashBrown As HashBrown = Await hashBrownTask
Console.WriteLine("Hash browns are ready")

Console.WriteLine("Breakfast is ready!")

Ahora tiene un desayuno preparado de forma asincrónica que tarda unos 20 minutos en prepararse. El tiempo total de cocción se reduce porque algunas tareas se ejecutan simultáneamente.

Diagrama que muestra instrucciones para preparar el desayuno como ocho tareas asincrónicas que se completan en unos 20 minutos, donde lamentablemente los huevos y las patatas fritas se queman.

El código actualiza el proceso de preparación reduciendo el tiempo de cocción, pero introduce una regresión al quemar los huevos y los hash browns. Inicia todas las tareas asincrónicas a la vez. Y esperará por una tarea solo cuando necesite los resultados. El código puede ser similar al programa de una aplicación web que realiza solicitudes a diferentes microservicios y, a continuación, combina los resultados en una sola página. Realiza todas las solicitudes inmediatamente y, a continuación, aplica la Await expresión en todas esas tareas y redacta la página web.

Soporte para la composición con tareas

Las revisiones de código anteriores ayudan a preparar todo para el desayuno al mismo tiempo, excepto la tostada. El proceso de hacer la tostada es una composición de una operación asincrónica (tostar el pan) con operaciones sincrónicas (untar mantequilla y mermelada en la tostada). En este ejemplo se muestra un concepto importante sobre la programación asincrónica:

Importante

La composición de una operación asincrónica seguida del trabajo sincrónico es una operación asincrónica. Se indicó otra manera, si alguna parte de una operación es asincrónica, toda la operación es asincrónica.

En las actualizaciones anteriores, aprendiste a utilizar objetos Task o Task<TResult> para retener tareas en ejecución. Espera en cada tarea antes de usar su resultado. El siguiente paso es crear métodos que representen la combinación de otro trabajo. Antes de servir el desayuno, querrá esperar en la tarea que representa tostar el pan antes de distribuir la mantequilla y la mermelada.

Puede representar este trabajo con el código siguiente:

Async Function MakeToastWithButterAndJamAsync(number As Integer) As Task(Of Toast)
    Dim toast As Toast = Await ToastBreadAsync(number)
    ApplyButter(toast)
    ApplyJam(toast)

    Return toast
End Function

El método MakeToastWithButterAndJamAsync tiene el modificador Async en su firma que señala al compilador que el método contiene una expresión Await y contiene operaciones asíncronas. Este método representa la tarea que tuesta el pan y, después, extiende la mantequilla y la mermelada. El método devuelve un Task<TResult> objeto que representa la composición de las tres operaciones.

El bloque principal revisado de código ahora tiene el siguiente aspecto:

Async Function Main() As Task
    Dim cup As Coffee = PourCoffee()
    Console.WriteLine("coffee is ready")

    Dim eggsTask = FryEggsAsync(2)
    Dim hashBrownTask = FryHashBrownsAsync(3)
    Dim toastTask = MakeToastWithButterAndJamAsync(2)

    Dim eggs = Await eggsTask
    Console.WriteLine("eggs are ready")

    Dim hashBrown = Await hashBrownTask
    Console.WriteLine("hash browns are ready")

    Dim toast = Await toastTask
    Console.WriteLine("toast is ready")

    Dim oj As Juice = PourOJ()
    Console.WriteLine("oj is ready")
    Console.WriteLine("Breakfast is ready!")
End Function

Este cambio de código ilustra una técnica importante para trabajar con código asincrónico. Puedes componer tareas separando las operaciones en un método nuevo que devuelva una tarea. Puede elegir cuándo esperar para continuar con esa tarea. Puede iniciar otras tareas simultáneamente.

Control de excepciones asincrónicas

Hasta este punto, el código asume implícitamente que todas las tareas se completan correctamente. Los métodos asincrónicos lanzan excepciones, al igual que sus homólogos sincrónicos. Los objetivos de compatibilidad asincrónica con excepciones y control de errores son los mismos que para la compatibilidad asincrónica en general. El procedimiento recomendado es escribir código que lea como una serie de instrucciones sincrónicas. Las tareas lanzan excepciones cuando no se pueden completar exitosamente. El código de cliente puede detectar esas excepciones cuando la Await expresión se aplica a una tarea iniciada.

En el ejemplo del desayuno, supongamos que la tostadora se activa mientras tosta el pan. Puede simular ese problema modificando el ToastBreadAsync método para que coincida con el código siguiente:

Private Async Function ToastBreadAsync(slices As Integer) As Task(Of Toast)
    For slice As Integer = 0 To slices - 1
        Console.WriteLine("Putting a slice of bread in the toaster")
    Next
    Console.WriteLine("Start toasting...")
    Await Task.Delay(2000)
    Console.WriteLine("Fire! Toast is ruined!")
    Throw New InvalidOperationException("The toaster is on fire")
    Await Task.Delay(1000)
    Console.WriteLine("Remove toast from toaster")

    Return New Toast()
End Function

Nota:

Al compilar este código, verá una advertencia sobre el código inaccesible. Se trata de un error por diseño. Después de que el tostador se active, las operaciones no continúan normalmente y el código devuelve un error.

Después de realizar los cambios en el código, ejecute la aplicación y compruebe la salida:

Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
   at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.vb:line 65
   at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.vb:line 36
   at AsyncBreakfast.Program.Main(String[] args) in Program.vb:line 24
   at AsyncBreakfast.Program.<Main>(String[] args)

Tenga en cuenta que bastantes tareas finalizan entre el momento en que el tostador se incendia y el sistema observa la excepción. Cuando una tarea que se ejecuta de forma asincrónica produce una excepción, se produce un error en esa tarea. El objeto Task almacena la excepción que se lanzó en la propiedad Task.Exception. Las tareas con errores lanzan la excepción cuando se aplica la Await expresión a la tarea.

Hay dos mecanismos importantes para comprender este proceso:

  • Cómo se almacena una excepción en una tarea con errores.
  • Cómo se desempaqueta una excepción y se vuelve a lanzar cuando el código espera (Await) en una tarea con errores.

Cuando el código que se ejecuta de forma asincrónica produce una excepción, la excepción se almacena en el Task objeto . La Task.Exception propiedad es un AggregateException objeto porque se puede producir más de una excepción durante el trabajo asincrónico. Cualquier excepción lanzada se agrega a la colección AggregateException.InnerExceptions. Si la Exception propiedad es null, se crea un nuevo AggregateException objeto y la excepción iniciada es el primer elemento de la colección.

El escenario más común para una tarea con errores es que la Exception propiedad contiene exactamente una excepción. Cuando su código espera en una tarea con error, vuelva a lanzar la primera excepción AggregateException.InnerExceptions de la colección. Este resultado es la razón por la que la salida del ejemplo muestra un InvalidOperationException objeto en lugar de un AggregateException objeto. La extracción de la primera excepción interna hace que el trabajo con métodos asincrónicos sea lo más parecido posible a trabajar con sus homólogos sincrónicos. Puede examinar la propiedad Exception en su código cuando su escenario podría generar varias excepciones.

Sugerencia

La práctica recomendada es que las excepciones de validación de argumentos surjan sincrónicamente de los métodos que devuelven tareas. Para obtener más información y ejemplos, vea Excepciones en métodos que devuelven tareas.

Antes de continuar con la sección siguiente, comente las dos instrucciones siguientes en el método ToastBreadAsync. No quieres iniciar otro incendio:

' Console.WriteLine("Fire! Toast is ruined!")
' Throw New InvalidOperationException("The toaster is on fire")

Aplicar expresiones await a tareas de forma eficaz

Puede mejorar la serie de Await expresiones al final del código anterior mediante métodos de la Task clase . Una API es el WhenAll método , que devuelve un Task objeto que se completa cuando se completan todas las tareas de su lista de argumentos. El código siguiente muestra este método:

Await Task.WhenAll(eggsTask, hashBrownTask, toastTask)
Console.WriteLine("Eggs are ready")
Console.WriteLine("Hash browns are ready")
Console.WriteLine("Toast is ready")
Console.WriteLine("Breakfast is ready!")

Otra opción es usar el WhenAny método , que devuelve un Task(Of Task) objeto que se completa cuando se completa cualquiera de sus argumentos. Puede esperar la tarea devuelta porque sabe que la tarea está terminada. En el código siguiente se muestra cómo puede usar el WhenAny método para esperar a que finalice la primera tarea y, a continuación, procesar su resultado. Después de procesar el resultado de la tarea completada, quite la tarea completada de la lista de tareas pasadas al WhenAny método .

Module ConcurrentBreakfastProgram
    Async Function Main() As Task
        Dim cup As Coffee = PourCoffee()
        Console.WriteLine("Coffee is ready")

        Dim eggsTask As Task(Of Egg) = FryEggsAsync(2)
        Dim hashBrownTask As Task(Of HashBrown) = FryHashBrownsAsync(3)
        Dim toastTask As Task(Of Toast) = MakeToastWithButterAndJamAsync(2)

        Dim breakfastTasks As New List(Of Task) From {eggsTask, hashBrownTask, toastTask}
        While breakfastTasks.Count > 0
            Dim finishedTask As Task = Await Task.WhenAny(breakfastTasks)
            If finishedTask Is eggsTask Then
                Console.WriteLine("eggs are ready")
            ElseIf finishedTask Is hashBrownTask Then
                Console.WriteLine("hash browns are ready")
            ElseIf finishedTask Is toastTask Then
                Console.WriteLine("toast is ready")
            End If
            Await finishedTask
            breakfastTasks.Remove(finishedTask)
        End While

        Dim oj As Juice = PourOJ()
        Console.WriteLine("oj is ready")
        Console.WriteLine("Breakfast is ready!")
    End Function

    Async Function MakeToastWithButterAndJamAsync(number As Integer) As Task(Of Toast)
        Dim toast As Toast = Await ToastBreadAsync(number)
        ApplyButter(toast)
        ApplyJam(toast)

        Return toast
    End Function

    Private Async Function ToastBreadAsync(slices As Integer) As Task(Of Toast)
        For slice As Integer = 0 To slices - 1
            Console.WriteLine("Putting a slice of bread in the toaster")
        Next
        Console.WriteLine("Start toasting...")
        Await Task.Delay(3000)
        Console.WriteLine("Remove toast from toaster")

        Return New Toast()
    End Function

    Private Async Function FryHashBrownsAsync(patties As Integer) As Task(Of HashBrown)
        Console.WriteLine($"putting {patties} hash brown patties in the pan")
        Console.WriteLine("cooking first side of hash browns...")
        Await Task.Delay(3000)
        For patty As Integer = 0 To patties - 1
            Console.WriteLine("flipping a hash brown patty")
        Next
        Console.WriteLine("cooking the second side of hash browns...")
        Await Task.Delay(3000)
        Console.WriteLine("Put hash browns on plate")

        Return New HashBrown()
    End Function

    Private Async Function FryEggsAsync(howMany As Integer) As Task(Of Egg)
        Console.WriteLine("Warming the egg pan...")
        Await Task.Delay(3000)
        Console.WriteLine($"cracking {howMany} eggs")
        Console.WriteLine("cooking the eggs ...")
        Await Task.Delay(3000)
        Console.WriteLine("Put eggs on plate")

        Return New Egg()
    End Function

    Private Function PourCoffee() As Coffee
        Console.WriteLine("Pouring coffee")
        Return New Coffee()
    End Function

    Private Function PourOJ() As Juice
        Console.WriteLine("Pouring orange juice")
        Return New Juice()
    End Function

    Private Sub ApplyJam(toast As Toast)
        Console.WriteLine("Putting jam on the toast")
    End Sub

    Private Sub ApplyButter(toast As Toast)
        Console.WriteLine("Putting butter on the toast")
    End Sub
End Module

Cerca del final del fragmento de código, observe la Await finishedTask expresión. Esta línea es importante porque Task.WhenAny devuelve una Task(Of Task) tarea contenedora que contiene la tarea completada. Cuando , Await Task.WhenAnyestá esperando a que se complete la tarea contenedora y el resultado es la tarea real que finalizó primero. Sin embargo, para recuperar el resultado de esa tarea o asegurarse de que se produzcan correctamente las excepciones, debe realizar Await la propia tarea completada (almacenada en finishedTask). Aunque sepa que la tarea ha finalizado, a la espera de nuevo le permite acceder a su resultado o controlar las excepciones que podrían haber provocado un error.

Revisión del código final

Este es el aspecto de la versión final del código:

Module ConcurrentBreakfastProgram
    Async Function Main() As Task
        Dim cup As Coffee = PourCoffee()
        Console.WriteLine("Coffee is ready")

        Dim eggsTask As Task(Of Egg) = FryEggsAsync(2)
        Dim hashBrownTask As Task(Of HashBrown) = FryHashBrownsAsync(3)
        Dim toastTask As Task(Of Toast) = MakeToastWithButterAndJamAsync(2)

        Dim breakfastTasks As New List(Of Task) From {eggsTask, hashBrownTask, toastTask}
        While breakfastTasks.Count > 0
            Dim finishedTask As Task = Await Task.WhenAny(breakfastTasks)
            If finishedTask Is eggsTask Then
                Console.WriteLine("eggs are ready")
            ElseIf finishedTask Is hashBrownTask Then
                Console.WriteLine("hash browns are ready")
            ElseIf finishedTask Is toastTask Then
                Console.WriteLine("toast is ready")
            End If
            Await finishedTask
            breakfastTasks.Remove(finishedTask)
        End While

        Dim oj As Juice = PourOJ()
        Console.WriteLine("oj is ready")
        Console.WriteLine("Breakfast is ready!")
    End Function

    Async Function MakeToastWithButterAndJamAsync(number As Integer) As Task(Of Toast)
        Dim toast As Toast = Await ToastBreadAsync(number)
        ApplyButter(toast)
        ApplyJam(toast)

        Return toast
    End Function

    Private Async Function ToastBreadAsync(slices As Integer) As Task(Of Toast)
        For slice As Integer = 0 To slices - 1
            Console.WriteLine("Putting a slice of bread in the toaster")
        Next
        Console.WriteLine("Start toasting...")
        Await Task.Delay(3000)
        Console.WriteLine("Remove toast from toaster")

        Return New Toast()
    End Function

    Private Async Function FryHashBrownsAsync(patties As Integer) As Task(Of HashBrown)
        Console.WriteLine($"putting {patties} hash brown patties in the pan")
        Console.WriteLine("cooking first side of hash browns...")
        Await Task.Delay(3000)
        For patty As Integer = 0 To patties - 1
            Console.WriteLine("flipping a hash brown patty")
        Next
        Console.WriteLine("cooking the second side of hash browns...")
        Await Task.Delay(3000)
        Console.WriteLine("Put hash browns on plate")

        Return New HashBrown()
    End Function

    Private Async Function FryEggsAsync(howMany As Integer) As Task(Of Egg)
        Console.WriteLine("Warming the egg pan...")
        Await Task.Delay(3000)
        Console.WriteLine($"cracking {howMany} eggs")
        Console.WriteLine("cooking the eggs ...")
        Await Task.Delay(3000)
        Console.WriteLine("Put eggs on plate")

        Return New Egg()
    End Function

    Private Function PourCoffee() As Coffee
        Console.WriteLine("Pouring coffee")
        Return New Coffee()
    End Function

    Private Function PourOJ() As Juice
        Console.WriteLine("Pouring orange juice")
        Return New Juice()
    End Function

    Private Sub ApplyJam(toast As Toast)
        Console.WriteLine("Putting jam on the toast")
    End Sub

    Private Sub ApplyButter(toast As Toast)
        Console.WriteLine("Putting butter on the toast")
    End Sub
End Module

Diagrama que muestra instrucciones para preparar el desayuno como seis tareas asincrónicas que se completan en unos 15 minutos, y los monitores de código para detectar posibles interrupciones.

El código completa las tareas de desayuno asincrónicas en unos 15 minutos. El tiempo total se reduce porque algunas tareas se ejecutan simultáneamente. El código supervisa simultáneamente varias tareas y realiza acciones solo según sea necesario.

El código final es asincrónico. Refleja con más precisión cómo una persona puede cocinar el desayuno. Compare el código final con el primer ejemplo de código del artículo. Las acciones principales siguen siendo claras leyendo el código. Puede leer el código final de la misma manera que leyó la lista de instrucciones para hacer un desayuno, como se muestra al principio del artículo. Las características del lenguaje para las palabras clave Async y Await proporcionan la traducción que cada persona realiza para seguir las instrucciones escritas: Inicia las tareas tan pronto como puedas y no te bloquees mientras esperas a que se completen.

Async/await frente a ContinueWith

Las palabras clave Async y Await simplifican la sintaxis en comparación con el uso directo de ContinueWith. Aunque Async/Await y ContinueWith tienen una semántica similar para controlar las operaciones asincrónicas, el compilador no traduce necesariamente Await directamente en llamadas al método ContinueWith. En su lugar, el compilador genera código de máquina de estado optimizado que proporciona el mismo comportamiento lógico. Esta transformación proporciona ventajas significativas de legibilidad y mantenimiento, especialmente al encadenar varias operaciones asincrónicas.

Considere un escenario en el que debe realizar varias operaciones asincrónicas secuenciales. Este es el aspecto de la misma lógica cuando se implementa con ContinueWith en comparación con Async/Await.

Uso de ContinueWith

Con ContinueWith, cada paso de una secuencia de operaciones asincrónicas requiere continuaciones anidadas:

' Using ContinueWith - demonstrates the complexity when chaining operations
Function MakeBreakfastWithContinueWith() As Task
    Return StartCookingEggsAsync() _
        .ContinueWith(Function(eggsTask)
                          Dim eggs = eggsTask.Result
                          Console.WriteLine("Eggs ready, starting bacon...")
                          Return StartCookingBaconAsync()
                      End Function) _
        .Unwrap() _
        .ContinueWith(Function(baconTask)
                          Dim bacon = baconTask.Result
                          Console.WriteLine("Bacon ready, starting toast...")
                          Return StartToastingBreadAsync()
                      End Function) _
        .Unwrap() _
        .ContinueWith(Function(toastTask)
                          Dim toast = toastTask.Result
                          Console.WriteLine("Toast ready, applying butter...")
                          Return ApplyButterAsync(toast)
                      End Function) _
        .Unwrap() _
        .ContinueWith(Function(butteredToastTask)
                          Dim butteredToast = butteredToastTask.Result
                          Console.WriteLine("Butter applied, applying jam...")
                          Return ApplyJamAsync(butteredToast)
                      End Function) _
        .Unwrap() _
        .ContinueWith(Sub(finalToastTask)
                          Dim finalToast = finalToastTask.Result
                          Console.WriteLine("Breakfast completed with ContinueWith!")
                      End Sub)
End Function

Uso de Async/Await

La misma secuencia de operaciones con Async/Await resulta mucho más natural.

' Using Async/Await - much cleaner and easier to read
Async Function MakeBreakfastWithAsyncAwait() As Task
    Dim eggs = Await StartCookingEggsAsync()
    Console.WriteLine("Eggs ready, starting bacon...")
    
    Dim bacon = Await StartCookingBaconAsync()
    Console.WriteLine("Bacon ready, starting toast...")
    
    Dim toast = Await StartToastingBreadAsync()
    Console.WriteLine("Toast ready, applying butter...")
    
    Dim butteredToast = Await ApplyButterAsync(toast)
    Console.WriteLine("Butter applied, applying jam...")
    
    Dim finalToast = Await ApplyJamAsync(butteredToast)
    Console.WriteLine("Breakfast completed with Async/Await!")
End Function

¿Por qué se prefiere Async/Await?

El Async/Await enfoque ofrece varias ventajas:

  • Legibilidad: el código lee como código sincrónico, lo que facilita la comprensión del flujo de operaciones.
  • Capacidad de mantenimiento: agregar o quitar pasos en la secuencia requiere cambios mínimos en el código.
  • Control de errores: el control de excepciones con Try/Catch bloques funciona de forma natural, mientras que ContinueWith requiere un control cuidadoso de las tareas con errores.
  • Depuración: La experiencia con la pila de llamadas y el depurador es mucho mejor con Async/Await.
  • Rendimiento: las optimizaciones del compilador para Async/Await son más sofisticadas que las cadenas manuales.ContinueWith

La ventaja se vuelve aún más evidente a medida que aumenta el número de operaciones encadenadas. Aunque una sola continuación puede ser manejable con ContinueWith, las secuencias de 3 a 4 o más operaciones asincrónicas se vuelven rápidamente difíciles de leer y mantener. Este patrón, conocido como "notación monádica" en la programación funcional, permite componer varias operaciones asincrónicas de una manera secuencial y legible.

Consulte también