TPL en C#

  1. Inleiding
  2. TPL en de CPU
  3. TPL en Taken
  4. TPL en de Cancellation Token
  5. Slot

Inleiding

Het krijgen van grip op parallelle processen en het afstemmen van die parallelle processen op elkaar? Dat kan met Threads een enorme uitdaging zijn. Ook de makers van C# was dit niet ontgaan en men heeft uiteindelijk de Task Parallel Library (TPL) ontwikkeld en beschikbaar gesteld in C# 4.0.

Het parallel laten draaien van een programma of het parallel laten draaien van onderdelen van een programma wordt met TPL voor een ontwikkelaar wat eenvoudiger.

TPL kan een aantal zaken van de ontwikkelaar overnemen zodat de ontwikkelaar niet meer zelf hoeft te kijken naar bijvoorbeeld het aantal Threads dat gedefinieerd moet worden en hoe de threads ingesteld moeten worden opdat alles zo optimaal mogelijk wordt opgepakt door de aanwezige processoren in de computer. Een aantal aspecten van de TPL zullen we in deze post bespreken.

Deze post gaat verder met het voorbeeld dat in de post over Threads is besproken. Het verdient de aanbeveling eerst de desbetreffende post te lezen alvorens verder te gaan met deze post.

up | down

TPL en de CPU

We beginnen met het besteden van aandacht aan een “huishoudelijke” taak van de TPL. Wat moeten we doen om ervoor te zorgen dat meerdere processoren ingezet worden voor de uitvoering van je programma?  In het geval van de TPL niks want de TPL regelt dat voor je achter de schermen.

Dit in tegenstelling tot een Thread. Als je een Thread gebruikt dan zal per Thread maar één processor benut worden en je zal moeten nadenken over hoeveel Threads je dan wel niet wil gebruiken.

We zullen het één en ander demonstreren met onderstaand voorbeeldprogramma. We laten de telling door een Thread doen waarop een programma “Herhalingen_Thread” draait:

using System;
using System.Threading;

namespace Parallellisme
{
 class Program
 {
  static void Main(string[] args)
  {
 
   // Met een Thread
   Thread thread = new Thread(() 
      => Herhalingen_Thread());
   thread.Start();

  }

  private static void Herhalingen_Thread()
  {
   for (int i = 0; i < 300000; i++)
   {
    Console.WriteLine(i + ".");
   }
  }

 }
}

We kijken met perfmon (zie de post over Performance Counters) wat er gebeurt als we het tellen laten doen door een Thread:

En we zien dat alleen maar één processor (die met het blauwe lijntje) echt bezig is met jouw programma. De overige processoren doen misschien ook wel iets met jouw programma, maar veel zal het niet zijn.

Maar waarom waren in het voorbeeld in de post over Threads dan wel meerdere processoren bezig? Het antwoord is dat we in het desbetreffende voorbeeld meerdere Threads hadden geactiveerd en we hebben in dit voorbeeld maar één thread geactiveerd.

Je zou dus meerdere threads moeten activeren bij een computer met meerdere processoren.  Maar hoeveel threads moet je dan wel niet activeren voor de meest optimale verwerking? En trekt de computer het nog wel als je te veel Threads activeert?

We laten de telling nu door de TPL doen waarbij de TPL zelf gaat tellen (de Parallel.For):

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

namespace Parallellisme
{
 class Program
 {
  static void Main(string[] args)
  {
   // TPL
   Parallel.For(0, 300000, i => Herhalingen_TPL(i));
  }

  private static void Herhalingen_TPL(int i)
  {
   Console.WriteLine(i);a
  }

 }
}

En we kijken met perfmon naar wat er gebeurt als we het tellen door de TPL laten doen en wat zien we? We zien dat alle beschikbare processoren door de TPL zijn gemobiliseerd en de TPL doet dat zo te zien vrij goed. M.a.w. je hoeft je eigenlijk niet meer bezig te houden met het vraagstuk hoe de processoren zo optimaal mogelijk te benutten. Je kan het overlaten aan de TPL.

up | down

TPL en Taken

In de post over Threads hadden we het voorbeeld van een olietank waar meerdere monteurs naar toegaan om de olie af te tappen die zij nodig hebben voor hun onderhoudsbeurten. We gebruikten voor iedere aftapbeurt een Thread en we zagen dat de  monteurs zich als “ongeleide projectielen” op de olietank storten.

