Dela via


Asynkron programmering med Async och Await (Visual Basic)

Modellen Task asynkron programmering (TAP) ger ett abstraktionslager över typisk asynkron kodning. I den här modellen skriver du kod som en sekvens med instruktioner, samma som vanligt. Skillnaden är att du kan läsa din uppgiftsbaserade kod när kompilatorn bearbetar varje -instruktion och innan den börjar bearbeta nästa instruktion. För att åstadkomma den här modellen utför kompilatorn många transformeringar för att slutföra varje uppgift. Vissa instruktioner kan initiera arbete och returnera ett Task objekt som representerar det pågående arbetet och kompilatorn måste lösa dessa transformeringar. Målet med asynkron aktivitetsprogrammering är att aktivera kod som läser som en sekvens med instruktioner, men körs i en mer komplicerad ordning. Genomförandet baseras på extern resursallokering och när uppgifterna är slutförda.

Uppgiftens asynkrona programmeringsmodell motsvarar hur personer ger instruktioner för processer som innehåller asynkrona uppgifter. Den här artikeln använder ett exempel med instruktioner för att göra frukost för att visa hur nyckelorden Async och Await gör det lättare att resonera om kod som innehåller en serie asynkrona instruktioner. Anvisningarna för att göra en frukost kan anges som en lista:

  1. Häll en kopp kaffe.
  2. Värm en panna och stek sedan två ägg.
  3. Koka tre hashbruna biffar.
  4. Rosta två brödbitar.
  5. Sprid smör och sylt på rostat bröd.
  6. Häll ett glas apelsinjuice.

Om du har erfarenhet av matlagning kan du slutföra dessa instruktioner asynkront. Du börjar värma pannan för ägg och börja sedan laga hashbruna. Du lägger brödet i brödrosten och börjar sedan laga äggen. I varje steg i processen startar du en uppgift och övergår sedan till andra uppgifter som är redo för din uppmärksamhet.

Matlagningsfrukost är ett bra exempel på asynkront arbete som inte är parallellt. En person (eller tråd) kan hantera alla uppgifter. En person kan göra frukosten asynkront genom att starta nästa uppgift innan föregående uppgift slutförs. Varje matlagningsuppgift fortsätter oavsett om någon aktivt tittar på processen. Så snart du börjar värma pannan för äggen kan du börja laga hashbruna. När hash browns börjar tillagas kan du lägga brödet i brödrosten.

För en parallell algoritm behöver du flera personer som lagar mat (eller flera trådar). En person lagar äggen, en annan lagar hashbruna och så vidare. Varje person fokuserar på en specifik uppgift. Varje person som lagar mat (eller varje tråd) blockeras synkront i väntan på att den aktuella uppgiften ska slutföras: Hash browns redo att vända, bröd redo att dyka upp i brödrost och så vidare.

diagram som visar instruktioner för att förbereda frukost som en lista över sju sekventiella uppgifter som slutförts på 30 minuter.

Överväg samma lista med synkrona instruktioner skrivna som Visual Basic-kodinstruktioner:

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

Om du tolkar dessa instruktioner som en dator skulle göra tar det cirka 30 minuter att förbereda frukosten. Varaktigheten är summan av de enskilda aktivitetstiderna. Datorn blockerar vid varje instruktion tills allt arbete har utförts och fortsätter sedan till nästa uppgiftsuttryck. Den här metoden kan ta mycket tid. I frukostexemplet skapar datormetoden en otillfredsställande frukost. Senare uppgifter i den synkrona listan, som att rosta brödet, startar inte förrän tidigare uppgifter har slutförts. Lite mat blir kall innan frukosten är redo att serveras.

