Gå til indholdet

Asynkron programmering

I traditionel programmering, herunder C#, er det normalt at afvikle en samling af instruktioner i et afviklingsrum, hvor ressourcer som processorkraft og hukommelse fordeles ligeligt. Det kaldes også afvikling på en tråd (thread på engelsk), hvilket er et ret godt billede af, hvad der reelt sker – en samling af instruktioner placeret på en tråd, der bliver afviklet én for én.

Information til undervisere

Asynkron programmering er vigtigt i alle moderne C# applikationer, men mange studerende har svært ved at forstå det. Det er vigtigt at forklare hvad asynkron programmering i virkeligheden er, og hvorfor det er vigtigt. Det er også vigtigt at forklare forskellen mellem synkron og asynkron kode. Derfor denne tekst og tilhørende eksempler - brug dem samtidigt med at du forklarer det..

I moderne programmering vil du dog falde over kode, som afvikles over flere tråde, hvilket er muligt, fordi computere i dag typisk har flere kerner, eller fordi operativsystemet fordeler alle instruktioner til afvikling. Det kaldes multitrådet kode (multithreading).

Hvis du skal forstå det rent konseptuelt, så tænk på en tråd som en slags “arbejder”, der udfører en eller flere opgaver. Hvis du har flere tråde, kan du udføre flere opgaver samtidigt (eller noget der minder om det), hvilket kan forbedre ydeevnen og responstiden. Du kan også se det - helt overordnet - som en konsol applikation som starter andre konsol applikationer for at udføre opgaver. Hoved applikationen er den første tråd (hovedtråden) og de andre applikationer er de andre separate tråde. Det er jo ikke helt sådan det foregår i virkeligheden, men det er et meget godt billede på hvad der konceptuelt sker.

Hvad er en tråd og hvad er en kerne

En tråd er en grundlæggende enhed af udførelse inden for et computerprogram. En tråd er ansvarlig for at udføre en række opgaver, og kan køre parallelt med andre tråde i et program. Dette gør det muligt for et program at udføre flere opgaver samtidigt, i stedet for at skulle vente på, at en opgave er færdig, inden den kan starte en anden opgave.

En kerne er en grundlæggende enhed inden for en computerprocessor, der er ansvarlig for at udføre de opgaver, som en computer har brug for at udføre. En computer kan have flere kerner, hvilket gør det muligt for den at udføre flere opgaver samtidigt. Dette gør det muligt for en computer at udføre opgaver hurtigere, da den kan dele opgaverne op på flere kerner.

En tråd kan udføres på en kerne, men de er ikke det samme ting. En tråd er en enhed af udførelse inden for et computerprogram, mens en kerne er en enhed inden for en computerprocessor. En kerne er en fysisk enhed inden for en computer, mens en tråd er en softwareenhed. Det er vigtigt at huske, at tråde og kerner er to forskellige ting.

En computer kan have flere tråde, men den har kun et fast antal kerner. Antallet af kerner på en computer afhænger af den specifikke processor, men det er typisk mellem to og otte kerner. Dette betyder, at en computer kan udføre op til otte opgaver samtidigt, hvis den har otte kerner. Tråde kan dog køre på alle kerner på en computer, så en computer kan udføre flere tråde samtidigt end det antal kerner, den har.

Samtidighed og parallellisme

Samtidighed og parallellisme er to begreber, der ofte bruges i sammenhæng med tråde og kerner i programmering.

Samtidighed refererer til evnen til at udføre flere opgaver samtidigt. Dette betyder, at opgaverne kan starte, udføres og afsluttes i løbet af samme tidsperiode. Samtidighed kan opnås ved at oprette flere tråde i et program, så opgaverne kan udføres parallelt.

Parallellisme refererer til evnen til at udføre flere opgaver samtidigt på en fysisk enhed, såsom en computerprocessor med flere kerner. Dette betyder, at opgaverne kan udføres på samme tid ved at dele dem op på flere kerner. Parallellisme kan opnås ved at have flere tråde, der udføres på flere kerner på en computer.

Udfordringer med asynkron kode

