Async in C#, de kunst van alles tegelijk.

Geschreven door Stefan van Tilborg op

Over de jaren heen heb ik al veel implementaties gezien van asynchrone software. Sommige goed en doordacht, andere helaas minder succesvol. Ik zie dit in mijn omgeving zowel bij startende developers als bij ervaren Xprtz wel eens mis gaan. Dit heeft me geïnspireerd om alle termen rondom Async nog eens langs te gaan in dit artikel

Waarom zou je asynchroon willen programmeren? De belangrijkste reden is: Zodat je intensieve taken kunt uitvoeren zonder dat de verwerking van je applicatie stil ligt tot deze intensieve taken afgerond zijn. Neem bijvoorbeeld het uitvoeren van een zware query om een grote dataset op te halen. Zonder asynchrone verwerking zou een klik op een button kunnen leiden tot een userinterface die niet meer reageert tot de query voltooid is. Het is hierbij belangrijk om de termen van asynchroon programmeren helder te hebben. Wat is precies concurrency? Wanneer verwerkt je applicatie data parallel? Hieronder een korte toelichting op deze termen.

Process

Een proces is een programma dat op je computer wordt uitgevoerd. Dit kan van alles zijn, van een kleine achtergrondtaak, zoals een spellingcontrole of systeemgebeurtenisafhandeling tot een volwaardige toepassing zoals Microsoft Edge of Microsoft Word. Elk proces heeft minstens één thread. Proces

Thread

Een thread wordt gedefinieerd als het “execution path” of uitvoeringspad van een programma. Elke thread definieert een unieke “flow of control”. Als je applicatie ingewikkelde en langlopende bewerkingen uitvoert, is het vaak handig om verschillende uitvoeringspaden of -threads in te stellen, waarbij elke thread een bepaalde taak uitvoert.

Asynchroon

Asynchroon programmeren verwijst naar het optreden van gebeurtenissen die onafhankelijk zijn van de hoofdprogramma-flow en manieren om met dergelijke gebeurtenissen om te gaan.

Concurrent

Wanneer een applicatie meerdere threads heeft en deze threads verwerkt op een enkele processor/core middels context switches, dan spreek je over concurrent processing.

Parallel

Wanneer een applicatie meerdere threads heeft en elk van deze threads verwerkt op een andere processor/core, dan spreek je over parallel processing.

Concurrent vs parallel

Implementatie

Nu dat we de termen helder hebben kunnen we aan de slag. De basis van asynchroon programmeren in C# is de Task. Hieronder een voorbeeld om deze toe te passen;

namespace TaskExample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var calc = new Calculator();
            var task = calc.CalculateFaculteit(10);
            
            while(!task.IsCompleted)
            {
                Console.WriteLine("waiting....");
            }
            
            Console.WriteLine($"Resultaat: {task.Result}");
        }
    }

    public class Calculator
    {
        public async Task<int> CalculateFaculteit(int faculteit)
        {
            var resultaat = 1; //start bij 1

            for (int i = 2; i <= faculteit; i++)
            {
                resultaat = resultaat * i;                
                await Task.Delay(5); // vertraag even
            }
            return resultaat;
        }
    }
} 

De Task wordt gestart op regel 8 en zal afzonderlijk van de main methode uitgevoerd worden. Met de while loop wordt gechecked of de task inmiddels al klaar is. Zodra CalculateFaculteit zijn berekening heeft voltooid zal de while loop breaken en wordt het resultaat geoutput naar de Console. Dit is een heel eenvoudig voorbeeld maar geeft de essentie van asynchrone verwerking weer.

Deadlocks

Een gevaar van asynchroon programmeren zijn deadlocks . Als voorbeeld heb ik een WPF app met een Listbox en een button: WPF UI

De button bevat de volgende code:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Results.Items.Clear();
        Results.Items.Add("Button clicked");
        DoWork();
        Results.Items.Add("Button click done");

    }

    private async Task WaitAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
    }

    private void DoWork()
    {
        Results.Items.Add("Do work start");

        Results.Items.Add("Do waitasync start");
        Task task = WaitAsync();            
        task.GetAwaiter().GetResult();
        Results.Items.Add("Do waitasync stop");

        Results.Items.Add("Do work done");
    }
}

Zodra op de Button geklikt wordt deadlockt de applicatie. Dit is het gevolg van DoWork welke synchroon wacht op het resultaat van de WaitAsync() task door GetAwaiter().GetResult() te gebruiken. De SynchronisateContext van de UI limiteert het aantal threads in de UI tot 1. De taak WaitAsync() wil na voltooien een synchronisatie uitvoeren, maar kan dit niet omdat DoWork() de UI thread blokkeert. ConfigureAwait Bovenstaande deadlock is te voorkomen door synchronisatie uit te zetten. Dit kan door op de Task.Delay() van WaitAsync() de methode ConfigureAwait(false) toe te voegen. Het is aan te raden deze methode standaard aan taken toe te voegen.

Locks

Wanneer je asynchroon werkt dan kan het zijn dat je resources deelt tussen threads. Meerdere threads kunnen tegelijk een waarde van een variabele in een class instantie proberen te wijzigen. Wanneer dit onbedoeld gebeurt kan dit leiden tot corrupte data. Een oplossing hiervoor is het gebruiken van een semaphore of een lock waarmee slechts één thread tegelijk toegang krijgt tot de variabele. Meer hierover kun je vinden bij de dotnet reference: lock statement - C# reference | Microsoft Docs SemaphoreSlim Class (System.Threading) | Microsoft Docs

Dotnet heet een aantal libraries die het asynchroon programmeren makkelijker maken en handige features bevatten. Een aantal hiervan zijn:

  • Task Parallel Library (TPL)
  • TPL Dataflow
  • P-LINQ
  • System.Reactive
  • Immutable / Concurrent / Blocking / ThreadSafe Collections

Het is zeker een aanrader om deze eens te bekijken of ze jouw leven makkelijker kunnen maken.

Tot slot

Om meer te weten te komen over asynchroon programmeren in C# raad ik het boek van Stephen Cleary, Concurrency in C# Cookbook aan. En wij staan uiteraard open voor alle vragen die je mocht hebben.

Links

Als je geïnteresseerd bent om je verder te verdiepen in een van de behandelde onderwerpen, dan zijn hier een aantal nuttige links:

Stefan van TilborgStefan is een .NET expert met veel ervaring mbt integratie vraagstukken en software architecture. Stefan is momenteel werkzaam als solution architect voor het project Digitalisering bij Rademaker.
← Terug
XPRTZ