Threads in C#
Inleiding
C# heeft de mogelijkheden voor het “parallel” doen uitvoeren van dingen en ook een “asynchrone” uitvoering wordt gefaciliteerd. Het wordt vaak in één adem genoemd, maar “parallel” en “asynchroon” zijn twee verschillende concepten met elk een ander uitgangspunt. We zullen de concepten toelichten aan de hand van een garagebedrijf dat onderhoudsbeurten doet aan auto’s.
Multitasken
Analoog aan het voorbeeld van het garagebedrijf kunnen we een computer met een besturingssysteem zien als het onderhoudsbedrijf dat allerlei onderhoudsklussen doet aan auto’s. Het garagebedrijf was een eenmanszaak en het bestond aanvankelijk uit één eigenaar die al het werk deed en we kunnen het garagebedrijf dan ook zien als een single core processor computer. De eigenaar kan eventueel multitasken door afwisselend tijd te besteden aan meerdere auto’s, maar hij is per keer maar met één auto bezig.
Parallel
Het gaat goed met het garagebedrijf en het wordt uitgebreid met meerdere monteurs en we kunnen het garagebedrijf na de uitbreiding als een computer zien met een multicore processor. Onderhoudsbeurten kunnen dankzij de aanwezigheid van meerdere monteurs nu echt parallel gedaan worden.
Als twee klanten tegelijkertijd hun auto brengen dan kunnen ze op hetzelfde tijdstip hun auto weer ophalen omdat monteur één aan de auto werkt van de eerste klant en een tweede monteur werkt aan de auto van de tweede klant. De monteurs en de auto’s zijn niet afhankelijk van elkaar en de monteurs kunnen onafhankelijk van elkaar parallel aan de auto’s werken.
Thread
We kunnen elke auto waaraan gewerkt moet worden als een programma zien. Aan het programma (een auto) moet een processor (monteur) toegewezen worden en de processor (de monteur) moet ook tijd krijgen voor het programma (de klus) zodat de processor (monteur) daadwerkelijk het programma kan draaien (het werk kan uitvoeren).
Wat aan een auto gedaan moet worden, dat kan ook weer opgesplitst worden in afzonderlijke delen. Zo kan parallel aan elkaar één monteur bijvoorbeeld de olie van de auto verversen terwijl een andere monteur een sterretje in de voorruit repareert. Dit soort werkzaamheden hebben allemaal te maken met één auto, maar ze staan los van elkaar en ze kunnen parallel naast elkaar gedaan worden door verschillende monteurs. Elk deel kan gezien worden als een thread.
Een programma kan ook opgedeeld worden in afzonderlijke delen en elk deel is een thread dat opgepakt kan worden door een processor waarbij de processoren de threads parallel naast elkaar kunnen uitvoeren.
Performance
Een parallelle verwerking en threads kunnen leiden tot een betere doorlooptijd (performance), maar de performance kan er ook door verslechteren. Parallel werken heeft pas zin bij dingen die los van elkaar uitgevoerd kunnen worden. Zo zal de performance niet verbeteren met parallel werken als je bijvoorbeeld pas aan iets mag beginnen als een eerdere actie voltooid is. Je kunt weliswaar in die tussentijd wat anders doen, maar je moet nog steeds blijven wachten totdat die eerdere actie voltooid is.
Verder kan het juist averechts werken als je meerdere processoren op een thread gaat zetten. Als monteur A al bezig is met het verversen van de olie dan moet je monteur B er niet opzetten want dan heb je de kans dat monteur B de verse olie gaat weggooien die monteur A net in de auto heeft gegoten.
Ten slotte mag de synchronisatie ook niet uit het oog verloren worden. Als monteur A pas gaat beginnen n.a.v. een seintje van monteur B en monteur B pas gaat beginnen n.a.v. een seintje van monteur A dan gebeurt er niks omdat beide monteurs op elkaar gaan wachten en geen van beiden een signaal geeft. Je hebt dan een deadlock.
Asynchroon
De klant kan ook als een programma gezien worden. De klant wil zijn dingen blijven doen en dat is mogelijk als de onderhoudsbeurt asynchroon uitgevoerd kan worden. De klant (het hoofdprogramma) kan eventueel naar huis om daar zijn eigen dingen te doen en het hoeft niet te werkeloos in de wachtkamer te wachten gedurende de tijd dat de monteur (het subprogramma) bezig is met de auto.
Bij computerprogramma’s komt een niet asynchrone werking tot uiting in schermen die “bevriezen”. Het scherm van het hoofdprogramma roept een subprogramma aan en het blokkeert gedurende de tijd dat het subprogramma bezig is.
Zo is het niet altijd zinvol om de doorlooptijd van een onderhoudsbeurt terug te brengen middels parallel werken. Je kan de doorlooptijd bijvoorbeeld terugbrengen van twee naar één uur zodat de auto nog eerder op de dag klaar is, maar voor de klant hoeft het niet en hij zal er niks van merken als hij de auto toch pas aan het einde van de dag ophaalt. Bij parallel werken gaat het om terugbrengen van de doorlooptijd terwijl bij asynchroniteit het kunnen doen van andere dingen tijdens de onderhoudsbeurt belangrijker is en de doorlooptijd wat minder van belang is.
We zullen in deze post demonstreren hoe je in C# threads kan activeren.
Voorbeeldprogramma
We demonstreren het gebruik van Threads met onderstaand voorbeeldprogramma. We maken een klasse OlieTank en de klasse is in het bezit van het volgende:
– een constructor,
– een property (property Inhoud dat staat voor het aantal liters dat de olietank bevat).
– een lockObject en
– een methode (methode TapAf).
Voor het aftappen wordt gebruik gemaakt van methode TapAf en we beschouwen deze methode als een deeltaak die we gaan toekennen aan een Thread zodat de Thread weer opgepakt kan worden door de processor die op dat moment beschikbaar is.
We gebruiken voor methode TapAf een lock object. Dit om er zeker van te zijn dat een monteur de olie af kan tappen die hij nodig heeft voor zijn olieverversingsbeurt en niet dat een andere monteur (thread) voorkruipt en er vandoor gaat met de olie die niet voor hem is bestemd. Verder wordt er met een lock object voor gezorgd dat property Inhoud correct wordt bijgewerkt met het aantal liters dat de monteur (thread) in kwestie daadwerkelijk aftapt.
using System;
using System.Threading;
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 TapAf()
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";
}
}
}
//------------------------------------------
class Program
{
static void Main(string[] args)
{
//Instantieer object
OlieTank olieTank = new OlieTank(100);
// Threads
Thread[] threads = new Thread[4];
// Eerste Thread
Thread thread =
new Thread(()
=> Console.WriteLine(olieTank.TapAf(10)));
threads[0] = thread;
// Tweede Thread
thread =
new Thread(()
=> Console.WriteLine(olieTank.TapAf(30)));
threads[1] = thread;
// Derde Thread
thread =
new Thread(()
=> Console.WriteLine(olieTank.TapAf(50)));
threads[2] = thread;
// Vierde Thread
thread =
new Thread(()
=> Console.WriteLine(olieTank.TapAf(70)));
threads[3] = thread;
// Start de Threads
threads[0].Start();
threads[1].Start();
threads[2].Start();
threads[3].Start();
// Gezien?
Console.ReadKey();
}
}
}
Resultaat
We starten het programma en we zien dat de aftapbeurten niet worden gedaan in de volgorde waarin we de threads hebben geactiveerd. Hoe kan dat?
Een verklaring is dat het besturingssyteem de eerste aftapbeurt /thread toekent aan een processor één waarbij de processor nog even bezig is met anders. Processor twee krijgt tegelijkertijd met processor één ook een uit te voeren aftapbeurt/thread en processor twee is in tegenstelling tot processor één wel direct beschikbaar. Het gevolg is dat de aftapbeurt van 30 liter (door processor twee) eerder geschiedt dan de aftapbeurt van 10 liter door processor één.
Perfmon
We kijken met perfmon (zie de post over Performance Counters) wat er gebeurt en we zien een piek waarin alle processoren (bijna) tegelijkertijd hun taak (aftapbeurt) doen. Zo moeten wel (een milliseconde?) op elkaar wachten want je mag geen olie aftappen als iemand anders al bezig is met een aftapbeurt. Het aftappen van de olie geschiedt in dit geval dus niet volledig parallel, maar het is ook niet zo dat al het werk door maar één processor wordt gedaan en de andere processoren werkeloos vanaf de zijlijn toekijken:
Slot
Ik ben deze post begonnen met een uiteenzetting over het “parallel” en het “asynchroon” doen uitvoeren van dingen door een computer. Het wordt vaak in één adem genoemd, maar “parallel” en “asynchroon” zijn twee verschillende concepten met elk een ander uitgangspunt en ik heb de concepten toegelicht aan de hand van een garagebedrijf dat onderhoudsbeurten doet aan auto’s.
Computers worden steeds krachtiger. Zo hadden we jaren geleden computers met één processor waarbij die ene processor zijn verwerkingskracht verdeelt over meerdere programma’s (multitasken) tot wat meer recenter, computers met meerdere processoren waarbij je alle processoren kan benutten voor je programma’s en de processoren parallel programma’s naast elkaar kunnen laten draaien.
De capaciteit van je computer kan je nog efficienter benutten als je ook je programma zou kunnen opsplitsen in meerdere taken. Die taken zou je dan willen toekennen aan de diverse processoren die je computer inmiddels heeft. Het is inmiddels allemaal mogelijk met threads. Een thread staat voor het opsplitsen van een programma in afzonderlijke onderdelen waarbij die onderdelen parallel naast elkaar uitgevoerd kunnen worden door meerdere processoren. We hebben in deze post gedemonstreerd hoe je in C# threads kan activeren.
Maar hoe krachtig een computer tegenwoordig ook mag wezen, het parallel doen uitvoeren van zaken leidt pas echt tot een betere performance als het gaat om zaken die los van elkaar uitgevoerd kunnen worden en geen afhankelijkheden hebben jegens elkaar. Als dat wel het geval is dan zal je alsnog moeten wachten op het voltooid zijn van een andere actie. Je kunt weliswaar in die tussentijd wat anders doen, maar je moet voor de voltooiing van de klus nog steeds wachten totdat die eerdere actie voltooid is.
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…