De vraag is of we invloed kunnen uitoefenen op de volgorde waarin alles parallel wordt verwerkt. Het antwoord is “ja” en we kunnen dat als volgt met de TPL doen:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Threading
{
 class OlieTank
 {
  private Object lockObject = new object();

  public double Inhoud { get; set; }

  // Constructor
  public OlieTank(double inhoud)
  {
   Inhoud = inhoud;
   Console.WriteLine(
     "De olietank heeft een 
      initiele inhoud van {0} liter.\r\n", Inhoud);
  }

  // Methode Neemop()
  public string TapAf(double liter)
  {
   double beginSaldo;
   lock (lockObject)
   {
    beginSaldo = Inhoud;
    if ((Inhoud - liter) < 0)
    {
     return
      "In de olietank zit nog " + 
       Inhoud + " liter.\r\n" +
      "U wilt " + liter +
      " liter aftappen wat te veel is \r\n
        voor de inhoud van deze olietank " +
      "(" + Inhoud + " liter)\r\n";
    }
    Inhoud -= liter;
    return
     "U heeft " + liter + " liter afgetapt.\r\n" +
     "Voor uw aftapbeurt zat " + 
      (Inhoud + liter) + 
      " liter in de olietank.\r\n" +
     "Na uw aftapbeurt zit nog " + 
      Inhoud + 
      " liter in de olietank.\r\n\r\n";
   }
  }

  // Methode VraagGoedKeuring
  public string VraagGoedKeuring()
  {
   Thread.Sleep(1000);
   return "Goedkeuring is gevraagd 
           en gekregen.\r\n";
  }

 }

 class Program
 {
  static void Main(string[] args)
  {
   //Instantieer object
   OlieTank olieTank = new OlieTank(100);

   // die 30 Liter pas opnemen 
   // nadat die 10 Liter is opgenomen
   var taak1 = Task.Factory.StartNew(() 
       => Console.WriteLine(olieTank.TapAf(10)))
    .ContinueWith((vorigeTaak) 
       => Console.WriteLine(olieTank.TapAf(30)));

   // dit zijn wat meer liters 
   // Eerst goedkeuring vragen.
   var taak2 = Task.Factory.StartNew(() 
        => Console.WriteLine
           (olieTank.VraagGoedKeuring()))
       .ContinueWith((vorigeTaak) 
        => Console.WriteLine
           (olieTank.TapAf(70)));

   // wachten totdat taak1 en 
   // taak2 gedaan zijn 
   var takenlijst 
      = new List<Task> { taak1, taak2 };
   Task.WaitAll(takenlijst.ToArray());

   // taak1 en taak2 zijn gedaan, 
   // we mogen verder met taak3
   var taak3 = Task.Factory.StartNew(() 
         => Console.WriteLine(olieTank.TapAf(50)));

   // Gezien?
   Console.ReadKey();
  }
 }
}

De eerste .ContinueWith() zorgt ervoor dat de aftapbeurt van 30 liter pas na de aftapbeurt van 10 liter geschiedt.

De tweede .ContinueWith() zorgt ervoor dat de aftapbeurt van 70 liter pas geschiedt nadat toestemming is gegeven. Het krijgen van goedkeuring kost tijd en als de toestemming is verkregen dan “mis je altijd de boot” want andere aftapbeurten hebben reeds plaatsgevonden en er is daardoor te weinig in de olietank over voor die 70 liter.

Ten slotte komt de boeking van 50 liter altijd als laatste en dat komt door de .Task.WaitAll().

Je hebt de aftapbeurten nu enigszins gereguleerd. De monteurs storten zich niet meer als ongeleide projectielen op de olietank:

up | down

TPL en de Cancellation Token

Je hebt met de TPL een taak opgestart dat parallel gaat draaien en het zou prettig zijn als je die taak kan afbreken indien gewenst en dat kan gelukkig ook met een CancellationToken.

Hieronder een voorbeeldprogramma waarin we gebruik maken van een CancellationToken. We kunnen het volgende zeggen over de CancellationToken:

  • We beginnen met een methode TapAf dat als een taak via de TPL uitgevoerd moet worden. De methode krijgt als parameter een CancellationToken mee en de methode kan via die token zien of vanuit het hoofdprogramma een “signaal” is gegeven dat gestopt moet worden.

  • De desbetreffende methode breekt dan zijn acties af en werpt met ThrowIfCancellationRequested een Exception op naar het hoofdprogramma.

  • De methode laat ten slotte nog weten hoever hij is gekomen voordat hij moest stoppen, maar als hij niet is onderbroken dan laat hij dat ook weten.

  • Vanuit het hoofdprogramma wordt een CancellationTokenSource object geinstantieerd en vanuit dat object wordt een CancellationToken meegegeven aan de methode die als Task gaat draaien (methode TapAf).

  • In dit geval is een druk op willekeurig welke toets het signaal voor de methode om zijn werkzaamheden af te breken en het signaal bereikt methode TapAf via die CancellationToken.

  • Er komt, als het signaal om te stoppen methode TapAf heeft bereikt en de methode zijn werkzaamheden heeft afgebroken een Exception terug die het hoofdprogramma zal afvangen.

In de broncode van onderstaand voorbeeldprogramma zien we verder dat de tekst “Druk op een toets om de taak te stoppen.” en de bijbehorende ReadKey() na de code van de taak komt. Als we kijken naar de output dan zien we dat de “Druk op een toets om de taak te stoppen.”-tekst plus bijbehorende ReadKey() voor de output van de taak verschijnt.

Een verklaring is dat de TPL ervoor gaat zorgen dat de taak parallel gaat draaien op een nieuwe Thread naast het hoofdprogramma. Het hoofdprogramma gaat verder niet wachten op wat de taken parallel naast het hoofdprogramma doen waardoor (in dit geval) de tekst “Druk op een toets om de taak te stoppen.” en de bijbehorende ReadKey() eerder wordt getoond dan de output van de taak.

using System;
using System.Threading;
using System.Threading.Tasks;
namespace AfbrekenTaak
{
 class Program
 {
  static void Main(string[] args)
  {
   Console.WriteLine("*** Begin Programma ***");

   // CancellationTokenSource
   CancellationTokenSource 
   cancellationTokenSource =
   new CancellationTokenSource();

   // De taak
   Task taak =
   Task.Run(() 
   => TapAf(cancellationTokenSource.Token));

   // Wat te doen om de taak 
   // tussentijds te stoppen.
   Console.WriteLine(
   "\r\nDruk op een toets om de taak 
    te stoppen.\r\n");
   Console.ReadKey();

   // Wat is de afloop?
   if (!taak.IsCompleted)
   {
    try
    {
     // Afsluiten
     cancellationTokenSource.Cancel();
     taak.Wait();
    }

    catch (AggregateException ex)
    {
     Console.WriteLine(
     "Exception - type  : " +
     ex.InnerExceptions[0].GetType());
     Console.WriteLine(
     "Exception - boodschap: " +
     ex.InnerExceptions[0].Message);

     // De vorige ReadKey() is gebruikt 
     // om de taak te stoppen
     Console.WriteLine(
     "\r\nGezien? Druk op een toets 
     om verder te gaan.\r\n");

     // Daarom nog een ReadKey() 
     // om de meldingen te zien
     //voordat het programma afsluit
     Console.WriteLine("*** Einde Programma ***");
     Console.ReadKey();
    }
   }
  }

  private static void TapAf
  (CancellationToken cancellationToken)
  {
   int tankbeurt = 1;
   Random random = new Random();

   while 
   (!cancellationToken.IsCancellationRequested)
   {
    Console.WriteLine(
    "Tankbeurt {0} getankt: {1} liter.", 
     tankbeurt, random.Next(1, 65));
    tankbeurt++;
    Thread.Sleep(250);
   }

   if (cancellationToken.IsCancellationRequested)
    Console.WriteLine("\r\nDe taak is afgebroken 
    na tankbeurt {0}.\r\n", tankbeurt - 1);
   else
   {
    Console.WriteLine(
    "\r\nAlles van de taak is gedaan. " +
    "Druk op een toets om verder te gaan.\r\n");
   }
   cancellationToken.ThrowIfCancellationRequested();
  }
 }
}

De IDE van Visual Studio is een beetje overijverig en zal meteen de Exception tonen die methode ThrowIfCancellationRequested heeft opgeworpen als we het programma opstarten vanuit de IDE van Visual Studio en als we het programma afbreken met een druk op de knop.

Wij willen echter zien hoe alles wordt getoond als het programma buiten Visual Studio draait en we starten de gegenereerde .exe dan ook op vanuit een .cmdbestand (Windows Command Script) en we krijgen dit te zien:

up | down

Slot

In deze post heb ik laten zien dat met TPL alle processoren van je computer voor je programma worden gemobiliseerd. Je hoeft je daardoor zelf niet meer bezig te houden met het vraagstuk hoe de processoren zo optimaal mogelijk te benutten. Je kan het overlaten aan de TPL.

Verder heb ik laten zien hoe je taken aanmaakt in TPL en hoe je de volgorde waarin de taken worden uitgevoerd met een .ContinueWith en een .WaitAll enigszins kunt reguleren.

Ten slotte heb ik laten zien hoe je met een CancellationToken de taken van de TPL kunt afbreken.

Hopelijk ben je met deze posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over C# heb geschreven? Hit the C# button…

up


Laat een reactie achter

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *