Parallel vs Asynchroon

In de vorige post heb ik vlug getoond hoe je met de Task Parallel Library code gelijktijdig kan laten uitvoeren. Daar was niet zo bijzonder. In deze post gaan we het parallelisme van de TPL gaan uitbreiden met asynchroniteit.

Een Verhaaltje

Maar eerst een waar gebeurd verhaal. Ik was eens in de Ikea. Ik had nog drie extra stukken nodig om een kast in elkaar te kunnen steken. De stukken lagen niet in de open rekken. Ik ging naar de balie en wachtte mijn beurt af. Toen het eindelijk aan mij was, stuurde de Ikea-medewerker er drie magazijniers erop uit om de drie stukken van mijn kast uit het magazijn te halen. Daarna vertelde de medewerker me om eventjes te wachten totdat de magazijniers alles gevonden hadden. Terwijl ik stond te wachten, hielp de Ikea-medewerker de volgende in de wachtrij verder.

“Wat een mooie analogie met asp.net”, dacht ik bij mezelf. “Die moet ik gebruiken als ik ooit blog over asynchroniteit!”

Inderdaad, net zoals er een beperkt aantal Ikea-medewerkers aan de balie staan, beschikt asp.net over beperkte resources om http-requests af te handelen: Worker Threads. Als een request de pipeline van asp.net binnenkomt dan krijgt deze een Thread toegewezen. In principe wordt deze Thread pas vrijgegeven wanneer de volledige request is afgehandeld. We kunnen onze request vlugger afhandelen door werk parallel te laten uitvoeren.

Door drie magazijniers in het werk te steken, ben ik vlugger geholpen. Maar voor de andere mensen die staan te wachten aan de balie is het frustrerend om te wachten terwijl de Ikea-medewerker toch niets aan het doen is. Hier komt het asynchrone naar boven. Omdat ik toch aan het wachten ben om de magazijniers kan de Ikea-medewerker de volgende persoon in de wachtrij verder helpen. Voor mij maakt dit niets uit, maar de wachtrij zal vlugger afgehandeld worden.

To The Codes!

Hoe gaan we dit niet simuleren in asp.net? In MVC3 hebben we de AsyncController gekregen. Als we deze gebruiken, kunnen we het volgend patroon gebruiken in onze Action Methods:

public class ParallelController : AsyncController
{
    public void IndexAsync()
    {
    }

    public ActionResult IndexCompleted(string one, string two, string three)
    {
    }
}

De IndexAsync methode is waar de request binnenkomt. Hier starten we onze asynchrone operaties. IndexCompleted wordt opgeroepen wanneer alle operaties zijn afgewerkt. Om dit alles te coördineren, moeten we de AsyncManager gebruiken. Eerst moeten we aangeven hoeveel taken er uitgevoerd zullen worden vooraleer de IndexCompleted method mag worden opgeroepen:

public void IndexAsync()
{
    AsyncManager.OutstandingOperations.Increment(3);
}

Daarna starten we onze operaties op met de TPL. Telkens er een operatie is afgerond, moeten we dit laten weten aan de AsyncManager. We kunnen ook het resultaat van onze operaties doorgeven via de Parameters property. De naam die we meegeven aan de indexer moet gelijk zijn aan de naam van de property op de IndexCompleted methode.

public void IndexAsync()
{
    AsyncManager.OutstandingOperations.Increment(3);

    Task.Factory.StartNew(() => TestOutput.DoWork("1")
        .ContinueWith(t =>
        {
            AsyncManager.OutstandingOperations.Decrement();
            AsyncManager.Parameters["one"] = t.Result;
        });
    Task.Factory.StartNew(() => TestOutput.DoWork("2"))
        .ContinueWith(t =>
        {
            AsyncManager.OutstandingOperations.Decrement();
            AsyncManager.Parameters["two"] = t.Result;
        });
    Task.Factory.StartNew(() => TestOutput.DoWork("3"))
        .ContinueWith(t =>
        {
            AsyncManager.OutstandingOperations.Decrement();
            AsyncManager.Parameters["three"] = t.Result;
        });
}

Als alle taken zijn opgestart, wordt de huidige Thread vrijgegeven voor een andere request. Als de AsyncManager te horen krijgt dat de OutstandingOperations zijn teruggebracht tot nul dan wordt IndexCompleted opgeroepen in een nieuwe Thread die wel dezelfde context heeft als de originele. Deze wordt dan gebruikt om de rest van de request af te handelen:

public ActionResult IndexCompleted(string one, string two, string three)
{
    return View(new TestOutput{ One = one, Two = two, Three = three);
}

Net zoals onze vorige versie duurt het een tweetal seconden tot de browser een antwoord krijgt. Het grote verschil is dat tijdens deze twee seconden de server niet geblokkeerd is. Net zoals de Ikea-medewerker kon de server ondertussen anderen helpen.

Alleen jammer dat het zo’n lelijke code oplevert. Al die loodgieterij met de AsyncManager, de action gaan opsplitsen, magic strings,…

C#5 To The Resque!

Dat dit nogal omslachtig en vuil is, moeten ze in Redmond ook gedacht hebben. Daarom dat één van de belangrijkste features in  C#5 twee nieuwe keywords zijn: await en async.

Met het keyword ‘async’ geef je aan dat er in een methode asynchroon werk gedaan wordt. Het echte werk wordt gedaan door ‘await’. ‘Await’ ga je gebruiken om aan de compiler te laten weten dat hetgene wat verder op de regel staat asynchroon zal worden uitgevoerd. Ondertussen wordt de huidige thread vrijgegeven en de regels code die onder een lijn met ‘await’ staan, worden hervat eenmaal de asychrone zaken zijn afgerond. Het lijkt ingewikkeld, maar het wordt duidelijker met code. Onze action van daarnet kan als volgt herschreven worden:

public async Task<ActionResult> Index()
{
    var results = await Task.WhenAll(
            Task.Run(() => TestOutput.DoWork("one")),
            Task.Run(() => TestOutput.DoWork("two")),
            Task.Run(() => TestOutput.DoWork("three"))
        );

    View(new TestOutput
    {
        One = results[0],
        Two = results[1],
        Three = results[2]
    });
}

Dat ziet er al heel wat properder uit! We hebben de zelfde functionaliteit als daarnet: De taken worden nog steeds parallel uitgevoerd en de Worker Thread wordt nog steeds vrijgegeven totdat alle taken afgewerkt zijn. Maar de code ziet is weer leesbaar en bevrijd van alle bijkomstigheden.