Spelen met de Task Parallel Library

Ze zeggen soms dat je iets pas begrijpt als je het kan uitleggen. Ze liegen niet: ik wou bloggen over asynchrone controllers, maak kwam uiteindelijk veel meer te weten over de Task Parallel Library en C#5 dan ik dacht. Ik leerde ook dat ik twee concepten helemaal door elkaar haalde: asynchroon en parallel.

Of, hoe één blogpost er plots twee werden…

Stel je eens voor dat je een controller hebt in asp.net MVC die gegevens uit drie verschillende bronnen moet gaan combineren. De drie bronnen zijn nogal traag en het duurt een tweetal seconden voordat er een antwoord komt:

public class TestOutput
{
    public string One { get; set; }
    public string Two { get; set; }
    public string Three { get; set; }

    public static string DoWork(>string input)
    {
        Thread.Sleep(2000);
        return input;
    }
}

public class SerialController : Controller
{
    public ActionResult Index()
    {
        var output = new TestOutput();
        output.One = TestOutput.DoWork("1");
        output.Two = TestOutput.DoWork("2");
        output.Three = TestOutput.DoWork("3");

        return View(output);
    }
}

Het is een beetje jammer om zes seconden te wachten op drie dingen die compleet onafhankelijk zijn van elkaar. Het is interessanter om de drie bronnen tegelijk aan te spreken. Vroeger zou dit betekenen dat we het BeginMethod/EndMethod pattern zouden implementeren op onze DoWork methode. Die zou er dan plots heel ingewikkeld beginnen uitzien omdat we allerlei vieze dingen moeten doen met Threads.

Gelukkig hebben ze bij Microsoft genoeg mensen die graag vieze dingen doem met Threads. Die mensen hebben ons de Task Parallel Library (TPL) geschonken. Deze library bevat een aantal klassen en methodes die threads mooi wegstoppen voor ons. De basis van TPL is de Task klasse. Een Task omvat een delegate naar de functie die het eigenlijke werk doet. Eenmaal het werk is uitgevoerd, kan de Task het resultaat ervan doorgeven aan een volgende delegate om er iets nuttigs mee te doen. Dankzij lambda functies en een aantal helpers kunnen we zo’n Task gemakkelijk als volgt aanmaken:

string result;

Task.Factory.StartNew(
    () => TestOutput.DoWork("One")
).ContinueWith(
    s => result = s.Result
);

Daarnaast bevat de TPL ook alle hulpmiddelen om te wachten op het resultaat van de verschillende taken. Dit zorgt er voor dat onze action er nu als volgt zal uitzien:

public ActionResult Parallel()
{
    var output = new TestOutput();

    Task.WaitAll(
        Task.Factory.StartNew(() => TestOutput.DoWork("One")).ContinueWith(s => output.One = s.Result),
        Task.Factory.StartNew(() => TestOutput.DoWork("Two")).ContinueWith(s => output.Two = s.Result),
        Task.Factory.StartNew(() => TestOutput.DoWork("Three")).ContinueWith(s => output.Three = s.Result)
    );

    return View("index", output);
}

Als we nu deze action oproepen dan krijgen we na 2 seconden al een resultaat! Uiteraard moet je PC natuurlijk in staat zijn om voldoende threads simultaan af te handelen, maar het opzetten en afhandelen ervan wordt allemaal mooi weggestoken en afgehandeld door de TPL. Toch zitten we nog steeds met een server die twee seconden lang ‘druk’ bezig is met wachten op een resultaat. Dat zou toch ook beter moeten kunnen. Maar dat is dan weer voer voor de volgende post.