Gå til indholdet

Task og async/await

Task og Task<T> er klasser, som repræsenterer en enkelt asynkron operation med en samling instruktioner, der afvikles på en tråd for sig selv.

Information til undervisere

Forståelse af Task og Task<T> er vigtig for at forstå asynkron programmering i C#. Det er vigtigt at forklare, hvordan disse klasser bruges til at repræsentere asynkrone operationer, og hvordan async og await nøgleordene bruges til at arbejde med dem.

I slutningen af kapitlet er lidt information om de mere avancerede begreber som deadlock og lock, som kan være nyttige at forklare, hvis du har studerende, der er interesseret i at forstå mere om asynkron programmering.

Klasserne har metoder til at starte og stoppe afvikling, samt en mulighed for at aflæse en status, som eksempelvis kan være:

  • startet
  • færdig
  • fejlet
  • afbrudt

Task betyder jo opgave på engelsk, så objektet repræsenterer en opgave, der på et tidspunkt enten er færdig, fejlet eller timet ud. I andre sprog kendes et Task objekt som et Promise-objekt, hvilket også er et meget godt navn – et løfte om en opgave der afvikles på et tidspunkt i fremtiden.

Task findes i to versioner:

  • Task repræsenterer en operation uden returværdi. Du kan se Task som en Action-delegate.
  • Task<T> repræsenterer en operation med returværdi (svarende til typen T). Du kan se Task<T> som en Func-delegate.

I grundlæggende C# modtager du Task og Task<T> objekter som returværdier fra mange forskellige metodekald i frameworket. Det kunne eksempelvis være, når du henter eller skriver til en fil, henter data fra nettet via HTTP eller kommunikerer med en database. I den mere avancerede C# kan du selv skabe metoder, der skaber og returnerer Task-objekter.

Info

Asynkron kode kan være kompliceret og der er mange faldgrupper. Derfor kan det være en god idé at bruge statisk kodeanalyse til at finde potentielle problemer i din kode. Se mere om dette her.

Om async og await

Du kan vælge at arbejde direkte med Task-objekterne og selv løbende spørge efter objektets status (er objektet startet, færdigt, fejlet med videre) og benytte objektets metoder til at få afviklet kode på givne tidspunkter, men du bør benytte kodeordene async og await for at simplificere og sikre koden så meget som muligt.

Kodeordet async skal placeres i metodedefinitionen og er et krav for at kunne benytte await-kodeordet. Når runtime ser async-kodeordet, vil der blive autogenereret kode, der repræsenterer en tilstandsmaskine, som vil holde styr på både status og kontekst. Koden, der genereres, er meget kompleks, men du behøver ikke forstå den. Det eneste du skal være bevidst om er, at async er et krav for await, og at await kodeordet vil afvente, at status i et Task-objekt ændrer sig til eksempelvis afsluttet eller fejlet. I mellemtiden fortsætter hovedtråden sin afvikling, og når Task objektet er færdigt, sørger den autogenerede tilstandsmaskine for, at kontekst reetableres og afvikling fortsætter fra instruktionen efter await-kodeordet.

At hovedtråden fortsætter sin afvikling, giver ikke meget mening i en konsolapplikation som tidligere nævnt, men tænk eksempelvis på Windows-applikationen, hvor hovedtråden har til formål at holde applikationen levende.

Hvis der er tale om void-metoder, vil await blot afvente at afviklingen er færdig. Hvis der er tale om en metode, der returnerer en værdi, vil await kodeordet betyde, at du kan tildele returværdien direkte til en variabel og på den måde konvertere Task<T> objektet (hvor T er typen af returværdien) til en konkret værdi af typen T.

await kodeordet vil få din kode til at ligne synkron kode, selv om det i virkeligheden er asynkront. Samtidig vil kodeordene async og await gøre fejlhåndtering meget simpel, for du skal blot benytte en try/catch struktur, som du hele tiden har gjort.

Forestil dig, at du i en Windows-applikation skal skrive koden, der henter et id fra en database, og benytter dette id til at hente en stor fil fra disken, og indholdet af denne fil skal sendes til en webserver. I synkron kode vil disse tre operationer skulle afvikles i rækkefølge, og i mellemtiden låser applikationen. I en asynkron applikation kan det skrives således:

try
{
    int id = await HentIdFraDatabase();
    string tekst = await HentTekstFraFil(id);
    await SendTekstTilServer(tekst);
}
catch (Exception ex)
{
    // log/besked ... der er sket en fejl
}