Om du vill att datorn ska köra instruktioner asynkront måste du skriva asynkron kod. När du skriver klientprogram vill du att användargränssnittet ska vara responsivt för användarinmatning. Ditt program bör inte frysa all interaktion när du laddar ned data från webben. När du skriver serverprogram vill du inte blockera trådar som kan hantera andra begäranden. Att använda synkron kod när det finns asynkrona alternativ försvårar möjligheten att skala ut mindre kostsamt. Du betalar för blockerade trådar.

Lyckade moderna appar kräver asynkron kod. Utan språkstöd kräver skrivning av asynkron kod återanrop, slutförandehändelser eller annat sätt att dölja kodens ursprungliga avsikt. Fördelen med synkron kod är den stegvisa åtgärden som gör det enkelt att skanna och förstå. Traditionella asynkrona modeller tvingar dig att fokusera på kodens asynkrona karaktär, inte på de grundläggande åtgärderna i koden.

Blockera inte, vänta i stället

Föregående kod visar en olycklig programmeringspraxis: Skriva synkron kod för att utföra asynkrona åtgärder. Koden blockerar den aktuella tråden från att utföra något annat arbete. Koden avbryter inte tråden när aktiviteter körs. Resultatet av den här modellen liknar att stirra på brödrosten när du har lagt i brödet. Du ignorerar eventuella avbrott och startar inte andra uppgifter förrän brödet dyker upp. Du tar inte smöret och sylten ur kylskåpet. Du kanske missar att upptäcka en brand som börjar på spisen. Du vill både rosta brödet och hantera andra problem på samma gång. Samma sak gäller för din kod.

Du kan börja med att uppdatera koden så att tråden inte blockeras när aktiviteter körs. Nyckelordet Await ger ett icke-blockerande sätt att starta en uppgift och sedan fortsätta körningen när aktiviteten är klar. En enkel asynkron version av frukostkoden ser ut som följande kodfragment:

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

Koden uppdaterar de ursprungliga metodkropparna FryEggsför , FryHashBrownsoch ToastBread för att returnera Task(Of Egg), Task(Of HashBrown)respektive Task(Of Toast) objekt. De uppdaterade metodnamnen innehåller suffixet "Async": FryEggsAsync, FryHashBrownsAsyncoch ToastBreadAsync. Funktionen Main returnerar Task objektet, även om det inte har något Return uttryck, vilket är avsiktligt.

Anmärkning

Den uppdaterade koden drar ännu inte nytta av viktiga funktioner i asynkron programmering, vilket kan leda till kortare slutförandetider. Koden bearbetar aktiviteterna på ungefär samma tid som den ursprungliga synkrona versionen. Fullständiga metodimplementeringar finns i den slutliga versionen av koden senare i den här artikeln.

Nu ska vi använda frukostexemplet på den uppdaterade koden. Tråden blockeras inte medan äggen eller rårakorna tillagas, men koden startar inte heller några andra uppgifter förrän det nuvarande arbetet har avslutats. Du lägger fortfarande brödet i brödrosten och stirrar på brödrosten tills brödet dyker upp, men du kan nu svara på avbrott. På en restaurang där flera beställningar görs kan kocken starta en ny beställning medan en annan redan lagar mat.

I den uppdaterade koden blockeras inte den tråd som hanterar frukosten medan den väntar på en annan uppgift som inte är avslutad. För vissa program är den här ändringen allt du behöver. Du kan aktivera din app för att stödja användarinteraktion medan data laddas ned från webben. I andra scenarier kanske du vill starta andra aktiviteter medan du väntar på att den föregående aktiviteten ska slutföras.

Starta aktiviteter samtidigt

För de flesta åtgärder vill du starta flera oberoende uppgifter omedelbart. När varje uppgift slutförs initierar du annat arbete som är redo att startas. När du tillämpar den här metoden på frukostexemplet kan du förbereda frukosten snabbare. Du får också allt klart vid ungefär samma tidpunkt, så att du kan njuta av en varm frukost.