Nu ved du lidt om asynkron kode generelt, og det lyder jo oplagt, at du bare skal skrive så meget asynkron kode som muligt. Men i virkeligheden er det meget komplekst at jonglere mange tråde, fordi der er så mange ting, du skal tage hensyn til. Du skal jo regne med, at asynkron kode bliver afviklet på sin helt egen tråd, så du ved som udgangspunkt ikke, hvornår instruktionerne er afviklet, og har dermed svært ved at få fat på en eventuel returværdi. Det er også komplekst at sende og modtage data fra en eller flere tråde. Og fejlhåndtering er et kapitel for sig! Hvad hvis der sker en fejl i en af de tråde, du har sat i gang. Hvordan får du besked om det?

Udfordringerne ved flere tråde

I de seneste versioner af C# er der tilføjet forskellige muligheder for at gøre det nemmere at arbejde med asynkron kode. Det er især kodeordene async og await samt klasserne Task og Task<T>, vi skal se nærmere på.

.NET biblioteker

.NET inkluderer to primære metoder til understøttelse af asynkron programmering: Task Parallel Library (TPL) og async/await (som er en del af C# sproget).

  1. Task Parallel Library (TPL): Dette bibliotek blev introduceret i .NET 4.0 og giver en højniveau API til at skabe og styre asynkrone operationer. Den centrale del af TPL er Task klassen, som repræsenterer en asynkron operation, der potentielt kan returnere en værdi (Task<TResult>). TPL tillader også parallel udførelse af opgaver ved hjælp af Parallel klassen, som kan parallelisere for-løkker (med Parallel.For og Parallel.ForEach) og udføre parallelle invokationer (med Parallel.Invoke).

  2. Async/Await: Dette er ikke et bibliotek i sig selv, men en del af C# og VB.NET sproget. Async/await blev introduceret i C# 5.0 og VB.NET 11, og er bygget oven på TPL. Det tilføjer async og await nøgleordene til sproget, hvilket gør det meget nemmere at skrive asynkron kode. En async metode er en metode, der kan have en eller flere await udtryk. Et await udtryk suspenderer udførelsen af metoden, indtil den ventede opgave er fuldført, men uden at blokere tråden.

Valget mellem at bruge Task Parallel Library (TPL) og async/await i din .NET kode afhænger primært af den specifikke opgave, du forsøger at løse, og hvilken del af din kode, der skal være asynkron.

Async/Await: Dette er generelt det foretrukne valg for I/O-bundne operationer, såsom netværksforespørgsler, databaseopkald eller filsystemoperationer. Disse operationer har tendens til at være “venteintensive”, hvor programmet ofte skal vente på, at en ekstern ressource bliver klar. Ved at bruge async/await kan du frigøre tråden til at udføre andet arbejde i stedet for blot at blokere, mens den venter. Async/await er også nemmere at bruge og læse, da det tillader dig at skrive asynkron kode på en måde, der ligner traditionel synkron kode.

public async Task<string> GetWebPageAsync(string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);
}

Task Parallel Library (TPL): TPL er mere velegnet til CPU-intensive opgaver, hvor du har en beregning, der kan opdeles i mindre, uafhængige enheder, der kan udføres parallelt. Dette kan være en beregning, der kan udføres på flere dataelementer samtidigt, eller en algoritme, der kan brydes ned i mindre, uafhængige trin. I disse tilfælde kan TPL hjælpe med at sprede arbejdet over flere kerner på din processor, hvilket kan forbedre den samlede ydeevne.

var numbers = Enumerable.Range(0, 10000).ToArray();
Parallel.For(0, numbers.Length, i =>
{
    numbers[i] = SomeCpuIntensiveOperation(numbers[i]);
});

Generelt set er async/await og TPL komplementære teknologier i .NET, der hver især har deres styrker afhængigt af situationen. I mange moderne .NET-applikationer vil du finde, at begge bruges sammen for at opnå den bedste ydeevne og skalerbarhed.

Optimering af tidskrævende metoder

Der kan være flere årsager til, at du ønsker kode afviklet over flere tråde. Den oplagte er, at du kan afvikle flere instruktioner på samme tid, og hvis du har blokke af instruktioner i din kode, som ikke er afhængig af hinanden, kan det være en fordel at afvikle dem alle på “samme tid” (husk - parallellisme er ikke det samme som samtidighed) i stedet for at afvikle en ad gangen.

Se følgende eksempel:

using System;
using System.Diagnostics;
using System.Threading;