Metoderne er opfundet til lejligheden, men pointen er, at de tre metoder afvikles på en tråd for sig selv, og at afvikling automatisk afventer resultatet af den forrige metode. Samtidig fortsætter hovedtråden sin afvikling.

Uden brugen af await kodeordet ville det kræve meget kompleks kode at opnå samme funktionalitet, for slet ikke at tale om den oplevelse det ville være at fejlfinde med debuggeren i Visual Studio uden brug af await.

Hvis du skal lege lidt med asynkron kode og Task eller Task<T> objektet, kan du kigge på metoder på File-klassen under System.IO. Der finder du metoderne ReadAllTextAsync (Task) og WriteAllTextAsync (Task<string>). Her er et eksempel på kode, der henter data fra en fil og gemmer i en anden fil:

using System;
using System.IO;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                string tekst = await File.
                    ReadAllTextAsync(@"y:\temp\data.txt");
                await File.AppendAllTextAsync
                    (@"x:\temp\data.txt", tekst);
            }
            catch (Exception ex)
            {
                // log ... der er sket en fejl
            }

        }
    }
}

Prøv at se på dokumentationen til de to metoder. Der vil du opdage, at de returnerer en Task<T> og en Task:

ReadAllTextAsync returnerer en Task

Læg også mærke til at metoden er awaitable.

Da metoderne returnerer en Task, kunne du vælge at holde fast på en reference til denne og så afvente resultatet senere:

using System;
using System.IO;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                Task<string> t1 = File.
                    ReadAllTextAsync(@"y:\temp\data.txt");
                string tekst = await t1;
                Task t2 = File.AppendAllTextAsync
                    (@"x:\temp\data.txt", tekst);
                await t2;
            }
            catch (Exception ex)
            {
                // log ... der er sket en fejl
            }

        }
    }
}

Det koster jo lidt mere kode, men giver omvendt lidt flere muligheder.

Brug af await

await kan du bruge foran Task- og Task<T>-objekter for at fortælle runtime, at der skal afventes at afviklingen er færdig - men at afviklingen returneres til hovedtråden. Når afvikling af Task-objektet er færdig afvikles resten af instruktionerne efter await - og contekst reetableres.

Som et eksempel på værdien ved async/await kan du se på denne kode, hvor metoden GemFilAsync(string sti, string indhold) gemmer indhold i en given fil asynkront, holder en lille pause, og samtidigt logger på konsollen hvad den gør. Metoden returnerer en Task (action/void) så den kan “awaites” hvis du ønsker. Se bort fra syntaksen i metoden GemFilAsync i følgende - den er underordnet - men fokuser på det logiske i koden.

Hvad tror du der sker:

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Start program");
            string indhold = "123";
            string sti = @"c:\temp\data.txt";
            GemFilAsync(sti, indhold);
            Console.WriteLine("Slut program");
        }

        static async Task GemFilAsync(string sti, string indhold)
        {
            await Task.Run(async () => {
                Console.WriteLine("Gemmer " + sti);
                Thread.Sleep(100);
                await File.AppendAllTextAsync(sti, indhold);
                Console.WriteLine("Gemt " + sti);
            });
        }
    }
}

Den skriver

Start program
Slut program

hvilke jo kan undre fordi GemFilAsync bliver jo kaldt??

Men GemFilAsync-metoden afvikle sin kode på en tråd for sig selv, og den når aldrig at komme igang før programmet er afsluttet!!

Prøv at rette koden til

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Start program");
            string indhold = "123";
            string sti = @"c:\temp\data.txt";
            GemFilAsync(sti, indhold);
            Console.WriteLine("Slut program");
            Console.ReadKey();
        }

        static async Task GemFilAsync(string sti, string indhold)
        {
            await Task.Run(async () => {
                Console.WriteLine("Gemmer " + sti);
                Thread.Sleep(100);
                await File.AppendAllTextAsync(sti, indhold);
                Console.WriteLine("Gemt " + sti);
            });
        }
    }
}
Den eneste forskel er den sidste linje i Main - Console.ReadKey() - som holder vinduet åbent og dermed programmet levende indtil der trykkes på en knap.

Nu er resultatet:

Start program
Slut program
Gemmer c:\temp\data.txt
Gemt c:\temp\data.txt

Nu bliver metoden da kaldt, men “Slut program” kommer før log fra metoden - hvorfor?? Igen - metoden afvikles på en tråd for sig selv, og inden den er igang er programpointeren allerede videre til næste linje.