Klassen Task och relaterade typer är klasser som du kan använda för att tillämpa den här typen av resonemang på aktiviteter som pågår. Med den här metoden kan du skriva kod som liknar hur du skapar frukost i verkligheten. Du börjar laga ägg, hash browns och rostat bröd på samma gång. Eftersom varje matobjekt kräver åtgärder, vänder du din uppmärksamhet mot den uppgiften, tar hand om åtgärden och väntar sedan på något annat som kräver din uppmärksamhet.

I koden startar du en uppgift och håller fast vid det Task objekt som representerar arbetet. Du använder metoden Await för aktiviteten för att fördröja arbetet tills resultatet är klart.

Tillämpa dessa ändringar på frukostkoden. Det första steget är att lagra uppgifter för åtgärder när de startas, i stället för att använda uttrycket Await:

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!")

Dessa revisioner hjälper inte till att få din frukost redo snabbare. Uttrycket Await tillämpas på alla aktiviteter så snart de startar. Nästa steg är att flytta uttrycken Await för hashbruna och ägg till slutet av metoden, innan du serverar frukosten:

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!")

Nu har du en asynkront förberedd frukost som tar cirka 20 minuter att förbereda. Den totala tillagningstiden minskas eftersom vissa aktiviteter körs samtidigt.

Diagram som visar instruktioner för att förbereda frukost som åtta asynkrona uppgifter som slutförs på cirka 20 minuter, där tyvärr, ägg och hash browns brinner.

Koduppdateringarna förbättrar förberedelseprocessen genom att minska koktiden, men de introducerar en regression genom att bränna äggen och hashbruna. Du startar alla asynkrona uppgifter samtidigt. Du väntar bara på varje aktivitet när du behöver resultatet. Koden kan likna programmet i ett webbprogram som skickar begäranden till olika mikrotjänster och sedan kombinerar resultatet till en enda sida. Du gör alla begäranden omedelbart och tillämpar sedan uttrycket Await på alla dessa uppgifter och skriver webbsidan.

Stöd komposition med uppgifter

De tidigare kodrevisionerna hjälper till att göra allt klart för frukost samtidigt, förutom rostbrödet. Processen att göra rostat bröd är en sammansättning av en asynkron åtgärd (rosta brödet) med synkrona åtgärder (sprid smör och sylt på rostat bröd). Det här exemplet illustrerar ett viktigt begrepp om asynkron programmering:

Viktigt!

Sammansättningen av en asynkron åtgärd följt av synkront arbete är en asynkron åtgärd. Ett annat sätt att säga det, om någon del av en operation är asynkron, är hela operationen asynkron.

I de tidigare uppdateringarna har du lärt dig hur du använder Task eller Task<TResult> objekt för att lagra aktiviteter som körs. Du väntar på varje aktivitet innan du använder resultatet. Nästa steg är att skapa metoder som representerar kombinationen av annat arbete. Innan du serverar frukost vill du vänta på uppgiften som representerar rostning av brödet innan du sprider smöret och sylten.

Du kan representera det här arbetet med följande kod:

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

Metoden MakeToastWithButterAndJamAsync har Async-modifieraren i sin signatur som signalerar till kompilatorn att metoden innehåller ett Await uttryck och innehåller asynkrona åtgärder. Metoden representerar den uppgift som rostar brödet och sprider sedan smöret och sylten. Metoden returnerar ett Task<TResult> objekt som representerar sammansättningen av de tre åtgärderna.

Det reviderade huvudkodblocket ser nu ut så här:

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

Den här kodändringen illustrerar en viktig teknik för att arbeta med asynkron kod. Du skapar aktiviteter genom att separera åtgärderna till en ny metod som returnerar en aktivitet. Du kan välja när du ska vänta på den uppgiften. Du kan starta andra aktiviteter samtidigt.

Hantera asynkrona undantag