namespace MinTest
{
    class Program
    {
        static void Main(string[] args)
        {

            Stopwatch s = new Stopwatch();
            s.Start();
            Console.WriteLine("Start");

            Console.WriteLine("Sleep 1");
            Thread.Sleep(500);
            Console.WriteLine("Sleep 2");
            Thread.Sleep(500);
            Console.WriteLine("Sleep 3");
            Thread.Sleep(500);

            Console.WriteLine("Slut");
            s.Stop();
            Console.WriteLine($"Tid: {s.ElapsedMilliseconds}");

        }
    }
} 

Koden benytter et stopur til at måle, hvor lang tid det tager at afvikle tre simulerede operationer, der tager 500 ms i rækkefølge (Thread.Sleep kan bruges til at holde en pause). Hvis du afvikler koden, vil du konstatere, at det tager omkring 1,5 sekund at afvikle applikationen – hvilket jo ikke er super overraskende.

Men hvis operationerne ikke er afhængige af hinanden, kan de jo godt sættes i gang samtidig, og koden vil dermed have 4 tråde på et tidspunkt – nemlig hovedtråden og tre enkelte tråde til de simulerede operationer. Hvis du ser bort fra nye kodeord, du endnu ikke kender til, kunne det se således ud:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static async Task Main(string[] args)
        {

            Stopwatch s = new Stopwatch();
            s.Start();
            Console.WriteLine("Start");

            Task t1 = Task.Run(() =>
            {
                Console.WriteLine("Sleep 1");
                Thread.Sleep(500);
            });

            Task t2 = Task.Run(() =>
            {
                Console.WriteLine("Sleep 2");
                Thread.Sleep(500);
            });

            Task t3 = Task.Run(() =>
            {
                Console.WriteLine("Sleep 3");
                Thread.Sleep(500);
            });

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("Slut");
            s.Stop();
            Console.WriteLine($"Tid: {s.ElapsedMilliseconds}");

        }
    }
}

Som det fremgår, bruges der nye kodeord som async og await samt en ny klasse kaldet Task. Det kommer vi tilbage til senere, men hvis du afvikler koden, vil du konstatere, at den afvikles på cirka 0,5 sekund – altså 1 sekund hurtigere end i eksemplet som kun benyttede hovedtråden. Og det giver jo fint mening. Hvis operativsystemet kan afvikle de tre operationer på “samme” tid (cirka), burde det tage 0,5 sekunder i alt at afvikle alle tre.

Men det kræver, at operationerne ikke er afhængige af hinanden på nogen måde, fordi vi i en rigtig applikation måske ikke har nogen anelse om, hvornår de er færdige. Runtime skal nok sørge for at vende tilbage til hovedtråden, men der er ingen garanti for hvornår.

Hvis du afvikler ovennævnte kode nogle gange, vil du se tydelige tegn på, at du har mistet noget af kontrollen. Hver sleep-operation udskriver, som det fremgår, noget på konsollen, og når du afvikler, bliver der udskrevet i vilkårlig rækkefølge.

Denne ene gang, du afvikler applikationen, kan det se således ud på konsollen:

Start
Sleep 1
Sleep 2
Sleep 3
Slut
Tid: 518

men næste gang:

Start
Sleep 3
Sleep 1
Sleep 2
Slut
Tid: 520

og så eventuelt:

Start
Sleep 1
Sleep 3
Sleep 2
Slut
Tid: 521

Faktum er, at du logisk nok ikke har kontrol over, hvornår en asynkron operation er færdig, og måske er det også ligegyldigt. Men hvis det har betydning, skal du i hvert fald være bevidst om det.

Samme kan opnås i “ægte parallel” kode med TPL:

using System;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static void Main(string[] args)
        {

            System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
            s.Start();
            Console.WriteLine("Start");

            Parallel.For(1, 4, nr => {
                Console.WriteLine("Sleep " + nr);
                System.Threading.Thread.Sleep(500);
            });

            Console.WriteLine("Slut");
            s.Stop();
            Console.WriteLine($"Tid: {s.ElapsedMilliseconds}");

        }
    }
}

Her fordeles afviklen ud over flere kerner, og du burde kunne se en forskel på to metoder i en grafisk repræsentation af afvikling i eksempelvis Windows Task Manager - enten er en kerne brugt meget eller også er det fordelt ud over mange kerner.

Optimering af brugerflade applikationer

I applikationstyper, som benytter brugerflader i en eller anden form, er asynkron programmering typisk også brugt. Ikke nødvendigvis for at spare afviklingstid, men måske mest for at frigøre hovedtråden, som styrer brugerfladen.