Prøv så at rette til

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Start program");
            string indhold = "123";
            string sti = @"c:\temp\data.txt";
            await GemFilAsync(sti, indhold);
            Console.WriteLine("Slut program");
        }

        static async Task GemFilAsync(string sti, string indhold)
        {
            await Task.Run(async () => {
                Console.WriteLine("Gemmer " + sti);
                Thread.Sleep(100);
                await File.AppendAllTextAsync(sti, indhold);
                Console.WriteLine("Gemt " + sti);
            });
        }
    }
}

Nu er Console.ReadKey() fjernet igen, men til gengæld er await-kodeordet tilføjet foran GemFilAsync. Nu er resultatet:

Start program
Gemmer c:\temp\data.txt
Gemt c:\temp\data.txt
Slut program

hvilket er det vi gerne vil. Rækkefølgen er nu logisk fordi await autogenerer en masse kode der har til formål at genetablere kontekst og afvikling når den asynkrone funktion er færdig. Det ligner og kodes som synkron kode, men det er asynkront.

Brug af async og await til asynkrone action delegates

Der er masser metoder i frameworket som fra Microsofts side er kodet asynkront - især metoder relateret til IO, HTTP og databaser. Mange af disse returnerer en Task (og ikke en Task<T>), og kan dermed awaites. Her er et eksempler:

await System.IO.File.WriteAllTextAsync(@"c:\temp\test.txt", "test");

Koden afventer at der gemmes en fil asynkront. Det kunne også skrives som:

Task task = System.IO.File.WriteAllTextAsync(@"c:\temp\test.txt", "test");
await task;

Nu gemmes Task-objektet som så afventes.

Brug af async og await til asynkrone func delegates

Hvis der er tale om en asynkton metode der returnerer noget benyttes Task<T> klassen. Hvis man benytter await er resultatet jo allerede kendt, og man kan derfor tildele det direkte til en variabel af den korrekte type.

string txt = await System.IO.File.ReadAllTextAsync(@"c:\temp\test.txt");
string[] linjer = await System.IO.File.ReadAllLinesAsync(@"c:\temp\test.txt");

Du kan også gemme Task-objektet og await’e det:

Task<string> t1 = System.IO.File.ReadAllTextAsync(@"c:\temp\test.txt");
string txt = await t1;

Task<string[]> t2 = System.IO.File.ReadAllLinesAsync(@"c:\temp\test.txt");
string[] linjer = await t2;

Afvent flere Task-objekter

I stedet for at afvente et enkelt Task eller Task<T> objekt, kan du vælge at afvente flere på en gang. Det betyder jo, at et metodekald ikke kan være afhængigt af returværdien fra et andet, men der er masser af eksempler på kode, hvor du blot ønsker at afvikle kode på en tråd for sig selv og enten er ligeglad med resultatet eller blot ønsker at afvente alle eller en enkelt.

Til det brug kan du bruge metoderne WhenAll eller WhenAny, som begge er statiske metoder på Task-klassen. De vil afvente, at alle eller et Task-objekt er færdigt, og returnerer i sig selv en Task.

Forestil dig, at du skal skrive kode, der gemmer tre store filer. Du vil sikkert kunne opnå en væsentlig performanceforbedring, hvis du kan sætte hver operation i gang på en tråd for sig selv, og så afvente at alle er færdige:

using System;
using System.IO;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                Task t1 = File.AppendAllTextAsync
                   (@"x:\temp\data1.txt", "tekst");
                Task t2 = File.AppendAllTextAsync
                   (@"x:\temp\data2.txt", "tekst");
                Task t3 = File.AppendAllTextAsync
                   (@"x:\temp\data3.txt", "tekst");
                await Task.WhenAll(t1, t2, t3);                
            }
            catch (Exception ex)
            {
                // log ... der er sket en fejl
            }
        }
    }
}

Et andet eksempel kunne være en opgave, hvor du skal hente tekst fra tre filer, og afvente at alle tekster er hentet:

using System;
using System.IO;
using System.Threading.Tasks;

namespace MinTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                Task<string> t1 = File.ReadAllTextAsync
                    (@"x:\temp\data1.txt");
                Task<string> t2 = File.ReadAllTextAsync
                    (@"x:\temp\data2.txt");
                Task<string> t3 = File.ReadAllTextAsync
                    (@"x:\temp\data3.txt");
                string[] tekster = await Task.WhenAll(t1, t2, t3);                
                // nu er tekster i et string array
            }
            catch (Exception ex)
            {
                // log ... der er sket en fejl
            }
        }
    }
}

