Når vi taler om asynkrone kombinatorer, refererer vi til metoder som WhenAll og WhenAny. Selvom de returnerer opgaver (Tasks), er de ikke selv asynkrone metoder, men snarere værktøjer, der kombinerer flere opgaver på nyttige måder. Du kan også skrive dine egne kombinatorer, hvis du har brug for dem. Dette giver dig en palet af genanvendelige parallelle adfærdsmønstre, som du kan anvende, hvor du ønsker.

Lad os tage et konkret eksempel på en kombinator, vi kunne skrive selv: måske ønsker vi at tilføje en timeout til en hvilken som helst opgave. Selvom vi nemt kunne skrive dette fra bunden, fungerer det godt som et eksempel på, hvordan både Delay og WhenAny kan anvendes. Generelt er kombinatorer ofte nemmest at implementere ved brug af async, som i dette tilfælde, men der er også situationer, hvor det ikke er nødvendigt.

For at implementere en timeout, kan vi bruge en Task, som afsluttes efter en bestemt forsinkelse. Når vi så anvender WhenAny på både denne forsinkede opgave og den oprindelige opgave, kan vi fortsætte med den, der afsluttes først: enten den oprindelige opgave eller timeouten. Her er et eksempel på koden:

csharp
private static async Task WithTimeout(Task task, int time)
{ Task delayTask = Task.Delay(time); Task firstToFinish = await Task.WhenAny(task, delayTask); if (firstToFinish == delayTask) { task.ContinueWith(HandleException); throw new TimeoutException(); } return await task; }

I denne kode opretter jeg en opgave ved hjælp af Task.Delay, som vil afslutte efter timeouten. Jeg bruger derefter WhenAny på både den oprindelige opgave og timeout-opgaven, så vi kan fortsætte med den, der afsluttes først. Hvis timeouten afsluttes først, kastes en TimeoutException, ellers returneres resultatet af den oprindelige opgave.

En vigtig detalje her er håndteringen af undtagelser. Jeg har vedhæftet en fortsættelse til den oprindelige opgave ved hjælp af ContinueWith, som sørger for at håndtere eventuelle undtagelser, hvis der opstår en. Hvis timeouten udløber, ved vi, at den oprindelige opgave aldrig afsluttede korrekt, og derfor skal vi håndtere den situation på en ordentlig måde.

Når vi arbejder med asynkron programmering, er det også vigtigt at forstå, hvordan afbrydelse fungerer. I stedet for at være knyttet til Task-typen, håndteres afbrydelse i Task Asynchronous Programming (TAP) ved hjælp af CancellationToken-typen. Konventionelt bør enhver asynkron metode, der understøtter afbrydelse, have en overload, der tager et CancellationToken som parameter.

Her er et grundlæggende eksempel på, hvordan man afbryder en asynkron metode:

csharp
CancellationTokenSource cts = new CancellationTokenSource();
cancelButton.Click += delegate { cts.Cancel(); }; int result = await dbCommand.ExecuteNonQueryAsync(cts.Token);

Når du kalder CancelCancellationTokenSource, ændres CancellationToken til en afbrudt tilstand. En nyttig metode er at bruge ThrowIfCancellationRequested i enhver iteration af en løkke, når du arbejder med asynkrone metoder, og et CancellationToken er tilgængeligt.