I en applikationstype, som Windows Forms (WinForm) eller Windows Presentation Foundation (WPF), vil en længerevarende operation låse brugerfladen, så brugeren ikke kan flytte vinduer eller trykke på knapper. Dermed kan en operation heller ikke afbrydes. Hele applikationen låser simpelthen, indtil operationen er afsluttet.

Hvis du har lyst, kan du se et eksempel ved at hente min demo fra: devcronberg/async-winform-task-await

På siden kan du finde en download-knap, hente hele projektet som en zip-fil og åbne og afvikle det gennem VS/VSC. Applikationen ser således ud:

Eksempel på en sync/async WinForm-applikation

Hvis du klikker på knapperne Sync eller ASync, bliver der hentet et tilfældigt tal fra nettet, og det tager et par sekunder. Applikationen har også en grøn linje nederst i vinduet, som løbende bliver fyldt ud.

Forskellen på de to knapper er naturligvis, at Sync-knappen henter tallet synkront og dermed låser brugerfladen. Når du klikker på knappen, kan du ikke flytte vinduet eller trykke på andre knapper. Den grønne linje bliver heller ikke tegnet mere. Først når tallet er hentet, er der liv i applikationen igen.

Hvis du klikker på ASync-knappen, kan du derimod flytte rundt på vinduet, mens tallet bliver hentet, og sågar klikke på Afbryd-knappen. Den grønne linje kører også uden problemer.

Hvis du kigger nærmere i koden, vil du også tydeligt kunne se, at Sync-knappen henter tallet via hovedtråden, mens ASync-knappen benytter en asynkron metode og dermed arbejder på en tråd for sig selv.

Så asynkron kode er meget benyttet i denne slags Windows-applikationer, men også i mobile applikationer, med en brugerflade, og i kode til afvikling på en webserver (ASP.NET Core). Sidstnævnte handler ikke om brugerfladen, fordi den dannes på serveren under alle omstændigheder, men om at give webserveren så meget luft som muligt til at håndtere forespørgsler fra andre klienter.

Konsol

Det er lidt sværere at vise i en simpel konsol applikation men her er et eksempel:

using System.Net.Http;

Console.WriteLine("Starter asynkron operation...");

// Start den asynkrone HTTP-anmodning
var httpTask = GetHttpContentAsync();

// Udfør andet arbejde, mens vi venter på HTTP-anmodningen
while (!httpTask.IsCompleted)
{
    Console.Beep();
    Console.WriteLine("Arbejder...");
    await Task.Delay(1000); // Vent et sekund
}

// Når HTTP-anmodningen er færdig, får vi resultatet
string result = await httpTask;
Console.WriteLine("HTTP-anmodningen er færdig.");
Console.WriteLine($"Resultat: {result}");

async Task<string> GetHttpContentAsync()
{
    using (HttpClient client = new HttpClient())
    {
        // Dette kalder en side, der forsinker svaret i 5 sekunder
        HttpResponseMessage response = await client.GetAsync("https://httpbin.org/delay/5");
        return await response.Content.ReadAsStringAsync();
    }
}

Du behøve ikke forstå alt koden men det vigtige er at du kan se at koden afvikler en HTTP-anmodning asynkront og imens den venter på svaret, så afvikler den andet arbejde.

En ansykron konsol app

Optimering af matematisk tunge operationer

Et mere komplet eksempel i brug af parallel kode kan du finde i følgende kode. Den beregner primtal (på en super langsom og “forkert” måde) på to forskellige måder.

BeregnPrimtal gør det serielt medens BeregnPrimtalParallel gør det parallelt. Når du prøver koden skal du gøre det i debug og holde øje med Visual Studios Diagnostic Tools (Debug -> Windows -> Diagnostic Tools) og se på CPU-brug samt på Windows Task Manager og se på alle CPU-forbrug.

I det første eksempel vil du bemærke et meget lavt CPU-forbrug,

mens det sidste eksempel vil bruge meget mere CPU - tæt på 100%.

Det viser sig også i tiden det tager at beregne primtal - det første eksempel tager lang tid, mens det sidste tager kort tid. Samtidigt vil du kunne se, at det første eksempel kun bruger en tråd.

mens det sidste bruger flere tråde.

Prøv det selv!

using System.Collections.Concurrent;
using System.Diagnostics;