Bemærk, at WhenAll-metoden returnerer et array af Task<string>, og når objektet afventer, kan indholdet i de tre filer aflæses i et array af strenge.

Opgaver

CancellationToken

En central del af asynkron programmering i C# er håndtering af opgaveannullering. CancellationToken er en struktur i .NET, der bruges til at annullere en eller flere løbende opgaver. Nedenfor er et eksempel på, hvordan en CancellationToken kan bruges i praksis.

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

class Program
{
    static async Task Main()
    {
        var cts = new CancellationTokenSource();
        var token = cts.Token;

        var task = Task.Run(() => DoSomethingAsync(token), token);


        cts.Cancel();

        try
        {
            await task;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine($"Caught an {nameof(OperationCanceledException)}");
        }
        finally
        {
            cts.Dispose();
        }
    }

    static async Task DoSomethingAsync(CancellationToken ct)
    {
        for (int i = 0; i < 5; i++)
        {

            ct.ThrowIfCancellationRequested();

            Console.WriteLine($"Loop {i}");

            await Task.Delay(1000, ct);
        }
    }
}

I dette eksempel oprettes en CancellationTokenSource, som bruges til at generere en CancellationToken. Denne token gives til den asynkrone metode DoSomethingAsync. Metoden kører en for-løkke, der simulerer en længerevarende opgave ved at vente i et sekund for hver iteration af løkken. Før hver iteration tjekker den, om annullering er blevet anmodet om ved at kalde ThrowIfCancellationRequested-metoden på cancellation token. Hvis annullering er blevet anmodet om, kastes der en OperationCanceledException.

I Main-metoden annulleres opgaven med cts.Cancel(), og await bruges til at vente på, at opgaven fuldføres. Hvis opgaven er blevet annulleret, fanges OperationCanceledException og en meddelelse udskrives til konsollen. Til sidst ryddes ressourcerne op ved at kalde DisposeCancellationTokenSource.

Opgaver

Fejlhåndtering

Når du arbejder med asynkrone opgaver i C#, er det vigtigt at håndtere potentielle fejl korrekt. Du kan gøre dette ved at bruge try-catch blokke på samme måde, som du ville i synkron kode. Nedenfor er et eksempel på, hvordan du kan bruge en try-catch blok til at håndtere fejl i en asynkron opgave.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        try
        {
            await DoSomethingAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Caught an exception: {ex.Message}");
        }
    }

    static async Task DoSomethingAsync()
    {
        await Task.Delay(1000); // Simulate async work

        throw new InvalidOperationException("Something went wrong");
    }
}

I dette eksempel har vi en DoSomethingAsync metode, der simulerer asynkron arbejde ved at vente i et sekund og derefter kaster en InvalidOperationException. I Main-metoden afventer vi udførelsen af DoSomethingAsync indenfor en try-catch blok.

Hvis en fejl opstår i DoSomethingAsync metoden - i dette tilfælde fordi vi manuelt kaster en InvalidOperationException - vil denne fejl blive fanget i catch-blokken i Main-metoden, og en fejlbesked vil blive udskrevet til konsollen. Dette sikrer, at uventede fejl kan håndteres på en kontrolleret måde, og at din applikation ikke stopper uventet.

At oprette egne metoder, der returnerer en Task, er en central del af asynkron programmering i .NET. Disse metoder gør det muligt at udføre baggrundsoperationer, såsom fil I/O, netværkskald eller databaselogik, uden at blokere applikationens hovedtråd. Når du opretter sådanne metoder, er der nogle vigtige overvejelser vedrørende brugen af await inden i metoden og fejlhåndtering.

Egne asynkrone metoder

Der er intet i vejen for at oprette dine egne asynkrone metoder, der returnerer en Task eller Task<T>. Dette gør det muligt at udføre asynkrone operationer i en metode, der kan afventes af forbrugeren af metoden:

public static async Task<string> HentFilIndholdAsync(string filsti)
{
    using (var reader = new StreamReader(filsti))
    {
        return await reader.ReadToEndAsync();
    }
}