csharp
foreach (var x in thingsToProcess)
{ cancellationToken.ThrowIfCancellationRequested(); // Process x ... }

Når ThrowIfCancellationRequested kaldes på et afbrudt token, vil det kaste en OperationCanceledException. Dette signalerer, at opgaven blev afbrudt, ikke fejlet, og derfor behandles denne undtagelse på en anden måde i Task Parallel Library.

En af de store fordele ved at bruge CancellationToken-metoden er, at det samme token kan distribueres til flere dele af den asynkrone operation. Dette betyder, at du kan afbryde flere samtidige opgaver på tværs af parallelle eller sekventielle beregninger.

For at forbedre brugeroplevelsen under langsommere operationer kan du også vise fremdrift under en asynkron opgave. Dette gøres ved at implementere IProgress-interfacet, som giver den asynkrone metode mulighed for at rapportere, hvordan det går. Du kan sende en instans af IProgress til den asynkrone metode, og den vil kunne opdatere UI'et eller give feedback til brugeren.

For at implementere fremdriftsrapportering kan du skrive noget som dette:

csharp
Task DownloadDataTaskAsync(Uri address, CancellationToken cancellationToken, IProgress progress)

For at bruge en sådan metode skal du oprette en implementering af IProgress, som kan modtage opdateringer og handle på dem. Et godt eksempel på en simpel fremdriftsrapportering er:

csharp
new Progress<int>(percentage => progressBar.Value = percentage);

Progress fanger automatisk den nødvendige SynchronizationContext, hvilket betyder, at opdateringerne altid vil blive sendt til den rigtige tråd, så du ikke behøver at bekymre dig om, hvilken tråd der udfører rapporteringen. Dette er meget nyttigt, da fremdriftsrapporteringen ofte vil blive kaldt fra en anden tråd end den, der startede den oprindelige opgave.

Når du skriver en metode, der understøtter fremdrift, skal du blot kalde Report-metoden på din IProgress-instans:

csharp
progress.Report(percent);

Vær opmærksom på, at den type, du vælger for T, skal være uforanderlig, da den bliver brugt på en anden tråd, hvilket kan føre til problemer, hvis objektet ændres under brug.

At kunne håndtere både afbrydelse og fremdrift i asynkrone operationer giver ikke kun en mere effektiv og kontrolleret brugeroplevelse, men giver også mulighed for at skabe robuste systemer, der kan håndtere både ventetid og brugerinteraktion på en effektiv måde.

Hvilken tråd kører min kode?

Når du arbejder med asynkrone metoder i .NET, opstår et væld af spørgsmål om, hvilken tråd der faktisk kører din kode. Asynkrone operationer giver dig muligheden for at fortsætte med at arbejde på opgaver uden at blokere tråde, hvilket er en af de grundlæggende funktioner, men hvordan hænger dette sammen med de konkrete tråde, der håndterer denne kode?

I en typisk UI-applikation, som for eksempel en Windows Forms eller WPF-applikation, er det første, du bør forstå, at koden, der udføres før den første await i en asynkron metode, altid bliver kørt af UI-tråden. Det betyder, at den oprindelige eksekvering ikke er asynkron, men synkron og sker i UI-tråden. Dette kan virke kontraintuitivt, men det er grundlaget for, hvordan UI-tråden fortsat kan reagere på brugerinput, mens den håndterer baggrundsoperationer.

For webapplikationer som dem, der kører på ASP.NET, er situationen lidt anderledes. Her håndteres den oprindelige eksekvering af en worker-tråd, der er dedikeret til at håndtere anmodninger og afvikling af koden. Når du kører en asynkron metode som en del af en HTTP-anmodning, er det et ASP.NET-worker thread, der kører den initiale kode, før await bliver kaldt. Dette betyder, at det ikke nødvendigvis er den samme tråd, som senere skal afslutte arbejdet, når den asynkrone opgave er fuldført.

Når du bruger await i en asynkron metode, bliver koden suspenderet, indtil den asynkrone opgave er afsluttet. Det er her, synkronisering og trådstyring spiller en vigtig rolle. Asynkronitet betyder ikke nødvendigvis, at du aldrig bruger flere tråde – det betyder blot, at du ikke holder tråde optaget med ventetid, som når du venter på en netværksforespørgsel eller en beregning. Når en opgave er i gang, kan den køre på en af flere tråde uden at blokere dem.

For eksempel, hvis du laver tusind netværksanmodninger på en gang, vil der ikke være tusind tråde, der venter på svar. I stedet bruger operativsystemet en IO completion port-tråd, der håndterer alle de afsluttede anmodninger én ad gangen. Den samme IO-completion port kan håndtere flere anmodninger samtidigt, men antallet af tråde, der faktisk venter på netværkssvar, forbliver lavt. Det betyder, at selvom du har flere anmodninger, vil du ikke nødvendigvis se flere tråde være "blokeret" – IO-completion porten administrerer dette effektivt.

Dette bringer os til begrebet SynchronizationContext. Det er en klasse, der hjælper med at styre, hvilken tråd der kører din kode. I en UI-applikation som WPF eller Windows Forms bliver koden efter et await som regel kørt på den samme tråd, der startede den asynkrone operation, netop på grund af den måde, SynchronizationContext fungerer på. Når await bliver mødt, gemmes den nuværende SynchronizationContext, og når den asynkrone operation afsluttes, bruges denne kontekst til at sikre, at koden fortsætter på den samme tråd.

Der er dog nogle undtagelser. Hvis du arbejder i en applikation uden en specialiseret tråd, som i en konsolapplikation, eller hvis du har konfigureret din opgave til at ignorere SynchronizationContext, vil koden fortsætte på en hvilken som helst tilgængelig tråd. Dette betyder, at det ikke altid er muligt at sikre, at din kode bliver kørt på den samme tråd, som den blev startet på. I en UI-applikation er det dog normalt vigtigt, at koden fortsætter på den oprindelige tråd – det er her, SynchronizationContext spiller en vigtig rolle.

Når man ser på livscyklussen af en asynkron operation, er det tydeligt, hvordan tråde interagerer med hinanden. Lad os tage et konkret eksempel, hvor en bruger klikker på en knap i en UI-applikation, og en asynkron opgave bliver startet. Først bliver koden, der håndterer knaptrykket, kørt af UI-tråden. Denne kode starter en asynkron opgave, som kan være en netværksanmodning. Når den asynkrone opgave er i gang, forlader UI-tråden denne kode og bliver ledig til at håndtere andre brugerinteraktioner. I mellemtiden arbejder IO-completion port-tråden på at hente data fra nettet, uden at nogen tråde bliver blokeret i ventetid.

Når dataene er hentet, og IO-completion port-tråden er færdig, sender den et signal til UI-tråden, som derefter fortsætter den oprindelige asynkrone kode ved hjælp af den opfangede SynchronizationContext. På denne måde kan du sikre, at koden fortsætter på den tråd, som den oprindeligt blev startet på – her UI-tråden – hvilket gør det muligt at opdatere UI’en, når opgaven er afsluttet. Hvis alt går glat, vil dette ske synkront, da den oprindelige tråd er den samme som den opfangede kontekst.

For at forstå disse detaljer er det også vigtigt at huske på, at selvom asynkrone operationer giver os mulighed for at køre kode effektivt uden at blokere tråde, kan dårlig implementering føre til, at UI’en stadig føles langsom. Det er derfor vigtigt at bruge performa

Hvordan bruger man Async i ASP.NET Web Forms og WinRT-applikationer?

ASP.NET Web Forms og standard ASP.NET har ikke en version, der er adskilt fra den version af .NET Framework, som de kører på. Fra .NET 4.5 understøtter ASP.NET async void metoder på din side, som eksempelvis Page_Load. En sådan metode kunne se ud som følgende:

csharp
protected async void Page_Load(object sender, EventArgs e) { Title = await GetTitleAsync(); }

Denne implementation kan virke underlig for nogle. Hvordan ved ASP.NET, hvornår den asynkrone void-metode er færdig? Det ville give mere mening at returnere en Task, som ASP.NET kunne vente på, før den renderede siden, lidt på samme måde som i MVC 4. Men af hensyn til bagudkompatibilitet kræver ASP.NET, at metoder returnerer void. I stedet anvender ASP.NET en speciel SynchronizationContext, som holder styr på de asynkrone operationer og først fortsætter, når alle disse operationer er afsluttet.

Der er dog en vigtig ting at bemærke: når du kører asynkron kode i ASP.NET’s SynchronizationContext, skal du være opmærksom på, at det er en single-threaded kontekst. Hvis du bruger en blokkerende venten på en Task (som eksempelvis ved at bruge Result-egenskaben), risikerer du at forårsage en deadlock. Dette sker, fordi dybere awaits ikke kan bruge SynchronizationContext til at fortsætte deres eksekvering.

I WinRT-applikationer er der et andet sæt udfordringer og løsninger omkring asynkron programmering. WinRT, der står for Windows Runtime, er en samling af API'er, som anvendes i Windows 8-applikationer, der kører på både Windows 8 og Windows RT (for ARM-processorer). Et centralt designmål for WinRT API'er er at opnå høj responsivitet, hvilket bliver muliggjort gennem asynkron programmering. Alle metoder, der kunne tage længere tid end 50 ms, er derfor asynkrone.

WinRT er designet til at kunne anvendes ensartet på tværs af tre helt forskellige teknologistakke: .NET, JavaScript og native kode (som regel C++). API'erne er defineret i et fælles metadataformat kaldet WinMD. Hver af de tre teknologistakke kan så kompilere mod denne WinMD-definition uden at skulle bruge et sprog-specifikt wrapper. Dette system kaldes projection, hvor hver compiler eller interpreter projicerer WinRT-typen, så den kan bruges som en normal type i det pågældende sprog.

WinRT’s asynkrone metoder bruger et mønster, der ligner .NET's Task-based Asynchronous Pattern (TAP), men returnerer i stedet IAsyncAction eller IAsyncOperation. Disse to interfaces arbejder meget ligesom Task og Task i .NET, og de bruges på en næsten identisk måde. Her er et eksempel på en metode i WinRT-typen SyndicationClient, som henter en RSS-feed:

csharp
IAsyncOperation RetrieveFeedAsync(Uri uri)

Både IAsyncAction og IAsyncOperation er ikke .NET-interfaces, men WinMD-interfaces, hvilket kan skabe lidt forvirring, da de kan bruges i C# som om de var normale .NET-interfaces. Når du arbejder med WinRT, kan du bruge await på disse interfaces på samme måde som med Task i .NET. Her er et eksempel:

csharp
SyndicationFeed feed = await rssClient.RetrieveFeedAsync(url);

Som med Task, kan du bruge await på enhver type, der opfylder det specifikke mønster af metoder, som er nødvendige. IAsyncOperation har ikke de samme metoder som Task, men dette mønster kan opfyldes gennem extension metoder. .NET tilbyder disse extension metoder på IAsyncOperation for at få await til at fungere korrekt. Hvis du har brug for at arbejde med et Task, som repræsenterer den asynkrone WinRT-opkald, kan du bruge en extension metode kaldet AsTask:

csharp
Task task = rssClient.RetrieveFeedAsync(url).AsTask();

Med AsTask kan du få et normalt Task-objekt, som kan anvendes på samme måde som i .NET.

Når vi taler om afbrydelse i WinRT, er designet anderledes end i .NET. I .NET TAP passerer vi normalt en CancellationToken som et ekstra parameter, mens WinRT vælger at implementere afbrydelse direkte i den returnerede IAsyncOperation. Det betyder, at du kan afbryde en asynkron operation i WinRT ved at kalde Cancel() på IAsyncOperation:

csharp
IAsyncOperation op = rssClient.RetrieveFeedAsync(url); op.Cancel();

Det er vigtigt at bemærke, at ikke alle WinRT-metoder nødvendigvis stopper, når Cancel bliver kaldt, men denne tilgang gør API'et renere sammenlignet med CancellationToken-tilgangen, hvor du skal propagere en enkelt CancellationToken til mange metoder.

Progress i WinRT følger et andet design end i TAP. I WinRT er muligheden for at returnere progress indbygget i den returnerede promise-type. Når en metode kan give opdateringer om fremdrift, returneres der specialiserede interfaces, som f.eks. IAsyncActionWithProgress og IAsyncOperationWithProgress. Disse interfaces adskiller sig ved at have en event, som bliver affyret, når fremdriften ændres. Den bedste måde at abonnere på denne fremdrift er at bruge en overload af AsTask, som tager en standard .NET IProgress og kobler det sammen med asynkron opkaldet:

csharp
await rssClient.RetrieveFeedAsync(url).AsTask(progress);

Det er også muligt at bruge både en CancellationToken og en IProgress, hvis det er nødvendigt.

Når man skriver sine egne biblioteker til WinRT, er det muligt at udnytte den samme kraft, som de eksisterende WinRT-komponenter tilbyder, ved at kompilere sit bibliotek som en WinRT-komponent i stedet for en .NET assembly. Dette gøres let i C#, men den offentlige interface af din komponent skal kun bruge typer, der enten er WinMD-typer eller automatisk projiceres af compilerne til WinMD-typer. Da Task ikke er en WinMD-type, skal vi derfor returnere en IAsyncOperation i stedet:

csharp
public IAsyncOperation GetTheIntAsync() { return GetTheIntTaskAsync().AsAsyncOperation(); }

.NET extension metoder som AsAsyncOperation og AsAsyncAction gør dette let. AsAsyncOperation konverterer et Task til et IAsyncOperation, og det samme kan gøres med AsAsyncAction for at konvertere et Task til et IAsyncAction.