Fram tills nu förutsätter koden implicit att alla uppgifter har slutförts. Asynkrona metoder utlöser undantag, precis som deras synkrona motsvarigheter. Målen för asynkront stöd för undantag och felhantering är desamma som för asynkront stöd i allmänhet. Det bästa sättet är att skriva kod som läser som en serie synkrona instruktioner. Aktiviteter utlöser undantag när de inte kan slutföras. Klientkoden kan fånga dessa undantag när Await-uttrycket tillämpas på en startad uppgift.

I frukostexemplet antar du att brödrosten fattar eld medan brödet rostas. Du kan simulera det problemet genom att ändra metoden ToastBreadAsync så att den matchar följande kod:

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

Anmärkning

När du kompilerar den här koden visas en varning om kod som inte kan nås. Det här felet är avsiktligt. När brödrosten har fattat eld fortsätter inte åtgärderna normalt och koden returnerar ett fel.

När du har ändrat koden kör du programmet och kontrollerar utdata:

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)

Observera att en hel del uppgifter slutförs mellan den tidpunkt då brödrosten fattar eld och systemet observerar undantaget. När en aktivitet som körs asynkront utlöser ett undantag den aktiviteten. Objektet Task innehåller undantaget som utlöstes i egenskapen Task.Exception . Felaktiga uppgifter utlöser undantaget när Await uttrycket tillämpas på aktiviteten.

Det finns två viktiga mekanismer för att förstå den här processen:

  • Hur ett undantag lagras i en felaktig uppgift.
  • Hur ett undantag avkodas och kastas om när koden väntar (Await) på en misslyckad uppgift.

När kod som körs asynkront genererar ett undantag lagras undantaget i Task-objektet. Egenskapen Task.Exception är ett AggregateException objekt eftersom mer än ett undantag kan genereras under asynkront arbete. Undantag som utlöses läggs till i samlingen AggregateException.InnerExceptions. Om egenskapen Exception är null skapas ett nytt AggregateException objekt och undantaget som genereras är det första objektet i samlingen.

Det vanligaste scenariot för en felaktig uppgift är att egenskapen Exception innehåller exakt ett undantag. När koden väntar på en felaktig uppgift, så överväxlar den det första AggregateException.InnerExceptions undantaget i samlingen. Det här resultatet är orsaken till att utdata från exemplet visar ett InvalidOperationException objekt i stället för ett AggregateException objekt. Om du extraherar det första inre undantaget kan du arbeta med asynkrona metoder på ett så liknande sätt som möjligt för att arbeta med deras synkrona motsvarigheter. Du kan undersöka egenskapen Exception i koden när ditt scenario kan generera flera undantag.

Tips/Råd

Den rekommenderade metoden är att alla argumentverifieringsfel visas synkront från metoder som returnerar uppgifter. Mer information och exempel finns i Undantag i uppgiftsreturmetoder.

Innan du fortsätter till nästa avsnitt kommenterar du ut följande två satser i din ToastBreadAsync-metod. Du vill inte starta en ny brand:

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

Tillämpa await-uttryck på uppgifter effektivt

Du kan förbättra serien med Await uttryck i slutet av föregående kod med hjälp av metoder för klassen Task. Ett API är metoden WhenAll, som returnerar ett Task objekt som slutförs när alla uppgifter i argumentlistan är slutförda. Följande kod visar den här metoden:

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!")

Ett annat alternativ är att använda metoden WhenAny, som returnerar ett Task(Of Task) objekt som slutförs när något av argumenten har slutförts. Du kan vänta på den returnerade uppgiften eftersom du vet att aktiviteten är klar. Följande kod visar hur du kan använda metoden WhenAny för att vänta på att den första aktiviteten ska slutföras och sedan bearbeta resultatet. När du har bearbetat resultatet från den slutförda aktiviteten tar du bort den slutförda aktiviteten från listan över aktiviteter som skickas till metoden WhenAny.

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