I dette eksempel bruger metoden StreamReader.ReadToEndAsync() til at læse hele filen asynkront. await nøgleordet bruges til at vente på, at denne asynkrone operation afsluttes, før resultatet returneres. Dette gør, at metoden selv bliver asynkron og derfor skal markeres med async nøgleordet, og returneringstypen er Task<string> for at indikere, at den returnerer en streng asynkront.

Brug af await i egne metoder

Det er vigtigt at bruge await for de asynkrone operationer inde i din metode for at sikre, at operationen fuldføres, før resultatet bruges - også selv om det er en simpel operation som ReadToEndAsync. Dette sikrer, at eventuelle exceptions, der kastes af den asynkrone operation, kan fanges ved brug af try-catch blokke.

Når du arbejder med asynkrone metoder, er det afgørende at implementere fejlhåndtering, typisk ved brug af try-catch blokke. Dette sikrer, at din applikation kan reagere korrekt på fejl, f.eks. hvis en fil ikke kan findes eller læses:

public static async Task<string> HentFilIndholdAsync(string filsti)
{
    try
    {
        using (var reader = new StreamReader(filsti))
        {
            return await reader.ReadToEndAsync();
        }
    }
    // andre catch blokke
    catch (Exception ex)
    {
        throw new InvalidOperationException($"Kunne ikke læse filen: {ex.Message}", ex);
    }
}

Her er et eksempel på kald til meotden:

string filsti = @"sti/til/din/fil.txt";

try
{
    string indhold = await HentFilIndholdAsync(filsti);
    Console.WriteLine("Filindhold:");
    Console.WriteLine(indhold);
}
catch (Exception ex)
{
    Console.WriteLine($"En uventet fejl opstod: {ex.Message}");
    throw;
}

Det er vigtigt at bemærke, at selvom metoden returnerer en Task<string>, så håndteres eventuelle fejl inden i metoden, hvilket gør det muligt at returnere en standardværdi eller null i tilfælde af en fejl, uden at forbrugeren af metoden nødvendigvis skal implementere sin egen fejlhåndtering.

Yderligere om udvikling af egne asynkrone metoder

Det er mange forskellige forhold der kan spille ind i beslutningen om hvordan en asynkron metode skal udvikles. Du kan overveje at bruge lidt tid på denne video (og de ressourcer der nævnes) - den giver et giver et godt overblik

Opgaver

Result og Wait()

Warning

Hold dig væk fra Result og Wait(). Du vil dog muligvis falde over den på nettet - derfor dette afsnit

.Result og .Wait() er begge metoder, du kan bruge til at få resultatet af en Task eller Task<T\> eller vente på, at en opgave fuldføres. Hvis tasken ikke er færdig, vil begge disse metoder blokere udførelsestråden, indtil tasken er fuldført.

Her er et simpelt eksempel:

Task<int> task = Task.Run(() => 
{    
    Thread.Sleep(2000);
    return 42;
});

int result = task.Result; 
task.Wait(); 
Console.WriteLine(result); 

Selvom det kan være fristende at bruge .Result eller .Wait() for at gøre asynkron kode synkron, er det generelt anbefalet at undgå det.

Grundene til dette inkluderer:

  1. Deadlocks: Hvis du bruger .Result eller .Wait() i en kontekst, der allerede er asynkron, kan du risikere at forårsage en deadlock. Dette sker, når udførelsestråden venter på, at en opgave skal fuldføres, men opgaven venter på, at udførelsestråden frigives, så den kan fortsætte.

  2. Blokerende ressourcer: Ved at blokere udførelsestråden, mens du venter på, at en asynkron operation fuldføres, kan du forhindre den i at udføre andre opgaver. Dette kan føre til dårligere responsivitet i din applikation, især i en GUI eller webkontekst.

  3. Race Conditions: Dette er et problem, der opstår, når to eller flere tråde får adgang til delt data samtidig uden korrekt synkronisering. Hvis én tråd ændrer data, mens en anden læser eller ændrer de samme data, kan resultatet blive uforudsigeligt. Det kan føre til fejl, der er svære at spore og reproducerbare.

I stedet for at bruge .Result eller .Wait(), bør du bruge async/await nøgleordene til at arbejde med asynkron kode. Dette vil tillade din applikation at være mere skalerbar og responsiv.

Tip

Du kunne overveje at tilføje statisk kodeanalyse til dit projekt for at få besked om eventuelle “faldgrupper” i din kode.

ConfigureAwait

Når du bruger await nøgleordet i C#, kan du også kalde ConfigureAwait(false) på den pågældende task. ConfigureAwait(false) instruerer systemet til at “glemme” den aktuelle udførelseskontekst, når operationen er fuldført.