BeregnPrimtal(50_000_000);
BeregnPrimtalParallel(50_000_000);

void BeregnPrimtal(int antal)
{
    ConcurrentDictionary<int, int> threads = new ConcurrentDictionary<int, int>();
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("Start");
    int minValue = 1;
    int maxValue = antal;

    List<int> primeNumbers = new List<int>();
    for (int i = minValue; i < maxValue; i++)
    {
        if (IsPrime(i))
        {
            primeNumbers.Add(i);
            // Hvor mange gange er en tråd benyttet
            threads.AddOrUpdate(Thread.CurrentThread.ManagedThreadId, 1, (_, oldValue) => oldValue + 1);
        }
    }

    stopwatch.Stop();
    Console.WriteLine("End");
    // Threads
    foreach (var item in threads)
    {
        Console.WriteLine($"Thread: {item.Key} - {item.Value}");
    }
    Console.WriteLine($"Time: {stopwatch.ElapsedMilliseconds} ms");
}

void BeregnPrimtalParallel(int antal)
{
    ConcurrentDictionary<int, int> threads = new ConcurrentDictionary<int, int>();
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("Start");
    int minValue = 1;
    int maxValue = antal;

    ConcurrentBag<int> primeNumbers = new ConcurrentBag<int>();

    Parallel.For(minValue, maxValue + 1, (i) =>
    {
        if (IsPrime(i))
        {
            primeNumbers.Add(i);
            // Hvor mange gange er en tråd benyttet
            threads.AddOrUpdate(Thread.CurrentThread.ManagedThreadId, 1, (_, oldValue) => oldValue + 1);
        }

    });

    stopwatch.Stop();
    Console.WriteLine("End");


    // Threads
    foreach (var item in threads)
    {
        Console.WriteLine($"Thread: {item.Key} - {item.Value}");
    }


    Console.WriteLine($"Time: {stopwatch.ElapsedMilliseconds} ms");
}

// Meget simpel primtalstest (kun til demonstration - ikke så hurtig)
bool IsPrime(int number)
{
    if (number <= 1) return false;
    if (number == 2) return true;
    if (number % 2 == 0) return false;

    var boundary = (int)Math.Floor(Math.Sqrt(number));

    for (int i = 3; i <= boundary; i += 2)
    {
        if (number % i == 0) return false;
    }

    return true;
}

Pyramid of hell

“Pyramid of Hell” er et udtryk, der ofte bruges til at beskrive, hvordan bland andet callbacks (en funktion som afvikler en anden funktion når den er færdig) kan føre til dybt indrykket og vanskeligt læsbar kode, især i forbindelse med asynkron programmering. I moderne C# kode anvendes async og await for at undgå dette problem, men her er koden der viser hvordan det kan se ud uden disse nøgleord:

class Program
{
    static void Main(string[] args)
    {        
        // Pyramid of hell
        Operation1().ContinueWith(task1 =>
        {            
            string result1 = task1.Result;

            Operation2(result1).ContinueWith(task2 =>
            {                
                string result2 = task2.Result;

                Operation3(result2).ContinueWith(task3 =>
                {                    
                    string result3 = task3.Result;

                    Operation4(result3).ContinueWith(task4 =>
                    {                        
                        string result4 = task4.Result;
                        Console.WriteLine($"Final result:\r\n{result4}");
                    });
                });
            });
        });

        Console.ReadKey();
    }

    static Task<string> Operation1()
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("1");
            await Task.Delay(1000); 
            return "Resultat fra Operation 1";
        });
    }

    static Task<string> Operation2(string input)
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("2");
            await Task.Delay(1000);
            return input + " -> Resultat fra Operation 2";
        });
    }

    static Task<string> Operation3(string input)
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("3");
            await Task.Delay(1000);
            return input + " -> Resultat fra Operation 3";
        });
    }

    static Task<string> Operation4(string input)
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("4");
            await Task.Delay(1000);
            return input + " -> Resultat fra Operation 4";
        });
    }
}
Callback hell

Hvis du er bekendt med sprog som JavaScript sker det samme “helvede” med de såkaldte callback functioner:

using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        Operation1((result1) =>
        {
            Operation2(result1, (result2) =>
            {
                Operation3(result2, (result3) =>
                {
                    Operation4(result3, (result4) =>
                    {
                        Console.WriteLine($"Final result: {result4}");
                    });
                });
            });
        });

        Console.ReadLine();
    }

    static void Operation1(Action<string> callback)
    {
        Task.Run(() =>
        {
            Console.WriteLine("1");
            Task.Delay(1000).Wait(); // Simulere asynkron operation
            callback("Resultat fra Operation 1");
        });
    }

    static void Operation2(string input, Action<string> callback)
    {
        Task.Run(() =>
        {
            Console.WriteLine("2");
            Task.Delay(1000).Wait(); // Simulere asynkron operation
            callback(input + " -> Resultat fra Operation 2");
        });
    }

    static void Operation3(string input, Action<string> callback)
    {
        Task.Run(() =>
        {
            Console.WriteLine("3");
            Task.Delay(1000).Wait(); // Simulere asynkron operation
            callback(input + " -> Resultat fra Operation 3");
        });
    }

    static void Operation4(string input, Action<string> callback)
    {
        Task.Run(() =>
        {
            Console.WriteLine("4");
            Task.Delay(1000).Wait(); // Simulere asynkron operation
            callback(input + " -> Resultat fra Operation 4");
        });
    }
}

Ved brug af async og await kan koden simplificeres så meget at den faktisk ligner synkron kode:

class Program
{
    static async Task Main(string[] args)
    {
        string result1 = await Operation1();
        string result2 = await Operation2(result1);
        string result3 = await Operation3(result2);
        string result4 = await Operation4(result3);
        Console.WriteLine($"Final result:\r\n{result4}");
    }

    static Task<string> Operation1()
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("1");
            await Task.Delay(1000); 
            return "Resultat fra Operation 1";
        });
    }

    static Task<string> Operation2(string input)
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("2");
            await Task.Delay(1000);
            return input + " -> Resultat fra Operation 2";
        });
    }

    static Task<string> Operation3(string input)
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("3");
            await Task.Delay(1000);
            return input + " -> Resultat fra Operation 3";
        });
    }

    static Task<string> Operation4(string input)
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("4");
            await Task.Delay(1000);
            return input + " -> Resultat fra Operation 4";
        });
    }
}

En asynkron konsol-applikation

Denne bog benytter udelukkende konsol-applikationer for at kunne holde fokus på kode og ikke brugerflade, og som du lige har lært, så er asynkron kode brugt en del i applikationer, hvor man gerne vil holde en brugerflade levende. Det er jo ikke tilfældet i en konsolapplikation, men derfor kan der godt være situationer, hvor du ønsker at afvikle asynkron kode i en konsolapplikation (se bare eksemplet i starten af kapitlet).

Men en konsolapplikation er som udgangspunkt ikke asynkron. Skabelonen ser således ud, som du ved:

using System;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

Men hvis du ønsker at afvikle moderne asynkron C# kode skal Main-metoden ændres således, at den supporterer de nyeste features.

Derfor kan du fra C# 8 ændre skabelonen til dette i stedet:

using System;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        static async Task Main(string[] args)
        {

        }
    }
}

Den eneste ændring er, at Main nu ikke længere returnerer void, men en reference til et Task objekt, og at metoden er markeret med async-kodeordet. Sidstnævnte er en forudsætning for, at du efterfølgende kan benytte await-kodeordet, som i den grad simplificerer asynkron kode. Faktisk vil Visual Studio give en advarsel om, at du benytter async-kodeordet uden at bruge await-kodeordet, fordi der bliver autogeneret og afviklet noget kode uden grund.

En Async funktion i en synkron app

Hvis det er muligt så start en applikation asynkront men nogen gange er det ikke muligt. Følgende kan benyttes men bør undgås hvis det er muligt:

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Start");
            Run().GetAwaiter().GetResult();
            Console.WriteLine("End");
        }

        static async Task Run()
        {
            await Task.Delay(100);
            await Console.Out.WriteLineAsync("Hello World");
        }

    }
}

Brugen er GetAwaiter().GetResult() er nødvendig for at afvente den asynkrone Run metode men kan give nogle udfordringer. Men nogen gange er det nødvendigt i ældre applikationer.

Om async og await - historisk

Asynkron programmering er en essentiel del af moderne softwareudvikling, da det muliggør mere effektiv brug af ressourcer ved at undgå blokeringer under ventetider. Når systemer skal kommunikere med eksterne kilder eller udføre tidskrævende opgaver, som netværksanmodninger eller filhåndtering, giver asynkron programmering mulighed for at frigøre CPU’en til andre opgaver, mens den venter. Begreberne “async” og “await” er blevet centrale for mange programmeringssprog for at kunne håndtere disse asynkrone operationer.

Historisk baggrund

Den tidlige historie for asynkron programmering involverede ofte brug af callbacks. Callbacks var funktioner, der blev kaldt, når en given opgave var fuldført. Dog viste brugen af callbacks sig at være problematisk og kompliceret i mere komplekse applikationer, hvilket resulterede i det velkendte problem kendt som “callback hell” eller “pyramiden af død”. Dette gjorde koden uoverskuelig og svær at vedligeholde.

For at afhjælpe dette problem introducerede mange programmeringssprog koncepter som promises og senere async/await. Promises forenklede håndteringen af asynkrone opgaver ved at tilbyde en klarere model for, hvordan man håndterer success eller fejl i en asynkron proces. Men promises kunne stadig være komplicerede at arbejde med i lange sekvenser af operationer.

Async og await blev udviklet som et forsøg på at forenkle skrivning og læsning af asynkron kode, så den ligner mere almindelig, sekventiel kode. Koncepterne blev populære på tværs af mange programmeringssprog, da de gjorde det lettere at læse, skrive og vedligeholde asynkron kode.

Implementering i forskellige sprog

Async og await er nu integreret i mange moderne programmeringssprog, og hvert sprog har sin egen unikke tilgang til implementering af disse nøgleord. Her er en oversigt over, hvordan async og await er blevet brugt i forskellige sprog, sorteret efter årstal:

  • C#: C# introducerede async og await i version 5.0 (2012), hvilket gjorde asynkron programmering meget lettere. C#’s async/await-mekanik gør det muligt at skrive asynkron kode, der ser ud som synkron kode, hvilket forbedrer læsbarheden. Dette var en stor forbedring i forhold til tidligere metoder som callbacks og Task Parallel Library (TPL), hvilket gjorde det lettere for udviklere at skrive mere vedligeholdelig kode.

  • Python: Python introducerede async/await i version 3.5 i 2015. Her blev det hurtigt populært til opgaver som netværkskommunikation og filhåndtering. Python’s async/await-mekanik minder meget om JavaScripts implementering, hvilket gør det lettere for udviklere at arbejde asynkront på tværs af sprog.

  • JavaScript: JavaScript var et af de første sprog, der for alvor tog async/await til sig efter introduktionen af promises. Med ES2017 blev async/await indført for at forenkle koden og gøre det nemmere at skrive og forstå asynkrone operationer, såsom API-kald i browseren. Async-funktioner returnerer en promise, og await suspenderer udførelsen, indtil promise’en er afgjort.

  • Rust: I Rust har async/await været en del af sproget siden version 1.39 (2019). Rust har fokuseret på effektiv hukommelseshåndtering, hvilket har gjort deres implementering af async/await anderledes ved at sikre nul-cost abstractions og sikre, at der ikke er data races, selv ved asynkron eksekvering.

  • Kotlin: Kotlin, et moderne sprog til JVM, har også understøttet async/await. Kotlin tilbyder coroutines, som fungerer som lette tråde, hvilket tillader udviklere at skrive asynkron kode på en sekventiel måde, hvor async og await bruges til at suspendere og genoptage koden.

  • Java: Java tog længere tid om at omfavne async/await direkte. I stedet blev Futures og CompletableFuture introduceret i Java 8 (2014) for at håndtere asynkrone opgaver, og senere har Java yderligere styrket brugen af asynkron programmering med introduktionen af nye biblioteker og frameworks som Project Loom, der bringer mere brugervenlige asynkrone konstruktioner.

Hvordan Async/Await har påvirket udvikling

Async og await har haft en dybtgående effekt på softwareudvikling. De giver udviklere mulighed for at skrive mere intuitiv og overskuelig kode, som lettere kan vedligeholdes. Samtidig gør de det muligt at udnytte ressourcer bedre, hvilket er essentielt i moderne applikationer, hvor responstider og effektivitet spiller en stor rolle.

Ved at gøre asynkrone operationer lettere at arbejde med, har async/await øget produktiviteten og kvaliteten af softwareudvikling. Udviklere kan nu fokusere mere på forretningslogik fremfor kompleksiteten i asynkron kontrolstrukturer.