Lägg märke till Await finishedTask uttryck i slutet av kodfragmentet. Den här raden är viktig eftersom Task.WhenAny returnerar en Task(Of Task) - en omslutningsaktivitet som innehåller den slutförda aktiviteten. När du Await Task.WhenAnyväntar på att omslutningsaktiviteten ska slutföras och resultatet är den faktiska uppgiften som slutfördes först. Men för att hämta aktivitetens resultat eller se till att eventuella undantag utlöses korrekt måste Await du själva den slutförda aktiviteten (lagras i finishedTask). Även om du vet att aktiviteten har slutförts kan du i väntan på den igen komma åt resultatet eller hantera eventuella undantag som kan ha orsakat felet.

Granska den slutliga koden

Så här ser den slutliga versionen av koden ut:

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

diagram som visar instruktioner för att förbereda frukost som sex asynkrona uppgifter som slutförs på cirka 15 minuter och koden övervakar eventuella avbrott.

Koden slutför de asynkrona frukostuppgifterna på cirka 15 minuter. Den totala tiden minskas eftersom vissa aktiviteter körs samtidigt. Koden övervakar flera uppgifter samtidigt och vidtar endast åtgärder efter behov.

Den slutliga koden är asynkron. Det återspeglar mer exakt hur en person kan laga frukost. Jämför den slutliga koden med det första kodexemplet i artikeln. Kärnåtgärderna är fortfarande tydliga genom att läsa koden. Du kan läsa den slutliga koden på samma sätt som du läser listan med instruktioner för att göra en frukost, som visas i början av artikeln. Språkfunktionerna för nyckelorden Async och Await ger den översättning som varje person gör för att följa de skriftliga instruktionerna: Starta uppgifter som du kan och blockera inte medan du väntar på att aktiviteter ska slutföras.

Async/await vs ContinueWith

Nyckelorden Async och Await ger syntaktisk förenkling jämfört med direkt användning ContinueWith . Även om Async/Await och ContinueWith har liknande semantik för hantering av asynkrona åtgärder, översätter Await kompilatorn inte nödvändigtvis uttryck direkt till ContinueWith metodanrop. I stället genererar kompilatorn optimerad tillståndsdatorkod som ger samma logiska beteende. Den här omvandlingen ger betydande fördelar med läsbarhet och underhåll, särskilt vid sammanlänkning av flera asynkrona åtgärder.

Tänk dig ett scenario där du behöver utföra flera sekventiella asynkrona åtgärder. Så här ser samma logik ut när den implementeras med ContinueWith jämfört med Async/Await:

Använda ContinueWith

Med ContinueWithkräver varje steg i en sekvens av asynkrona åtgärder kapslade fortsättningar:

' 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

Använda Async/Await

Samma sekvens av åtgärder med Async/Await läser mycket mer naturligt:

' 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

Varför Async/Await föredras

Metoden Async/Await erbjuder flera fördelar:

  • Läsbarhet: Koden läser som synkron kod, vilket gör det lättare att förstå flödet av åtgärder.
  • Underhåll: Att lägga till eller ta bort steg i sekvensen kräver minimala kodändringar.
  • Felhantering: Undantagshantering med Try/Catch block fungerar naturligt, medan ContinueWith kräver noggrann hantering av felaktiga uppgifter.
  • Felsökning: Samtalsstacken och felsökningsupplevelsen är mycket bättre med Async/Await.
  • Prestanda: Kompilatoroptimeringarna för Async/Await är mer avancerade än manuella ContinueWith kedjor.

Fördelen blir ännu tydligare i takt med att antalet sammanlänkade åtgärder ökar. Även om en enskild fortsättning kan vara hanterbar med blir sekvenser med ContinueWith3–4 eller fler asynkrona åtgärder snabbt svåra att läsa och underhålla. Det här mönstret, som kallas "monadisk do-notation" i funktionell programmering, gör att du kan skapa flera asynkrona åtgärder på ett sekventiellt och läsbart sätt.

Se även