Dette er især vigtigt i GUI eller ASP.NET applikationer, hvor du typisk ønsker at undgå at blokere hovedtråden. I disse tilfælde kan du bruge ConfigureAwait(false) til at sikre, at din kode ikke forsøger at vende tilbage til hovedtråden, hvilket kan forårsage blokering eller endda risikere en deadlock.

Her er et eksempel:

public async Task MyMethodAsync()
{
    int result = await LongRunningOperationAsync().ConfigureAwait(false);
    Console.WriteLine(result);
}

public async Task<int> LongRunningOperationAsync()
{
    // Simulerer en langvarig operation
    await Task.Delay(2000);
    return 42;
}

I dette eksempel, efter LongRunningOperationAsync er fuldført, vil MyMethodAsync fortsætte udførelsen på en baggrundstråd, i stedet for at forsøge at vende tilbage til hovedtråden.

Det er vigtigt at bemærke, at brugen af ConfigureAwait(false) kan gøre din kode mere kompleks, da det kan føre til, at dele af din metode kører i forskellige kontekster. Derfor bør du kun bruge det, når det er nødvendigt, og du forstår konsekvenserne.

Deadlock

En deadlock er en situation i flertrådet eller parallelt programmering, hvor to eller flere tråde er ude af stand til at fortsætte, fordi hver holder en ressource, som den anden tråd eller tråde også har brug for. I praksis betyder det, at ingen af trådene kan fortsætte.

I asynkron programmering, som f.eks. i C#, kan deadlocks opstå, når du forsøger at vende tilbage til en synkron kontekst. Dette sker ofte, når du bruger .Result eller .Wait() metoderne på en Task, der ikke er færdig, inden for en asynkron metode.

For eksempel, hvis du har en asynkron metode, der køres på UI-tråden i en WPF eller Windows Forms applikation, og du forsøger at vente på en Task med .Result eller .Wait(), vil UI-tråden blive blokeret, indtil Task er færdig. Men fordi UI-tråden er blokeret, kan Task ikke fuldføre, fordi den forsøger at vende tilbage til UI-tråden. Dette resulterer i en deadlock.

Hvordan kan Deadlocks undgås?

Deadlocks kan undgås ved korrekt brug af async og await nøgleordene. Ved at bruge await i stedet for .Result eller .Wait(), kan du sikre, at din asynkrone kode ikke blokerer den aktuelle tråd, mens den venter på en Task at fuldføre. I stedet vil den frigive tråden, så den kan udføre andre opgaver.

Derudover, i situationer, hvor det er vigtigt at undgå at vende tilbage til den oprindelige kontekst (f.eks. UI-tråden), kan du bruge ConfigureAwait(false). Dette instruerer systemet til at “glemme” den aktuelle udførelseskontekst, når operationen er fuldført, og forhindre potentielle deadlocks.

Lock

lock er en nøglefunktion i C# brugt til at sikre, at en blok kode kun kan udføres af en enkelt tråd ad gangen. Dette kaldes ofte for at sikre “thread safety”.

Lad os sige, at du har en del af din kode, der læser og skriver til en fælles ressource. Hvis flere tråde forsøger at udføre denne kode samtidig, kan det føre til uforudsigelige resultater - en situation kaldet “race condition”. For at forhindre dette, kan du bruge lock.

Her er et eksempel:

private object _lock = new object();
private int _counter = 0;

public void IncrementCounter()
{
    lock(_lock)
    {
        _counter++;
    }
}

I dette eksempel sikrer lock-statementet, at kun en tråd ad gangen kan inkrementere _counter. Hvis en anden tråd forsøger at udføre koden inde i lock-statementet, mens en anden tråd allerede kører det, vil den vente, indtil den første tråd er færdig.

Mens lock er et kraftfuldt værktøj til at sikre thread safety, skal det bruges med forsigtighed. Ukorrekt brug af lock kan føre til deadlocks. For eksempel, hvis tråd A låser ressource X og forsøger at låse ressource Y, mens tråd B har låst ressource Y og forsøger at låse ressource X, vil begge tråde være låst fast, da de venter på, at den anden tråd frigiver en ressource. Dette er en deadlock.

Husk altid at minimere mængden af kode, du putter i en lock-statement, og undgå at holde låse, mens du venter på nogle eksterne ressourcer eller operationer for at minimere risikoen for deadlocks.