10. Uitvoeringsthreads
10.1. De klasse Thread
Wanneer een applicatie wordt gestart, wordt deze uitgevoerd in een uitvoeringsstroom die een thread wordt genoemd. De klasse .NET die een thread modelleert, is de klasse System.Threading.Thread en heeft de volgende definitie:
Constructors
![]() |
In de volgende voorbeelden zullen we alleen de constructors [1,3] gebruiken. De constructor [1] accepteert als parameter een methode met de signatuur [2], c.a.d, die een parameter van het type object heeft en geen resultaat retourneert. De constructor [3] accepteert als parameter een methode met de signatuur [4], c.a.d, die geen parameter heeft en geen resultaat retourneert.
Eigenschappen
Enkele nuttige eigenschappen:
- Thread CurrentThread: statische eigenschap die een verwijzing geeft naar de thread waarin de code zich bevindt die deze eigenschap heeft opgevraagd
- string Name: de naam van de thread
- bool IsAlive: geeft aan of de thread momenteel wordt uitgevoerd of niet.
Methoden
De meest gebruikte methoden zijn de volgende:
- Start(), Start(object obj): start de asynchrone uitvoering van de thread, eventueel door informatie door te geven in een type object.
- Abort(), Abort(object obj): om een thread geforceerd te beëindigen
- Join(): de thread T1 die T2.Join uitvoert, wordt geblokkeerd totdat de thread T2 is voltooid. Er bestaan varianten om het wachten na een bepaalde tijd te beëindigen.
- Sleep(int n): statische methode – de thread die de methode uitvoert, wordt gedurende n milliseconden opgeschort. Hij verliest dan de processor, die aan een andere thread wordt toegewezen.
Laten we eens kijken naar een eerste toepassing die het bestaan van een hoofduitvoeringsthread laat zien, namelijk degene waarin de functie Main van een klasse wordt uitgevoerd:
using System;
using System.Threading;
namespace Chap8 {
class Program {
static void Main(string[] args) {
// huidige thread initialiseren
Thread main = Thread.CurrentThread;
// weergave
Console.WriteLine("Thread courant : {0}", main.Name);
// de naam wordt gewijzigd
main.Name = "main";
// controle
Console.WriteLine("Thread courant : {0}", main.Name);
// oneindige lus
while (true) {
// weergave
Console.WriteLine("{0} : {1:hh:mm:ss}", main.Name, DateTime.Now);
// tijdelijke stop
Thread.Sleep(1000);
}//while
}
}
}
- regel 8: er wordt een verwijzing opgehaald naar de thread waarin de methode [main] wordt uitgevoerd
- regels 10-14: de naam ervan wordt weergegeven en gewijzigd
- regels 17-22: een lus die elke seconde een weergave uitvoert
- regel 21: de thread waarin de methode [main] wordt uitgevoerd, wordt gedurende 1 seconde opgeschort
De schermresultaten zijn als volgt:
- regel 1: de huidige thread had geen naam
- regel 2: hij heeft er nu wel een
- regels 3-7: de weergave die elke seconde plaatsvindt
- regel 8: het programma wordt onderbroken met Ctrl-C.
10.2. Aanmaken van uitvoeringsthreads
Er zijn toepassingen waarbij stukjes code "gelijktijdig" in verschillende uitvoeringsthreads worden uitgevoerd. Wanneer men zegt dat thread'en gelijktijdig worden uitgevoerd, is dat vaak een verkeerde benaming. Als de machine slechts één processor heeft, zoals nog vaak het geval is, delen de thread'en deze processor: ze beschikken er om de beurt gedurende een kort moment (enkele milliseconden) over. Dit wekt de illusie van parallelle uitvoering. De tijd die aan een thread wordt toegewezen, hangt af van verschillende factoren, waaronder de prioriteit ervan, die een standaardwaarde heeft maar ook programmatisch kan worden ingesteld. Wanneer een thread de processor tot zijn beschikking heeft, gebruikt hij deze normaal gesproken gedurende de volledige tijd die hem is toegewezen. Hij kan de processor echter ook vroegtijdig vrijgeven:
- door te wachten op een gebeurtenis (Wait, Join)
- door gedurende een bepaalde tijd in slaapstand te gaan (Sleep)
- Een thread T wordt in eerste instantie aangemaakt door een van de hierboven genoemde constructors, bijvoorbeeld:
waarbij Start een methode is met een van de volgende twee signaturen:
Het aanmaken van een thread start deze nog niet.
- De uitvoering van thread T wordt gestart door T.Start(): de methode Start die aan de constructor van T wordt doorgegeven, wordt vervolgens door de thread T uitgevoerd. Het programma dat de instructie T.Start() uitvoert, wacht niet tot de taak T is voltooid: het gaat onmiddellijk verder met de volgende instructie. Er zijn dan twee taken die parallel worden uitgevoerd. Deze moeten vaak met elkaar kunnen communiceren om te weten hoe ver het gezamenlijke werk is gevorderd. Dit is het probleem van de synchronisatie van threads.
- Eenmaal gestart, draait thread T autonoom. Hij stopt wanneer de methode Start die hij uitvoert, zijn werk heeft voltooid.
- We kunnen thread T dwingen om te stoppen:
- T.Abort() vraagt thread T om te stoppen.
- Je kunt ook wachten tot de uitvoering ervan is voltooid met T.Join(). Dit is een blokkerende instructie: het programma dat deze uitvoert, wordt geblokkeerd totdat taak T zijn werk heeft voltooid. Dit is een manier van synchronisatie.
Laten we het volgende programma eens bekijken:
using System;
using System.Threading;
namespace Chap8 {
class Program {
public static void Main() {
// huidige thread initialiseren
Thread main = Thread.CurrentThread;
// de thread een naam geven
main.Name = "Main";
// uitvoeringsthreads aanmaken
Thread[] tâches = new Thread[5];
for (int i = 0; i < tâches.Length; i++) {
// thread i wordt aangemaakt
tâches[i] = new Thread(Affiche);
// de naam van de thread wordt vastgelegd
tâches[i].Name = i.ToString();
// de thread i wordt gestart
tâches[i].Start();
}
// einde van de hoofdfunctie
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}",main.Name,DateTime.Now);
}
public static void Affiche() {
// weergave van het begin van de uitvoering
Console.WriteLine("Début d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}",Thread.CurrentThread.Name,DateTime.Now);
// 1 seconde in slaapstand
Thread.Sleep(1000);
// weergave einde uitvoering
Console.WriteLine("Fin d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}", Thread.CurrentThread.Name, DateTime.Now);
}
}
}
- regels 8-10: we geven een naam aan de thread die de methode [Main] uitvoert
- regels 13-21: er worden 5 threads aangemaakt en uitgevoerd. De verwijzingen naar de threads worden in een array opgeslagen, zodat ze later kunnen worden opgehaald. Elke thread voert de methode Affiche uit, zoals beschreven in de regels 27-35.
- regel 20: thread nr. i wordt gestart. Deze bewerking is niet-blokkerend. Thread nr. i wordt parallel uitgevoerd met de thread van de methode [Main] die hem heeft gestart.
- regel 24: de thread die de methode [Main] uitvoert, wordt beëindigd.
- regels 27-35: de methode [Affiche] voert weergaven uit. Ze geeft de naam weer van de thread die haar uitvoert, evenals het begin- en eindtijdstip van de uitvoering.
- regel 31: elke thread die de methode [Affiche] uitvoert, wordt gedurende 1 seconde gestopt. De processor wordt dan toegewezen aan een andere thread die op de processor wacht. Aan het einde van de seconde dat de thread stil ligt, komt de stilgelegde thread weer in aanmerking voor de processor. Deze krijgt de processor toegewezen zodra hij aan de beurt is. Dit hangt af van verschillende factoren, waaronder de prioriteit van de andere threads die op de processor wachten.
De resultaten zijn als volgt:
Deze resultaten zijn zeer leerzaam:
- allereerst zien we dat het starten van de uitvoering van een thread niet blokkerend is. De methode Main heeft de uitvoering van 5 threads parallel gestart en heeft zijn eigen uitvoering eerder voltooid dan die van de threads. De bewerking
start de uitvoering van de thread tâches[i], maar zodra dit is gebeurd, gaat de uitvoering onmiddellijk verder met de volgende instructie, zonder te wachten tot de thread is voltooid.
- Alle aangemaakte threads moeten de methode Affiche uitvoeren. De uitvoervolgorde is onvoorspelbaar. Ook al lijkt de uitvoervolgorde in het voorbeeld de volgorde van de uitvoerverzoeken te volgen, hieruit kunnen geen algemene conclusies worden getrokken. Het besturingssysteem beschikt hier over 6 threads en één processor. Het verdeelt de processor over deze 6 threads volgens zijn eigen regels.
- In de resultaten zien we een gevolg van de methode Sleep. In het voorbeeld is het thread 0 die als eerste de methode Affiche uitvoert. Het bericht dat de uitvoering begint, wordt weergegeven, waarna de methode Sleep wordt uitgevoerd, die de thread gedurende 1 seconde opschort. De thread verliest dan de processor, die daardoor beschikbaar komt voor een andere thread. Het voorbeeld laat zien dat thread 1 de processor krijgt. Thread 1 volgt hetzelfde verloop, net als de andere threads. Wanneer de slaapduur van 1 seconde van thread 0 is verstreken, kan de uitvoering ervan worden hervat. Het systeem wijst de processor aan deze thread toe en deze kan de uitvoering van de methode Affiche voltooien.
Laten we ons programma aanpassen om de methode Main af te sluiten met de instructies:
// einde van de handeling
Console.WriteLine("Fin du thread " + main.Name);
// alle threads worden gestopt
Environment.Exit(0);
De uitvoering van het nieuwe programma levert de volgende resultaten op:
- regels 1-5: de threads die door de functie Main zijn aangemaakt, beginnen met uitvoeren en worden gedurende 1 seconde onderbroken
- regel 6: de thread [Main] krijgt de processor weer toegewezen en voert de instructie uit:
Deze instructie stopt alle threads van de applicatie en niet alleen de thread Main.
Als de methode Main wil wachten tot de threads die zij heeft aangemaakt zijn voltooid, kan zij de methode Join van de klasse Thread gebruiken:
public static void Main() {
...
// wachten op alle threads
for (int i = 0; i < tâches.Length; i++) {
// wachten op het einde van de uitvoering van thread i
tâches[i].Join();
}
// einde van de hoofdprocedure
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
- regel 6: de thread [Main] wacht op elk van de threads. Hij wordt eerst geblokkeerd in afwachting van thread nr. 1, vervolgens van thread nr. 2, enzovoort. Uiteindelijk, wanneer hij uit de lus van de regels 2-5 komt, zijn de vijf threads die hij heeft gestart, voltooid.
Dit levert dan de volgende resultaten op:
- regel 11: de thread [Main] is beëindigd nadat de threads die hij had gestart, waren voltooid.
10.3. Het nut van threads
Nu we hebben aangetoond dat er standaard een thread bestaat – namelijk degene die de methode Main uitvoert – en nu we weten hoe we andere threads kunnen aanmaken, gaan we even stilstaan bij het nut van threads voor ons en bij de redenen waarom we ze hier behandelen. Er is een type applicaties dat zich goed leent voor het gebruik van threads, namelijk client-server-applicaties op het internet. We zullen deze in het volgende hoofdstuk bespreken. In een client-server-toepassing op het internet beantwoordt een server op een machine S1 verzoeken van clients op externe machines C1, C2, ..., Cn.
![]() |
We maken dagelijks gebruik van internettoepassingen die aan dit schema voldoen: webservices, e-mail, het raadplegen van forums, bestandsoverdracht... In het bovenstaande schema moet de server S1 de clients Ci gelijktijdig bedienen. Als we het voorbeeld nemen van een server FTP (File Transfer Protocol) die bestanden aan zijn clients levert, weten we dat een bestandsoverdracht soms meerdere minuten kan duren. Het is natuurlijk uitgesloten dat één client de server gedurende zo’n lange tijd in zijn eentje monopoliseert. Wat gewoonlijk gebeurt, is dat de server evenveel uitvoeringsthreads aanmaakt als er clients zijn. Elke thread is dan verantwoordelijk voor één specifieke client. Aangezien de processor cyclisch wordt verdeeld over alle actieve threads van de machine, besteedt de server een beetje tijd aan elke client, waardoor de gelijktijdigheid van de dienstverlening wordt gewaarborgd.
![]() |
In de praktijk maakt de server gebruik van een threadpool met een beperkt aantal threads, bijvoorbeeld 50. De 51e klant wordt dan gevraagd te wachten.
10.4. Uitwisseling van informatie tussen threads
In de voorgaande voorbeelden werd een thread als volgt geïnitialiseerd:
waarbij Run een methode was met de volgende signatuur:
Het is ook mogelijk om de volgende signatuur te gebruiken:
Hiermee kan informatie worden doorgegeven aan de gestarte thread. Dus
start de thread t, die vervolgens de methode Run uitvoert die er bij het aanmaken aan is gekoppeld, waarbij de daadwerkelijke parameter obj1 wordt doorgegeven. Hier volgt een voorbeeld:
using System;
using System.Threading;
namespace Chap8 {
class Program4 {
public static void Main() {
// de huidige thread initialiseren
Thread main = Thread.CurrentThread;
// we geven de thread een naam
main.Name = "Main";
// uitvoeringsthreads aanmaken
Thread[] tâches = new Thread[5];
Data[] data = new Data[5];
for (int i = 0; i < tâches.Length; i++) {
// de thread i wordt aangemaakt
tâches[i] = new Thread(Sleep);
// de naam van de thread instellen
tâches[i].Name = i.ToString();
// de thread i wordt gestart
tâches[i].Start(data[i] = new Data { Début = DateTime.Now, Durée = i+1 });
}
// wachten op alle threads
for (int i = 0; i < tâches.Length; i++) {
// wachten tot de uitvoering van thread i is voltooid
tâches[i].Join();
// weergave van het resultaat
Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
tâches[i].Name,data[i].Début,data[i].Durée,data[i].Fin,(data[i].Fin-data[i].Début));
}
// einde van de routine
Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
}
public static void Sleep(object infos) {
// de parameter wordt opgehaald
Data data = (Data)infos;
// in slaapstand gedurende Durée seconden
Thread.Sleep(data.Durée*1000);
// einde van de uitvoering
data.Fin = DateTime.Now;
}
}
internal class Data {
// diverse informatie
public DateTime Début { get; set; }
public int Durée { get; set; }
public DateTime Fin { get; set; }
}
}
- regels 45-50: de informatie van het type [Data] die aan de threads wordt doorgegeven:
- Début: tijdstip waarop de thread begint te draaien – vastgesteld door de startende thread
- Durée: duur in seconden van de Sleep die door de gestarte thread wordt uitgevoerd – vastgesteld door de startende thread
- Fin: tijdstip waarop de thread is gestart – vastgesteld door de startende thread
- regels 35-43: de methode Sleep die door de threads wordt uitgevoerd, heeft de signatuur void Sleep(object obj). De daadwerkelijke parameter obj zal van het type [Data] zijn, gedefinieerd op regel 45.
- regels 15-22: aanmaken van 5 threads
- regel 17: elke thread wordt gekoppeld aan de methode Sleep op regel 35
- regel 21: een object van het type [Data] wordt doorgegeven aan de methode Start, die de thread start. In dit object zijn het starttijdstip van de thread en de duur in seconden waarop deze moet slapen vastgelegd. Dit object wordt opgeslagen in de array op regel 14.
- regels 24-30: de thread [Main] wacht tot alle threads die hij heeft gestart, zijn voltooid.
- regels 28-29: de thread [Main] haalt het object data[i] op van thread nr. i en geeft de inhoud ervan weer.
- regels 35-42: de methode Sleep wordt uitgevoerd door de threads
- regel 37: de parameter van het type [Data] wordt opgehaald
- regel 39: het veld Durée van de parameter wordt gebruikt om de duur van Sleep vast te stellen
- regel 41: het veld Fin van de parameter wordt geïnitialiseerd
De resultaten van de uitvoering zijn als volgt:
Dit voorbeeld laat zien dat twee threads informatie met elkaar kunnen uitwisselen:
- de startende thread kan de uitvoering van de gestarte thread sturen door deze informatie te verstrekken
- de gestarte thread kan resultaten terugsturen naar de startende thread.
Om ervoor te zorgen dat de gestarte thread weet wanneer de resultaten waarop hij wacht beschikbaar zijn, moet hij op de hoogte worden gesteld van het einde van de gestarte thread. In dit geval heeft hij gewacht tot deze was voltooid met behulp van de methode Join. Er zijn andere manieren om hetzelfde te doen. Deze zullen we later bespreken.
10.5. Gelijktijdige toegang tot gedeelde bronnen
10.5.1. Niet-gesynchroniseerde gelijktijdige toegang
In de paragraaf over de uitwisseling van informatie tussen threads werd de informatie slechts tussen twee threads uitgewisseld en op welbepaalde momenten. Dit was een klassieke parameteroverdracht. Er zijn andere gevallen waarin informatie wordt gedeeld door meerdere threads die deze tegelijkertijd willen lezen of bijwerken. Dan rijst het probleem van de integriteit van deze informatie. Stel dat de gedeelde informatie een structuur S is met diverse gegevens I1, I2, ... In.
- Een thread T1 begint de structuur S bij te werken: hij wijzigt het veld I1 en wordt onderbroken voordat hij de volledige bijwerking van de structuur S heeft voltooid
- een thread T2 die de processor overneemt, leest vervolgens de structuur S om beslissingen te nemen. Hij leest een structuur in een onstabiele toestand: sommige velden zijn bijgewerkt, andere niet.
Deze situatie wordt ‘toegang tot een gedeelde bron’ genoemd, in dit geval de structuur S, en is vaak vrij lastig te beheren. Laten we het volgende voorbeeld nemen om de problemen te illustreren die kunnen ontstaan:
- een applicatie genereert n threads, waarbij n als parameter wordt doorgegeven
- de gedeelde bron is een teller die door elke gegenereerde thread moet worden verhoogd
- aan het einde van de applicatie wordt de waarde van de teller weergegeven. We zouden dus n moeten vinden.
Het programma ziet er als volgt uit:
using System;
using System.Threading;
namespace Chap8 {
class Program {
// klassevariabelen
static int cptrThreads = 0; // threadteller
//main
public static void Main(string[] args) {
// gebruiksaanwijzing
const string syntaxe = "pg nbThreads";
const int nbMaxThreads = 100;
// controle aantal argumenten
if (args.Length != 1) {
// fout
Console.WriteLine(syntaxe);
// stop
Environment.Exit(1);
}
// controle kwaliteit van het argument
int nbThreads = 0;
bool erreur = false;
try {
nbThreads = int.Parse(args[0]);
if (nbThreads < 1 || nbThreads > nbMaxThreads)
erreur = true;
} catch {
// fout
erreur = true;
}
// fout?
if (erreur) {
// fout
Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)");
// einde
Environment.Exit(2);
}
// aanmaken en genereren van threads
Thread[] threads = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// aanmaken
threads[i] = new Thread(Incrémente);
// naamgeving
threads[i].Name = "" + i;
// starten
threads[i].Start();
}//for
// wachten tot de threads zijn voltooid
for (int i = 0; i < nbThreads; i++) {
threads[i].Join();
}
// teller weergeven
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// verhoogt de threadteller
// teller uitlezen
int valeur = cptrThreads;
// opvolging
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
// wachten
Thread.Sleep(1000);
// teller verhogen
cptrThreads = valeur + 1;
// tracking
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
}
}
}
We zullen niet verder ingaan op het gedeelte over het genereren van threads, dat we al hebben behandeld. Laten we ons in plaats daarvan richten op de methode Incrémente, op regel 59, die door elke thread wordt gebruikt om de statische teller cptrThreads op regel 8 te verhogen.
- regel 62: de teller wordt uitgelezen
- regel 66: de thread stopt 1 s. Hij verliest dus de processor
- regel 68: de teller wordt verhoogd
Stap 2 is er alleen om de thread te dwingen de processor te verliezen. Deze wordt vervolgens aan een andere thread toegewezen. In de praktijk is er geen garantie dat een thread niet wordt onderbroken tussen het moment waarop hij de teller leest en het moment waarop hij deze verhoogt. Zelfs als we cptrThreads++ schrijven, waardoor de schijn van één enkele instructie wordt gewekt, bestaat het risico dat de processor wordt vrijgegeven tussen het moment waarop de waarde van de teller wordt gelezen en het moment waarop de met 1 verhoogde waarde wordt geschreven. De hoogwaardige bewerking cptrThreads++ wordt namelijk op processorniveau uitgevoerd door meerdere elementaire instructies. De slaapfase van één seconde in stap 2 is er dus alleen om dit risico te minimaliseren.
De resultaten die met 5 threads zijn verkregen, zijn als volgt:
Als we deze resultaten bekijken, zien we duidelijk wat er gebeurt:
- regel 1: een eerste thread leest de teller. Hij vindt 0. Hij stopt 1 s en verliest dus de processor
- regel 2: een tweede thread neemt vervolgens de processor over en leest ook de waarde van de teller. Deze staat nog steeds op 0, aangezien de vorige thread deze nog niet heeft verhoogd. Ook deze stopt 1 s en verliest op zijn beurt de processor.
- regels 1-5: in 1 s hebben de 5 threads allemaal de tijd gehad om langs te komen en de waarde 0 te lezen.
- regels 6-10: wanneer ze één voor één weer actief worden, verhogen ze de waarde 0 die ze hebben gelezen en schrijven ze de waarde 1 in de teller, wat het hoofdprogramma (Main) in regel 11 bevestigt.
Waar komt het probleem vandaan? De tweede thread heeft een verkeerde waarde gelezen omdat de eerste thread was onderbroken voordat hij zijn taak had voltooid, namelijk het bijwerken van de teller in het venster. Dit brengt ons bij het begrip ‘kritieke resource’ en ‘kritieke sectie’ van een programma:
- een kritieke resource is een resource die slechts door één thread tegelijk kan worden bezet. In dit geval is de kritieke resource de teller.
- Een kritieke sectie van een programma is een reeks instructies in de uitvoeringsstroom van een thread waarin deze toegang heeft tot een kritieke resource. We moeten ervoor zorgen dat tijdens deze kritieke sectie alleen deze thread toegang heeft tot de resource.
In ons voorbeeld is de kritieke sectie de code tussen het uitlezen van de teller en het schrijven van de nieuwe waarde:
// meterstand
int valeur = cptrThreads;
// in afwachting
Thread.Sleep(1000);
// teller verhogen
cptrThreads = valeur + 1;
Om deze code uit te voeren, moet een thread de garantie hebben dat hij alleen is. Hij kan worden onderbroken, maar tijdens deze onderbreking mag een andere thread dezezelfde code niet kunnen uitvoeren. Het .NET-platform biedt diverse hulpmiddelen om de exclusieve toegang tot kritieke codegedeelten te waarborgen. We zullen er nu enkele bekijken.
10.5.2. De lock-clausule
Met de lock-clausule kun je een kritiek gedeelte als volgt afbakenen:
obj moet een objectreferentie zijn die zichtbaar is voor alle threads die het kritieke gedeelte uitvoeren. De lock-clausule zorgt ervoor dat slechts één thread tegelijk het kritieke gedeelte uitvoert. Het vorige voorbeeld wordt als volgt herschreven:
using System;
using System.Threading;
namespace Chap8 {
class Program2 {
// klassevariabelen
static int cptrThreads = 0; // threadteller
static object synchro = new object(); // synchronisatieobject
//main
public static void Main(string[] args) {
...
// wachten tot de threads zijn voltooid
Thread.CurrentThread.Name = "Main";
for (int i = nbThreads - 1; i >= 0; i--) {
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
threads[i].Join();
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a été prévenu de la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
}
// teller weergeven
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// verhoogt de threadteller
// er wordt exclusieve toegang tot de teller aangevraagd
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend l'autorisation d'entrer dans la section critique", DateTime.Now, Thread.CurrentThread.Name);
lock (synchro) {
// teller uitlezen
int valeur = cptrThreads;
// opvolging
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
// wachten
Thread.Sleep(1000);
// teller verhogen
cptrThreads = valeur + 1;
// opvolging
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
}
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a quitté la section critique", DateTime.Now, Thread.CurrentThread.Name);
}
}
}
- regel 9: synchro is het object dat de synchronisatie van alle threads mogelijk maakt.
- regels 16-23: de methode [Main] wacht op de threads in de omgekeerde volgorde van hun aanmaak.
- regels 29-40: het kritieke gedeelte van de methode Incrémente is omkaderd door de clausule lock.
De resultaten die met 3 threads zijn verkregen, zijn als volgt:
- thread 0 komt als eerste in de kritieke sectie: regels 1, 2, 6, 8
- de twee andere threads worden geblokkeerd zolang thread 0 zich nog in de kritieke sectie bevindt: regels 3 en 4
- Vervolgens is thread 1 aan de beurt: regels 7, 9, 10
- daarna is thread 2 aan de beurt: regels 11, 12, 13
- regel 14: de Main-thread, die wachtte tot thread 2 klaar was, wordt hiervan op de hoogte gebracht
- regel 15: de Main-thread wacht nu op het einde van thread 1. Deze is al voltooid. De Main-thread wordt hiervan onmiddellijk op de hoogte gebracht, regel 16.
- regels 17-18: hetzelfde proces vindt plaats met thread 0
- regel 19: het aantal threads klopt
10.5.3. De klasse Mutex
Ook met de klasse System.Threading.Mutex kunnen kritieke secties worden afgebakend. Deze verschilt van de lock-clausule wat betreft zichtbaarheid:
- met de lock-clausule kun je threads binnen dezelfde applicatie synchroniseren
- met de klasse Mutex kunnen threads van verschillende applicaties worden gesynchroniseerd.
We zullen de volgende constructor en methoden gebruiken:
maakt een Mutex M aan | |
De thread T1 die de bewerking M.WaitOne() uitvoert, vraagt de eigendom van het synchronisatieobject M aan. Als de Mutex M door geen enkele thread wordt vastgehouden (wat in het begin het geval is), wordt het „toegewezen” aan de thread T1 die erom heeft gevraagd. Als even later een thread T2 dezelfde bewerking uitvoert, wordt deze geblokkeerd. Een Mutex kan namelijk slechts aan één thread toebehoren. De thread wordt gedeblokkeerd zodra de thread T1 de Mutex M vrijgeeft die hij in bezit heeft. Zo kunnen meerdere threads geblokkeerd raken in afwachting van de Mutex M. | |
De thread T1 die de bewerking M.ReleaseMutex() uitvoert, geeft het bezit van de Mutex M op. Wanneer de thread T1 de processor verliest, kan het systeem deze toewijzen aan een van de threads die op de mutex M wachten. Slechts één thread zal deze op zijn beurt krijgen; de andere threads die op M wachten, blijven geblokkeerd |
Een Mutex M beheert de toegang tot een gedeelde bron R. Een thread vraagt de bron R aan via M.WaitOne() en geeft deze terug via M.ReleaseMutex(). Een kritiek gedeelte van de code dat slechts door één thread tegelijk mag worden uitgevoerd, is een gedeelde hulpbron. De uitvoering van het kritieke gedeelte kan als volgt worden gesynchroniseerd:
waarbij M een object Mutex is. Vergeet niet een Mutex vrij te geven dat niet langer nodig is, zodat een andere thread de kritieke sectie kan betreden; anders krijgen de threads die wachten op het nooit vrijgegeven Mutex nooit toegang tot de processor.
Als we wat we zojuist hebben gezien toepassen op het vorige voorbeeld, ziet onze applicatie er als volgt uit:
using System;
using System.Threading;
namespace Chap8 {
class Program3 {
// klassevariabelen
static int cptrThreads = 0; // threadteller
static Mutex synchro = new Mutex(); // synchronisatieobject
//main
public static void Main(string[] args) {
...
}
public static void Incrémente() {
....
synchro.WaitOne();
try {
...
} finally {
...
synchro.ReleaseMutex();
}
}
}
}
- regel 9: het synchronisatieobject voor de threads is nu een Mutex.
- regel 18: begin van de kritieke sectie – er mag slechts één thread deze sectie betreden. We blijven wachten totdat de Mutex synchro vrij is.
- regel 33: omdat een Mutex altijd moet worden vrijgegeven, ongeacht of er een uitzondering optreedt of niet, wordt de kritieke sectie afgehandeld met een try/finally-blok om de Mutex in de finally vrij te geven.
- regel 23: de Mutex wordt vrijgegeven zodra het kritieke gedeelte is doorlopen.
De verkregen resultaten zijn dezelfde als eerder.
10.5.4. De klasse AutoResetEvent
Een AutoResetEvent-object is een barrière die slechts één thread tegelijk doorlaat, net als de twee voorgaande tools lock en Mutex. We construeren een AutoResetEvent-object op de volgende manier:
De booleaanse waarde état geeft aan of de slagboom gesloten (false) of open (true) is. Een thread die de slagboom wil passeren, geeft dit als volgt aan:
- als de barrière open is, gaat de thread erdoor en wordt de barrière achter hem weer gesloten. Als er meerdere threads stonden te wachten, is het zeker dat er slechts één doorgaat.
- Als de barrière gesloten is, wordt de thread geblokkeerd. Een andere thread zal deze openen wanneer het moment daar is. Dit moment is volledig afhankelijk van het te behandelen probleem. De barrière wordt geopend door de bewerking:
Het kan voorkomen dat een thread een barrière wil sluiten. Dit kan hij doen met:
Als we in het vorige voorbeeld het object Mutex vervangen door een object van het type AutoResetEvent, wordt de code als volgt:
using System;
using System.Threading;
namespace Chap8 {
class Program4 {
// klassevariabelen
static int cptrThreads = 0; // threadteller
static EventWaitHandle synchro = new AutoResetEvent(false); // synchronisatieobject
//hoofdprogramma
public static void Main(string[] args) {
....
// de barrière van de kritieke sectie wordt geopend
Console.WriteLine("A {0:hh:mm:ss}, le thread {1} ouvre la barrière de la section critique", DateTime.Now, Thread.CurrentThread.Name);
synchro.Set();
// wachten tot de threads zijn voltooid
...
// teller weergeven
Console.WriteLine("Nombre de threads générés : " + cptrThreads);
}
public static void Incrémente() {
// verhoogt de teller met het aantal threads
// er wordt exclusieve toegang tot de teller aangevraagd
...
synchro.WaitOne();
try {
...
} finally {
// de bron wordt vrijgegeven
...
synchro.Set();
}
}
}
}
- regel 9: de barrière wordt gesloten aangemaakt. Deze wordt geopend door de thread Main op regel 16.
- regel 27: de thread die verantwoordelijk is voor het verhogen van de threadteller vraagt toestemming om de kritieke sectie te betreden. De verschillende threads zullen zich ophopen voor de gesloten barrière. Wanneer de thread Main deze opent, zal een van de wachtende threads doorgang krijgen.
- regel 33: wanneer deze thread zijn werk heeft voltooid, opent hij de barrière weer, zodat een andere thread naar binnen kan.
We krijgen resultaten die vergelijkbaar zijn met de vorige.
10.5.5. De klasse Interlocked
Met de klasse Interlocked kan een groep bewerkingen atomair worden gemaakt. In een groep bewerkingen atomique worden ofwel alle bewerkingen uitgevoerd door de thread die de groep uitvoert, ofwel geen enkele. Er ontstaat geen toestand waarin sommige bewerkingen wel en andere niet zijn uitgevoerd. De synchronisatieobjecten lock, Mutex en AutoResetEvent hebben allemaal tot doel om van atomique een groep bewerkingen te maken. Dit resultaat wordt bereikt ten koste van het blokkeren van threads. De klasse Interlocked maakt het mogelijk om bij eenvoudige maar vrij frequente bewerkingen het blokkeren van threads te voorkomen. De klasse Interlocked biedt de volgende statische methoden:

De methode Increment heeft de volgende signatuur:
Hiermee kan de parameter location met 1 worden verhoogd. De bewerking is gegarandeerd atomique.
Ons programma voor het tellen van threads kan er dan als volgt uitzien:
using System;
using System.Threading;
namespace Chap8 {
class Program5 {
// klassevariabelen
static int cptrThreads = 0; // threadteller
//main
public static void Main(string[] args) {
...
}
public static void Incrémente() {
// de threadteller wordt verhoogd
Interlocked.Increment(ref cptrThreads);
}
}
}
- regel 17: de threadteller wordt op atomaire wijze met 1 verhoogd.
10.6. Gelijktijdige toegang tot meerdere gedeelde bronnen
10.6.1. Een voorbeeld
In onze voorgaande voorbeelden werd één enkele bron gedeeld door de verschillende threads. De situatie kan ingewikkelder worden als er meerdere bronnen zijn die van elkaar afhankelijk zijn. Er kan dan met name een deadlock ontstaan. Deze situatie, ook wel deadlock genoemd, is er een waarin twee threads op elkaar wachten. Laten we de volgende acties bekijken die elkaar in de tijd opvolgen:
- een thread T1 verkrijgt de eigendom van een mutex M1 om toegang te krijgen tot een gedeelde bron R1
- een thread T2 verkrijgt de eigendom van een mutex M2 om toegang te krijgen tot een gedeelde bron R2
- de thread T1 vraagt de mutex M2 aan. Hij wordt geblokkeerd.
- De thread T2 vraagt de mutex M1 aan. Hij wordt geblokkeerd.
Hier wachten de threads T1 en T2 op elkaar. Dit scenario doet zich voor wanneer threads twee gedeelde bronnen nodig hebben: de bron R1, die wordt beheerd door de mutex M1, en de bron R2, die wordt beheerd door de mutex M2. Een mogelijke oplossing is om beide resources tegelijkertijd aan te vragen met behulp van één enkele mutex M. Dit is echter niet altijd mogelijk, bijvoorbeeld als dit leidt tot een langdurige bezetting van een kostbare resource. Een andere oplossing is dat een thread die M1 heeft en M2 niet kan verkrijgen, vervolgens M1 vrijgeeft om een deadlock te voorkomen.
- We hebben een array waarin threads gegevens opslaan (de schrijvers) en andere threads deze lezen (de lezers).
- De schrijvers zijn onderling gelijk, maar sluiten elkaar uit: er kan slechts één schrijver tegelijk zijn gegevens in de array plaatsen.
- De lezers zijn onderling gelijk maar sluiten elkaar uit: er kan slechts één lezer tegelijk de gegevens lezen die in de array zijn geplaatst.
- Een lezer kan de gegevens uit de array alleen lezen wanneer een schrijver er gegevens in heeft geplaatst, en een schrijver kan alleen nieuwe gegevens in de array plaatsen wanneer de gegevens die erin staan door een lezer zijn gelezen.
Er kunnen twee soorten gedeelde bronnen worden onderscheiden:
- de tabel voor schrijven: er mag slechts één schrijver tegelijk toegang hebben.
- de tabel voor lezen: er mag slechts één lezer tegelijk toegang hebben.
en een volgorde voor het gebruik van deze bronnen:
- een lezer moet altijd na een schrijver komen.
- een schrijver moet altijd na een lezer komen, behalve de eerste keer.
De toegang tot deze twee bronnen kan worden geregeld met twee barrières van het type AutoResetEvent:
- de barrière peutEcrire regelt de toegang van schrijvers tot de tabel.
- De slagboom peutLire zal de toegang van de lezers tot het bord controleren.
- De slagboom peutEcrire wordt in eerste instantie in geopende stand gezet, waardoor een eerste schrijver wordt doorgelaten en alle anderen worden tegengehouden.
- De barrière peutLire wordt aanvankelijk gesloten aangemaakt, waardoor alle lezers worden tegengehouden.
- Zodra een schrijver zijn werk heeft voltooid, opent hij de barrière peutLire om een lezer binnen te laten.
- Wanneer een lezer zijn werk heeft voltooid, opent hij de barrière peutEcrire om een schrijver binnen te laten.
Het programma dat deze gebeurtenissynchronisatie illustreert, is als volgt:
using System;
using System.Threading;
namespace Chap8 {
class Program {
// gebruik van lees- en schrijfthreads
// illustreert het gebruik van synchronisatiegebeurtenissen
// klassevariabelen
static int[] data = new int[3]; // een gedeelde bron tussen lees- en schrijfthreads
static Random objRandom = new Random(DateTime.Now.Second); // een generator voor willekeurige getallen
static AutoResetEvent peutLire; // geeft aan dat de inhoud van data kan worden gelezen
static AutoResetEvent peutEcrire; // geeft aan dat de inhoud van data kan worden geschreven
//main
public static void Main(string[] args) {
// het aantal te genereren threads
const int nbThreads = 2;
// initialisatie van de vlaggen
peutLire = new AutoResetEvent(false); // kan nog niet worden gelezen
peutEcrire = new AutoResetEvent(true); // men kan al schrijven
// aanmaken van leesthreads
Thread[] lecteurs = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// aanmaken
lecteurs[i] = new Thread(Lire);
lecteurs[i].Name = "L" + i.ToString();
// starten
lecteurs[i].Start();
}
// aanmaken van schrijv-threads
Thread[] écrivains = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// aanmaken
écrivains[i] = new Thread(Ecrire);
écrivains[i].Name = "E" + i.ToString();
// starten
écrivains[i].Start();
}
//einde hand
Console.WriteLine("Fin de Main...");
}
// de inhoud van de tabel lezen
public static void Lire() {
...
}
// naar de tabel schrijven
public static void Ecrire() {
....
}
}
}
- regel 11: de array data is de gedeelde bron tussen de lezer- en schrijver-threads. Deze wordt gedeeld voor lezen door de lezer-threads en voor schrijven door de schrijver-threads.
- regel 13: het object peutLire dient om de lezer-threads te waarschuwen dat ze de array data mogen lezen. Het wordt op ‘waar’ gezet door de schrijver-thread die de array data heeft gevuld. Het wordt geïnitialiseerd op false, regel 23. Een schrijvende thread moet de array eerst vullen voordat de gebeurtenis peutLire wordt doorgegeven aan vrai.
- regel 14: het object peutEcrire dient om de schrijvende threads te laten weten dat ze in de array data mogen schrijven. Het wordt op ‘waar’ gezet door de lezende thread die de gehele array data heeft verwerkt. Het wordt geïnitialiseerd op true, regel 24. Het array data is namelijk vrij om te schrijven.
- regels 27-34: aanmaken en starten van de lezer-threads
- regels 37-44: aanmaken en starten van de schrijvende threads
De methode Lire die door de lezer-threads wordt uitgevoerd, is als volgt:
public static void Lire() {
// opvolging
Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// we moeten wachten op leesautorisatie
peutLire.WaitOne();
// tabel lezen
for (int i = 0; i < data.Length; i++) {
//1 seconde wachten
Thread.Sleep(1000);
// weergave
Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// schrijven is mogelijk
peutEcrire.Set();
// vervolgen
Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
- regel 5: er wordt gewacht tot een schrijvende thread aangeeft dat de array is gevuld. Zodra dit signaal wordt ontvangen, mag slechts één van de lezende threads die op dit signaal wachten, doorgaan.
- regels 7-12: gebruik van de array data met een Sleep in het midden om de thread te dwingen de processor vrij te geven.
- regel 14: geeft aan de schrijvende threads door dat de array is gelezen en dat deze opnieuw kan worden gevuld.
De methode Ecrire die door de schrijvende threads wordt uitgevoerd, is als volgt:
public static void Ecrire() {
// vervolgen
Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// moet wachten op schrijftoestemming
peutEcrire.WaitOne();
// tabel schrijven
for (int i = 0; i < data.Length; i++) {
//1 seconde wachten
Thread.Sleep(1000);
// weergave
data[i] = objRandom.Next(0, 1000);
Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// lezen is mogelijk
peutLire.Set();
// vervolgen
Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
- regel 5: er wordt gewacht tot een leesthread aangeeft dat de array is gelezen. Zodra dit signaal wordt ontvangen, kan slechts één van de schrijfthreads die op dit signaal wachten, doorgaan.
- regels 7-13: verwerking van de array data met een Sleep in het midden om de thread te dwingen de processor af te staan.
- regel 15: geeft aan de lezer-threads door dat het array is gevuld en dat het opnieuw kan worden gelezen.
De uitvoering levert de volgende resultaten op:
De volgende punten vallen op:
- er is inderdaad slechts één lezer tegelijk, hoewel deze de processor verliest in de kritieke sectie Lire
- er is inderdaad slechts één schrijver tegelijk, hoewel deze de processor verliest in de kritieke sectie Ecrire
- een lezer leest alleen wanneer er iets te lezen valt in de tabel
- een schrijver schrijft alleen wanneer de tabel volledig is gelezen
10.6.2. De klasse Monitor
In het vorige voorbeeld:
- zijn er twee gedeelde bronnen die moeten worden beheerd
- voor een bepaalde resource zijn de threads gelijk.
Wanneer de schrijvende threads geblokkeerd zijn op de instructie peutEcrire.WaitOne, wordt een van hen – willekeurig welke – gedeblokkeerd door de bewerking peutEcrire.Set. Als de voorgaande bewerking de barrière voor een specifieke schrijver moet openen, wordt het ingewikkelder.
We kunnen dit vergelijken met een openbare instelling met loketten, waarbij elk loket gespecialiseerd is. Wanneer de klant arriveert, haalt hij een kaartje bij de kaartjesautomaat voor loket X en gaat vervolgens zitten. Elk kaartje is genummerd en de klanten worden via een luidspreker bij hun nummer opgeroepen. Terwijl hij wacht, doet de klant wat hij wil. Hij kan lezen of een dutje doen. Hij wordt telkens gewekt door de luidspreker die aankondigt dat nr. Y wordt opgeroepen bij loket X. Als hij aan de beurt is, staat de klant op en gaat hij naar loket X; zo niet, dan gaat hij verder met waar hij mee bezig was.
Hier kan men op een vergelijkbare manier te werk gaan. Laten we het voorbeeld van schrijvers nemen:
hun threads zijn geblokkeerd | |
De thread die de array in leesmodus gebruikte, geeft aan de schrijvers door dat de array beschikbaar is. Deze thread of een andere thread heeft de schrijvende thread vastgezet, die de barrière moet passeren. | |
Elke thread controleert of hij de uitverkorene is. Zo ja, dan passeert hij de barrière. Zo niet, dan gaat hij weer in de wachtrij staan. |
Met de klasse Monitor kan dit scenario worden geïmplementeerd.

We beschrijven nu een standaardconstructie (pattern), voorgesteld in hoofdstuk Threading van het boek C# 3.0 waarnaar in de inleiding van dit document wordt verwezen, waarmee barrièreproblemen met toegangsvoorwaarden kunnen worden opgelost.
- Allereerst krijgen de threads die een resource delen (het loket, ...) toegang tot deze resource via een object dat we een token noemen. Om de barrière te openen die naar het loket leidt, is het token nodig, en er is slechts één token. De threads moeten het token dus onderling doorgeven.
- Om naar het loket te gaan, vragen de threads eerst om het token:
Als het token vrij is, wordt het toegewezen aan de thread die de vorige bewerking heeft uitgevoerd; anders wordt de thread in de wachtrij geplaatst totdat het token beschikbaar komt.
- Als de toegang tot het loket op een ongeordende manier verloopt, c.a.d. in het geval dat het niet uitmaakt wie er binnenkomt, volstaat de vorige bewerking. De thread die het token heeft, gaat naar het loket. Als de toegang op een geordende manier verloopt, controleert de thread die het token heeft of hij voldoet aan de voorwaarde om naar het loket te gaan:
Als de thread niet degene is die bij het loket wordt verwacht, geeft hij zijn beurt af door het token terug te geven. Hij gaat over in een geblokkeerde toestand. Hij wordt weer geactiveerd zodra het token weer voor hem beschikbaar komt. Hij controleert dan opnieuw of hij aan de voorwaarde voldoet om naar het loket te gaan. De bewerking Monitor.Wait(token), waarmee het token wordt vrijgegeven, kan alleen worden uitgevoerd als de thread de eigenaar van het token is. Als dat niet het geval is, wordt er een uitzondering gegenereerd.
- De thread die controleert of aan de voorwaarde voor het bezoeken van het loket is voldaan, gaat erheen:
- // werk aan het loket
- ....
Voordat de thread het loket verlaat, moet hij zijn token teruggeven, anders blijven de threads die op het token wachten voor onbepaalde tijd geblokkeerd. Er zijn twee verschillende situaties:
- de eerste situatie is die waarbij de thread die het token heeft, ook degene is die aan de threads die op het token wachten, meldt dat het token vrij is. Dit doet hij op de volgende manier:
Op regel 6 wekt hij de threads die op het token wachten. Dit betekent dat ze in aanmerking komen om het token te ontvangen. Dit betekent niet dat ze het onmiddellijk ontvangen. Op regel 8 wordt het token vrijgegeven. Alle in aanmerking komende threads zullen het token om de beurt ontvangen, op een onvoorspelbare manier. Dit geeft hen de gelegenheid om opnieuw te controleren of ze aan de toegangsvoorwaarde voldoen. De thread die het token heeft vrijgegeven, heeft deze voorwaarde in regel 4 gewijzigd om een nieuwe thread toegang te verlenen. De eerste die aan de voorwaarde voldoet, behoudt het token en gaat op zijn beurt naar de balie.
- De tweede situatie is die waarbij de thread die het token heeft, niet degene is die de threads die op het token wachten, moet laten weten dat het token vrij is. Hij moet het token echter wel vrijgeven, omdat de thread die dit signaal moet verzenden, in het bezit moet zijn van het token. Hij doet dit met de volgende bewerking:
Het token is nu beschikbaar, maar de threads die erop wachten (ze hebben een Wait(token)-bewerking uitgevoerd) worden hier niet van op de hoogte gebracht. Deze taak wordt toevertrouwd aan een andere thread die op een bepaald moment code zal uitvoeren die vergelijkbaar is met het volgende:
Uiteindelijk is de standaardconstructie die wordt voorgesteld in hoofdstuk Threading van het boek C# 3.0 als volgt:
- definieer het toegangstoken voor het loket:
- vraag toegang tot het loket aan:
lock(jeton){
while (! jeNeSuisPasCeluiQuiEstAttendu)
Monitor.Wait(jeton);
}
// overgang naar de balie
...
is gelijk aan
Merk op dat in dit schema het token onmiddellijk wordt vrijgegeven zodra de barrière is gepasseerd. Een andere thread kan dan de toegangsvoorwaarde controleren. De vorige constructie laat dus alle threads toe die de toegangsvoorwaarde controleren. Als dit niet de bedoeling is, kan men het volgende schrijven:
lock(jeton){
while (! jeNeSuisPasCeluiQuiEstAttendu)
Monitor.Wait(jeton);
// overgang naar het loket
...
}
waarbij het token pas wordt vrijgegeven nadat het loket is gepasseerd.
- de voorwaarde voor toegang tot het loket aanpassen en de andere threads hiervan op de hoogte stellen
lock(jeton){
// de voorwaarde voor toegang tot de balie wijzigen
...
// de threads die op het token wachten hiervan op de hoogte stellen
Monitor.PulseAll(jeton);
}
Hierboven kan de toegangsvoorwaarde alleen worden gewijzigd door de thread die het token bezit. Men zou ook kunnen schrijven:
// de toegangsvoorwaarde voor de loket wijzigen
...
// de threads die op het token wachten hiervan op de hoogte stellen
Monitor.PulseAll(jeton);
// het token vrijgeven
Monitor.Exit(jeton);
als de thread het token al heeft.
Met deze informatie kunnen we de lees-/schrijftoepassing herschrijven door een volgorde vast te leggen voor de lezers en schrijvers bij de toegang tot hun respectievelijke poorten. De code is als volgt:
using System;
using System.Threading;
namespace Chap8 {
class Program2 {
// gebruik van lees- en schrijfthreads
// illustreert het gebruik van synchronisatiegebeurtenissen
// klassevariabelen
static int[] data = new int[3]; // een gedeelde bron tussen lees- en schrijfthreads
static Random objRandom = new Random(DateTime.Now.Second); // een generator voor willekeurige getallen
static object peutLire = new object(); // geeft aan dat de inhoud van data kan worden gelezen
static object peutEcrire = new object(); // geeft aan dat de inhoud van data kan worden geschreven
static bool lectureAutorisée = false; // om het lezen van de array toe te staan
static bool écritureAutorisée = false; // om het schrijven in de tabel toe te staan
static string[] ordreLecture; // bepaalt de volgorde van de lezers
static string[] ordreEcriture; // bepaalt de volgorde van de schrijvers
static int lecteurSuivant = 0; // geeft het nummer van de volgende lezer aan
static int écrivainSuivant = 0; // geeft het nummer van de volgende schrijver aan
//hoofd
public static void Main(string[] args) {
// het aantal te genereren threads
const int nbThreads = 5;
// aanmaken van de lezer-threads
Thread[] lecteurs = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// aanmaken
lecteurs[i] = new Thread(Lire);
lecteurs[i].Name = "L" + i.ToString();
// starten
lecteurs[i].Start();
}
// aanmaken van de leesvolgorde
ordreLecture = new string[nbThreads];
for (int i = 0; i < nbThreads; i++) {
ordreLecture[i] = lecteurs[nbThreads - i - 1].Name;
Console.WriteLine("Le lecteur {0} est en position {1}", ordreLecture[i], i);
}
// aanmaken van schrijfthreads
Thread[] écrivains = new Thread[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// aanmaken
écrivains[i] = new Thread(Ecrire);
écrivains[i].Name = "E" + i.ToString();
// start
écrivains[i].Start();
}
// aanmaken van de schrijfvolgorde
ordreEcriture = new string[nbThreads];
for (int i = 0; i < nbThreads; i++) {
ordreEcriture[i] = écrivains[i].Name;
Console.WriteLine("L'écrivain {0} est en position {1}", ordreEcriture[i], i);
}
// schrijftoestemming
lock (peutEcrire) {
écritureAutorisée = true;
Monitor.Pulse(peutEcrire);
}
//einde van de hand
Console.WriteLine("Fin de Main...");
}
// de inhoud van de tabel lezen
public static void Lire() {
...
}
// naar de tabel schrijven
public static void Ecrire() {
...
}
}
}
Toegang tot het leeskanaal is afhankelijk van de volgende elementen:
- regel 13: het token peutLire
- regel 15: de booleaanse waarde lectureAutorisée
- regel 17: de geordende lijst van lezers. De lezers gaan naar het leesloket in de volgorde van deze lijst, die hun namen bevat.
- regel 19: lecteurSuivant geeft het nummer aan van de volgende lezer die naar het loket mag gaan.
De toegang tot het schrijfloket is afhankelijk van de volgende elementen:
- regel 14: het token peutEcrire
- regel 16: de booleaanse waarde écritureAutorisée
- regel 18: de geordende lijst van schrijvers. De schrijvers gaan naar het schrijfloket in de volgorde van deze lijst, die hun namen bevat.
- regel 20: écrivainSuivant geeft het nummer aan van de volgende schrijver die naar het loket mag gaan.
De overige onderdelen van de code zijn als volgt:
- regels 29-36: aanmaken en starten van de lezer-threads. Ze worden allemaal geblokkeerd omdat lezen niet is toegestaan (regel 15).
- regels 39-43: de volgorde waarin ze naar het loket gaan, is omgekeerd aan de volgorde waarin ze zijn aangemaakt.
- regels 46-53: aanmaken en starten van de schrijvers-threads. Deze worden allemaal geblokkeerd omdat schrijven niet is toegestaan (regel 16).
- regels 56-60: de volgorde waarin ze aan de balie komen, is in de volgorde waarin ze zijn aangemaakt.
- regel 64: schrijven wordt toegestaan
- regel 65: de schrijvers worden gewaarschuwd dat er iets is veranderd.
De methode Lire is als volgt:
public static void Lire() {
// opvolging
Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// we moeten wachten op de leestoestemming
lock (peutLire) {
while (!lectureAutorisée || ordreLecture[lecteurSuivant] != Thread.CurrentThread.Name) {
Monitor.Wait(peutLire);
}
// tabel lezen
for (int i = 0; i < data.Length; i++) {
//1 seconde wachten
Thread.Sleep(1000);
// weergave
Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// volgende lezer
lectureAutorisée = false;
lecteurSuivant++;
// de schrijvers worden gewaarschuwd dat ze kunnen schrijven
lock (peutEcrire) {
écritureAutorisée = true;
Monitor.PulseAll(peutEcrire);
}
// vervolg
Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
}
- de volledige toegang tot het loket wordt geregeld door de lock van de regels 5-27. De lezer die het token ontvangt, houdt het gedurende zijn hele bezoek aan het loket bij zich
- regels 6-8: een lezer die het token uit regel 5 heeft opgepakt, laat het los als het lezen niet is toegestaan of als hij nog niet aan de beurt is.
- regels 10-15: bezoek aan het loket (verwerking van de tabel)
- regels 17-18: de thread wijzigt de toegangsvoorwaarden voor het leeskanaal. Merk op dat hij nog steeds het leestoken heeft en dat deze wijzigingen een lezer nog niet toestaan om langs te komen.
- regels 20-23: de thread wijzigt de toegangsvoorwaarden voor het schrijfloket en waarschuwt alle wachtende schrijvers dat er iets is veranderd.
- regel 27: de thread lock wordt beëindigd, het token peutLire wordt vrijgegeven. Een leesthread zou dit token dan kunnen verwerven (regel 5), maar zou niet aan de toegangsvoorwaarde voldoen omdat de booleaanse waarde lectureAutorisée op ‘false’ staat. Bovendien blijven alle threads die wachten op het token peutLire in de wachtrij staan, omdat de bewerking PulseAll(peutLire) nog niet heeft plaatsgevonden.
De methode Ecrire is als volgt:
public static void Ecrire() {
// opvolging
Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
// we moeten wachten op schrijftoestemming
lock (peutEcrire) {
while (!écritureAutorisée || ordreEcriture[écrivainSuivant] != Thread.CurrentThread.Name) {
Monitor.Wait(peutEcrire);
}
// tabel schrijven
for (int i = 0; i < data.Length; i++) {
//1 seconde wachten
Thread.Sleep(1000);
// weergave
data[i] = objRandom.Next(0, 1000);
Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
}
// volgende schrijver
écritureAutorisée = false;
écrivainSuivant++;
// lezers die wachten op het token worden geactiveerd peutLire
lock (peutLire) {
lectureAutorisée = true;
Monitor.PulseAll(peutLire);
}
// vervolg
Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
}
}
- de volledige toegang tot het schrijfloket wordt geregeld door de lock van de regels 5-27. De schrijver die het token ophaalt, behoudt het gedurende zijn hele verblijf aan het loket
- regels 6-8: een schrijver die het token in regel 5 heeft verkregen, geeft het vrij als de schrijfbewerking niet is toegestaan of als hij nog niet aan de beurt is.
- regels 10-16: bezoek aan het loket (verwerking van de tabel)
- regels 18-19: de thread wijzigt de toegangsvoorwaarden voor het schrijfloket. Merk op dat hij nog steeds het schrijftoken heeft en dat deze wijzigingen een schrijver nog niet toestaan om aan de beurt te komen.
- regels 21-24: de thread wijzigt de toegangsvoorwaarden voor het leeskanaal en waarschuwt alle wachtende lezers dat er iets is veranderd.
- regel 27: de thread lock wordt beëindigd, het token peutEcrire wordt vrijgegeven. Een schrijfthread zou dit token dan kunnen verwerven (regel 5), maar zou niet aan de toegangsvoorwaarde voldoen omdat de booleaanse waarde écritureAutorisée op ‘false’ staat. Bovendien blijven alle threads die in de wachtrij staan voor het token peutEcrire in de wachtrij staan in afwachting van een nieuwe bewerking PulseAll(peutEcrire).
Een voorbeeld van de uitvoering is als volgt:
10.7. De threadpools
Tot nu toe hebben we, om threads te beheren:
- we ze aangemaakt met Thread T = new Thread(...)
- en vervolgens uitgevoerd met T.Start()
In het hoofdstuk „Databases” hebben we gezien dat het met bepaalde SGBD mogelijk was om pools van geopende verbindingen te hebben:
- n-verbindingen worden bij het opstarten van de pool geopend
- wanneer een thread een verbinding aanvraagt, krijgt deze een van de open verbindingen uit de pool
- wanneer de thread de verbinding sluit, wordt deze niet gesloten maar teruggegeven aan de pool
Het gebruik van een verbindingspool is transparant op codeniveau. Het voordeel ligt in de verbetering van de prestaties: het openen van een verbinding kost veel. Hier kunnen 10 geopende verbindingen honderden verzoeken afhandelen.
Er bestaat een soortgelijk systeem voor threads:
- min threads worden aangemaakt bij het opstarten van de pool. De waarde van min wordt ingesteld met de methode ThreadPool.SetMinThreads(min1,min2). Een threadpool kan worden gebruikt voor het uitvoeren van blokkerende of niet-blokkerende, zogenaamde asynchrone taken. De eerste parameter min1 bepaalt het aantal blokkerende threads, de tweede parameter min2 het aantal asynchrone threads. De huidige waarden van deze twee parameters kunnen worden opgevraagd via ThreadPool.GetMinThreads(out min1,out min2).
- Als dit aantal niet voldoende is, zal de pool extra threads aanmaken om de verzoeken te verwerken, tot aan de limiet van max threads. De waarde van max wordt ingesteld met de methode ThreadPool.SetMaxThreads(max1,max2). Beide parameters hebben dezelfde betekenis als in de methode SetMinThreads. De huidige waarden van deze twee parameters kunnen worden opgevraagd via ThreadPool.GetMaxThreads(out max1,out max2). Wanneer het aantal max1-threads is bereikt, worden verzoeken om threads voor blokkerende taken in de wachtrij geplaatst totdat er een vrije thread in de pool beschikbaar komt.
Een threadpool biedt diverse voordelen:
- net als bij de verbindingspool wordt tijd bespaard bij het aanmaken van threads: 10 threads kunnen honderden verzoeken afhandelen.
- de applicatie wordt veiliger gemaakt: door een maximum aantal threads vast te stellen, wordt voorkomen dat de applicatie verstikt raakt door te veel verzoeken. Deze worden in de wachtrij geplaatst.
Om een taak aan een thread uit de pool toe te wijzen, gebruikt men een van de twee methoden:
- ThreadPool.QueueWorkItem(WaitCallBack)
- ThreadPool.QueueWorkItem(WaitCallBack,object)
waarbij WaitCallBack elke methode is met de signatuur void WaitCallBack(object). Methode 1 vraagt een thread om de methode WaitCallBack uit te voeren zonder er een parameter aan door te geven. Methode 2 doet hetzelfde, maar geeft daarbij een parameter van het type object door aan de methode WaitCallBack.
Hieronder volgt een programma dat deze concepten illustreert:
using System;
using System.Threading;
namespace Chap8 {
class Program {
public static void Main() {
// huidige thread initialiseren
Thread main = Thread.CurrentThread;
// de thread krijgt een naam
main.Name = "Main";
// er wordt gebruikgemaakt van een threadpool
int min1, min2;
// het minimumaantal blokkerende threads instellen
ThreadPool.GetMinThreads(out min1, out min2);
Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool : {0}", min1);
Console.WriteLine("Nombre minimum de tâches asynchrones dans le pool : {0}", min2);
ThreadPool.SetMinThreads(3, min2);
ThreadPool.GetMinThreads(out min1, out min2);
Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool après changement : {0}", min1);
// het maximale aantal blokkerende threads instellen
int max1, max2;
ThreadPool.GetMaxThreads(out max1, out max2);
Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool : {0}", max1);
Console.WriteLine("Nombre maximum de tâches asynchrones dans le pool : {0}", max2);
ThreadPool.SetMaxThreads(5, max2);
ThreadPool.GetMaxThreads(out max1, out max2);
Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool après changement : {0}", max1);
// er worden 7 threads uitgevoerd
for (int i = 0; i < 7; i++) {
// thread i wordt in een pool gestart
ThreadPool.QueueUserWorkItem(Sleep, new Data2 { Numéro = i.ToString(), Début = DateTime.Now, Durée = i + 10 });
}
// einde van de hoofdprocedure
Console.Write("Tapez [entrée] pour terminer le thread {0} à {1:hh:mm:ss:FF}", main.Name, DateTime.Now);
// wachten
Console.ReadLine();
}
public static void Sleep(object infos) {
// de parameter wordt opgehaald
Data2 data = infos as Data2;
Console.WriteLine("A {2:hh:mm:ss:FF}, le thread n° {0} va dormir pendant {1} seconde(s)", data.Numéro, data.Durée,DateTime.Now);
// status van de pool
int cpt1, cpt2;
ThreadPool.GetAvailableThreads(out cpt1, out cpt2);
Console.WriteLine("Nombre de threads pour tâches bloquantes disponibles dans le pool : {0}", cpt1);
// in slaapstand gedurende Duur seconden
Thread.Sleep(data.Durée * 1000);
// einde uitvoering
data.Fin = DateTime.Now;
Console.WriteLine("A {3:hh:mm:ss:FF}, le thread n° {0} se termine. Il était programmé pour durer {1} seconde(s). Il a duré {2} seconde(s)", data.Numéro, data.Durée, data.Fin - data.Début,DateTime.Now);
}
}
internal class Data2 {
// diverse informatie
public string Numéro { get; set; }
public DateTime Début { get; set; }
public int Durée { get; set; }
public DateTime Fin { get; set; }
}
}
- regel 15-17: het huidige minimumaantal van beide soorten threads in de threadpool wordt opgevraagd en weergegeven
- regel 18: het minimumaantal threads voor blokkerende taken wordt gewijzigd in 2
- regels 19-21: de nieuwe minimumwaarden worden weergegeven
- regels 22-28: we doen hetzelfde om het maximale aantal threads voor blokkerende taken vast te stellen: 5
- regels 30-33: er worden 7 taken uitgevoerd in een pool van 5 threads. 5 taken zouden 1 thread moeten krijgen, de eerste 2 snel omdat er nog 2 threads beschikbaar zijn, de overige 3 met een wachttijd van 0,5 seconde. 2 taken zouden moeten wachten tot er een thread vrijkomt.
- regel 32: de taken voeren de methode Sleep uit de regels 40-54 uit door er een parameter van het type Data2 aan door te geven, gedefinieerd in de regels 56-62.
- regel 40: de methode Sleep die door de taken wordt uitgevoerd
- regel 42: de parameter die aan de methode Sleep is doorgegeven, wordt opgehaald.
- regel 43: de taak identificeert zich op de console
- regels 45-47: het aantal momenteel beschikbare threads wordt weergegeven. We willen zien hoe dit aantal verandert.
- regel 49: de taak stopt enkele seconden (blokkerende taak).
- regel 52: wanneer de taak weer actief wordt, wordt er wat informatie over de taak weergegeven.
De verkregen resultaten zijn als volgt.
Voor de aantallen min en max threads in de pool:
Voor de uitvoering van de 7 threads:
- regels 1-6: de eerste 3 taken worden om beurten uitgevoerd. Ze vinden onmiddellijk 1 beschikbare thread (MinThreads=3) en gaan vervolgens in slaapstand.
- regels 7-9: voor taken 3 en 4 duurt het iets langer. Voor elk van deze taken was er geen vrije thread. Er moest er een worden aangemaakt. Dit mechanisme is mogelijk tot 5 (MaxThreads=5).
- regel 10: er zijn geen threads meer beschikbaar: taken 5 en 6 zullen moeten wachten.
- regels 11-12: taak 0 is voltooid. Taak 5 neemt de thread over.
- regels 13-14: taak 1 is voltooid. Taak 6 neemt de thread over.
- regels 17-21: de taken worden een voor een voltooid.
10.8. De klasse BackgroundWorker
10.8.1. Voorbeeld 1
De klasse BackgroundWorker behoort tot de naamruimte [System.ComponentModel]. Ze wordt gebruikt als een thread, maar heeft enkele bijzonderheden die haar in bepaalde gevallen interessanter kunnen maken dan de klasse [Thread]:
- ze genereert de volgende gebeurtenissen:
- DoWork: een thread heeft gevraagd om de uitvoering van BackgroundWorker
- ProgressChanged: het object BackgroundWorker heeft de methode ReportProgress uitgevoerd. Deze methode dient om een uitvoeringspercentage weer te geven.
- RunWorkerCompleted: het object BackgroundWorker heeft zijn taak voltooid. Dit kan op normale wijze zijn gebeurd, of door annulering of een uitzondering.
Deze gebeurtenissen maken BackgroundWorker nuttig in grafische interfaces: een langdurige taak wordt toevertrouwd aan een BackgroundWorker, dat via de gebeurtenis ProgressChanged verslag kan doen van de voortgang en via de gebeurtenis RunWorkerCompleted van de voltooiing. Het werk dat door de BackgroundWorker moet worden uitgevoerd, wordt gedaan via een methode die aan de gebeurtenis DoWork is gekoppeld.
- Het is mogelijk om de taak te annuleren. In een grafische interface kan een langdurige taak zo door de gebruiker worden geannuleerd.
- De objecten BackgroundWorker behoren tot een pool en worden naar behoefte hergebruikt. Een applicatie die een BackgroundWorker-object nodig heeft, haalt dit op uit de pool, die een reeds bestaande maar ongebruikte thread toewijst. Door threads op deze manier te hergebruiken in plaats van telkens een nieuwe thread aan te maken, worden de prestaties verbeterd.
We gebruiken deze tool in de vorige applicatie in het geval dat de toegang tot de balie ongecontroleerd is:
using System;
using System.Threading;
using System.ComponentModel;
namespace Chap8 {
class Program2 {
// gebruik van lees- en schrijfthreads
// illustreert het gelijktijdige gebruik van gedeelde bronnen en synchronisatie
// klassevariabelen
const int nbThreads = 2; // totaal aantal threads
static int nbLecteursTerminés = 0; // aantal voltooide threads
static int[] data = new int[5]; // tabel die wordt gedeeld door lees- en schrijfthreads
static object appli; // synchroniseert de toegang tot het aantal voltooide threads
static Random objRandom = new Random(DateTime.Now.Second); // een generator voor willekeurige getallen
static AutoResetEvent peutLire; // geeft aan dat de inhoud van de array kan worden gelezen
static AutoResetEvent peutEcrire; // geeft aan dat er in de array geschreven kan worden
static AutoResetEvent finLecteurs; // geeft aan dat de lezers zijn voltooid
//main
public static void Main(string[] args) {
// de thread krijgt een naam
Thread.CurrentThread.Name = "Main";
// initialisatie van de vlaggen
peutLire = new AutoResetEvent(false); // er kan nog niet worden gelezen
peutEcrire = new AutoResetEvent(true); // schrijven is al mogelijk
finLecteurs = new AutoResetEvent(false); // app nog niet voltooid
// synchroniseert de toegang tot de teller van voltooide threads
appli = new object();
// aanmaken van lezer-threads
MyBackgroundWorker[] lecteurs = new MyBackgroundWorker[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// aanmaken
lecteurs[i] = new MyBackgroundWorker();
lecteurs[i].Numéro = "L" + i;
lecteurs[i].DoWork += Lire;
lecteurs[i].RunWorkerCompleted += EndLecteur;
// starten
lecteurs[i].RunWorkerAsync();
}
// aanmaken van schrijvthreads
MyBackgroundWorker[] écrivains = new MyBackgroundWorker[nbThreads];
for (int i = 0; i < nbThreads; i++) {
// aanmaken
écrivains[i] = new MyBackgroundWorker();
écrivains[i].Numéro = "E" + i;
écrivains[i].DoWork += Ecrire;
// starten
écrivains[i].RunWorkerAsync();
}
// wachten tot alle threads zijn voltooid
finLecteurs.WaitOne();
//einde van de hand
Console.WriteLine("Fin de Main...");
}
public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
...
}
// de inhoud van de array lezen
public static void Lire(object sender, DoWorkEventArgs infos) {
...
}
// naar de tabel schrijven
public static void Ecrire(object sender, DoWorkEventArgs infos) {
...
}
}
// thread
internal class MyBackgroundWorker : BackgroundWorker {
// diverse informatie
public string Numéro { get; set; }
}
}
We geven alleen de wijzigingen weer:
- de klasse Thread is vervangen door de klasse MyBackgroundWorker in de regels 79-82. De klasse BackgroundWorker is afgeleid om de thread een nummer te geven. We hadden het ook anders kunnen aanpakken door een object door te geven aan de methode RunWorkerAsync in de regels 43 en 54, waarbij dit object het nummer van de thread bevat.
- regel 58: de methode Main wordt beëindigd nadat alle lezer-threads hun werk hebben voltooid. Hiervoor telt de teller nbLecteursTerminés in regel 12 het aantal lezer-threads dat zijn werk heeft voltooid. Deze teller wordt verhoogd door de methode EndLecteur in de regels 63-65, die wordt uitgevoerd telkens wanneer een lezer-thread wordt beëindigd. Het is deze procedure die de gebeurtenis AutoResetEvent finLecteurs in regel 18 aanstuurt, waarop de methode Main. in regel 59 synchroniseert
- regel 16: omdat meerdere lezer-threads tegelijkertijd de teller nbLecteursTerminés zouden kunnen willen verhogen, wordt exclusieve toegang hiertoe gewaarborgd door het synchronisatieobject appli. Dit scenario is onwaarschijnlijk, maar theoretisch mogelijk.
- regels 35-44: aanmaken van de lezer-threads
- regel 38: aanmaken van de thread van het type MyBackgroundWorker
- regel 39: er wordt een nummer aan toegekend
- regel 40: de uit te voeren methode Lire wordt eraan toegewezen
- regel 41: de methode EndLecteur wordt uitgevoerd nadat de thread is beëindigd
- regel 43: de thread wordt gestart
- regels 47-55: aanmaken van de schrijvende threads
- regel 50: aanmaken van de thread van het type MyBackgroundWorker
- regel 51: er wordt een nummer aan toegewezen
- regel 52: de uit te voeren methode Ecrire wordt eraan toegewezen
- regel 54: de thread wordt gestart
De methoden Lire en Ecrire blijven ongewijzigd. De methode EndLecteur wordt aan het einde van elke lezer-thread uitgevoerd. De code ervan is als volgt:
public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
// verhoging van het aantal voltooide lezers
lock (appli) {
nbLecteursTerminés++;
if (nbLecteursTerminés == nbThreads)
finLecteurs.Set();
}
}
De methode EndLecteur heeft als taak de methode Main te melden dat alle lezers hun werk hebben voltooid.
- regel 4: de teller nbLecteursTerminés wordt verhoogd.
- regels 5-6: als alle lezers hun werk hebben gedaan, wordt de gebeurtenis finLecteurs op ‘waar’ gezet om de methode Main te waarschuwen, die op deze gebeurtenis wacht.
- Omdat de procedure EndLecteur door meerdere threads wordt uitgevoerd, wordt het voorgaande kritieke gedeelte beschermd door de clausule lock in regel 3.
De uitvoering levert resultaten op die vergelijkbaar zijn met die van de versie die gebruikmaakt van threads.
10.8.2. Voorbeeld 2
De volgende code illustreert andere aspecten van de klasse BackgroundWorker:
- de mogelijkheid om de taak te annuleren
- het doorgeven van een uitzondering die in de taak is gegenereerd
- het doorgeven van een I/O-parameter aan de taak
using System;
using System.Threading;
using System.ComponentModel;
namespace Chap8 {
class Program3 {
// threads
static BackgroundWorker[] tâches = new BackgroundWorker[5];
public static void Main() {
// huidige thread initialiseren
Thread main = Thread.CurrentThread;
// een naam toekennen aan de thread
main.Name = "Main";
// threads aanmaken
for (int i = 0; i < tâches.Length; i++) {
// thread nr. i aanmaken
tâches[i] = new BackgroundWorker();
// de thread wordt geïnitialiseerd
tâches[i].DoWork += Sleep;
tâches[i].RunWorkerCompleted += End;
tâches[i].WorkerSupportsCancellation = true;
// de thread wordt gestart
tâches[i].RunWorkerAsync(new Data { Numéro = i, Début = DateTime.Now, Durée = i + 1 });
}
// de laatste thread wordt geannuleerd
tâches[4].CancelAsync();
// einde van de hand
Console.WriteLine("Fin du thread {0}, tapez [entrée] pour terminer...", main.Name);
Console.ReadLine();
return;
}
public static void Sleep(object sender, DoWorkEventArgs infos) {
...
}
public static void End(object sender, RunWorkerCompletedEventArgs infos) {
...
}
internal class Data {
// diverse informatie
public int Numéro { get; set; }
public DateTime Début { get; set; }
public int Durée { get; set; }
public DateTime Fin { get; set; }
}
}
}
- regel 9: de array van BackgroundWorker
- regels 18-27: het aanmaken van threads
- regel 20: het aanmaken van de thread
- regel 22: de thread voert de methode Sleep uit van de regels 39-41
- regel 23: de methode End uit de regels 43-45 wordt aan het einde van de thread uitgevoerd
- regel 24: de thread kan worden geannuleerd
- regel 26: de thread wordt gestart met een parameter van het type [Data], gedefinieerd in de regels 49-52. Dit object heeft de volgende velden:
- Numéro (invoer): threadnummer
- Début (invoer): starttijd van de thread
- Durée (invoer): uitvoeringsduur van Sleep
- Fin (uitvoer): einde van de uitvoering van de thread
- regel 29: thread nr. 4 wordt geannuleerd
Alle threads voeren de volgende methode Sleep uit:
public static void Sleep(object sender, DoWorkEventArgs infos) {
// de parameter 'infos' wordt verwerkt
Data data = (Data)infos.Argument;
// uitzondering voor taak nr. 3
if (data.Numéro == 3) {
throw new Exception("test....");
}
// in slaapstand gedurende 'Duur' seconden met een onderbreking om de 'ttes' seconden
for (int i = 1; i <= data.Durée && !tâches[data.Numéro].CancellationPending; i++) {
// 1 seconde wachten
Thread.Sleep(1000);
}
// einde van de uitvoering
data.Fin = DateTime.Now;
// het resultaat wordt geïnitialiseerd
infos.Result = data;
infos.Cancel = tâches[data.Numéro].CancellationPending;
}
- regel 1: de methode Sleep heeft de standaardsignatuur van gebeurtenishandlers. Deze methode ontvangt twee parameters:
- sender: de gebeurtenisverzoeker, in dit geval de BackgroundWorker die de methode uitvoert
- infos: van het type DoWorkEventArgs, die informatie geeft over de gebeurtenis DoWork. Deze parameter dient zowel om informatie door te geven aan de thread als om de resultaten ervan op te halen.
- regel 3: de parameter die aan de methode RunWorkerAsync van de taak is doorgegeven, is terug te vinden in de eigenschap infos.Argument.
- regels 5-7: er wordt een uitzondering gegenereerd voor taak nr. 3
- regels 9-12: de thread „slaapt“ Durée seconden in stappen van één seconde om de annulatietest van regel 9 mogelijk te maken. Dit simuleert een langdurige taak waarbij de thread regelmatig zou controleren of er een annulatieverzoek is. Om aan te geven dat de taak is geannuleerd, moet de thread de eigenschap infos.Cancel op ‘waar’ zetten (regel 17).
- regel 16: de thread kan een resultaat teruggeven aan de thread die hem heeft gestart. Hij plaatst dit resultaat in infos.Result.
Zodra ze klaar zijn, voeren de threads de volgende methode End uit:
public static void End(object sender, RunWorkerCompletedEventArgs infos) {
// de parameter 'infos' wordt gebruikt om het resultaat van de uitvoering weer te geven
// uitzondering?
if (infos.Error != null) {
Console.WriteLine("Le thread {1} a rencontré l'erreur suivante : {0}", infos.Error.Message, sender);
} else
if (!infos.Cancelled) {
Data data = (Data)infos.Result;
Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
data.Numéro, data.Début, data.Durée, data.Fin, (data.Fin - data.Début));
} else {
Console.WriteLine("Thread {0} annulé", sender);
}
}
- regel 1: de methode End heeft de standaardsignatuur van gebeurtenishandlers. Ze ontvangt twee parameters:
- sender: de verzender van de gebeurtenis, in dit geval de BackgroundWorker die de methode
- infos: van het type RunWorkerCompletedEventArgs, die informatie geeft over de gebeurtenis RunWorkerCompleted.
- regel 4: het veld infos.Error van het type Exception wordt alleen ingevuld als er een uitzondering is opgetreden.
- regel 7: het veld infos.Cancelled van het type booleaanse waarde met de waarde true als de thread is geannuleerd.
- regel 8: als er geen uitzondering of afbreking heeft plaatsgevonden, dan is infos.Result het resultaat van de uitgevoerde thread. Als dit resultaat wordt gebruikt terwijl de thread is afgebroken of een uitzondering heeft gegenereerd, leidt dit tot een uitzondering. In regels 5 en 13 kunnen we dus het nummer van de afgebroken thread of de thread die een uitzondering heeft gegenereerd niet weergeven, omdat dit nummer in infos.Result staat. Dit probleem kan worden omzeild door de klasse BackgroundWorker te afleiden om daarin de informatie op te nemen die moet worden uitgewisseld tussen de aanroepende thread en de aangeroepen thread, zoals in het vorige voorbeeld is gedaan. We gebruiken dan het argument sender, dat staat voor BackgroundWorker, in plaats van het argument infos.
De uitvoerresultaten zijn als volgt:
10.9. Lokale gegevens voor een thread
10.9.1. Het principe
Laten we eens kijken naar een applicatie met drie lagen:
![]() |
Stel dat de applicatie voor meerdere gebruikers is bedoeld, bijvoorbeeld een webapplicatie. Elke gebruiker wordt bediend door een eigen thread. De levenscyclus van de thread is als volgt:
- de thread wordt aangemaakt of opgevraagd uit een threadpool om aan een verzoek van een gebruiker te voldoen
- als dit verzoek gegevens vereist, voert de thread een methode uit van de laag [ui], die op haar beurt een methode aanroept van de laag [metier], die op haar beurt weer een methode aanroept van de laag [dao].
- De thread geeft het antwoord terug aan de gebruiker. Vervolgens verdwijnt hij of wordt hij teruggeplaatst in een threadpool.
Bij bewerking 2 kan het interessant zijn dat de thread over eigen gegevens beschikt, c.a.d, die niet met andere threads worden gedeeld. Deze gegevens zouden bijvoorbeeld kunnen behoren tot de specifieke gebruiker die door de thread wordt bediend. Deze gegevens zouden dan in de verschillende lagen [ui, metier, dao] kunnen worden gebruikt.
De klasse Thread maakt dit scenario mogelijk dankzij een soort privéwoordenboek waarin de sleutels van het type LocalDataStoreSlot zijn:
maakt een vermelding aan in het privéwoordenboek van de thread voor de sleutel name. | |
koppelt de waarde data aan de sleutel name in het privéwoordenboek van de thread | |
haalt de waarde op die is gekoppeld aan de sleutel name uit het privéwoordenboek van de thread |
Een voorbeeld van het gebruik zou als volgt kunnen zijn:
- om een paar (clé,valeur) te maken dat is gekoppeld aan de huidige thread:
- om de waarde op te halen die is gekoppeld aan clé:
10.9.2. Toepassing van het principe
Laten we de volgende drielaagse applicatie eens bekijken:
![]() |
Stel dat de laag [dao] een artikelstam beheert en dat de interface ervan aanvankelijk als volgt is:
using System.Collections.Generic;
namespace Chap8 {
public interface IDao {
int InsertArticle(Article article);
List<Article> GetAllArticles();
void DeleteAllArticles();
}
}
- regel 5: om een artikel in de database in te voeren
- regel 6: om alle artikelen uit de database op te halen
- regel 7: om alle artikelen uit de database te verwijderen
Later ontstaat de behoefte aan een methode om een tabel met artikelen via een transactie in te voeren, omdat men op een ‘alles of niets’-basis wil werken: ofwel worden alle artikelen ingevoerd, ofwel geen enkel artikel. De interface kan dan worden aangepast om aan deze nieuwe behoefte tegemoet te komen:
using System.Collections.Generic;
namespace Chap8 {
public interface IDao {
int InsertArticle(Article article);
void insertArticles(Article[] articles);
List<Article> GetAllArticles();
void DeleteAllArticles();
}
}
- regel 6: om een tabel met artikelen aan de database toe te voegen
Later ontstaat voor een andere toepassing de behoefte om een lijst met artikelen te verwijderen die in een lijst is opgeslagen, nog steeds via een transactie. We zien dat de laag [dao] zal moeten worden uitgebreid om aan verschillende bedrijfsbehoeften te voldoen. We kunnen een andere aanpak kiezen:
- alleen de basisbewerkingen InsertArticle, DeleteArticle, UpdateArticle, SelectArticle en SelectArticles
- de bewerkingen voor het gelijktijdig bijwerken van meerdere artikelen naar de laag [métier] verplaatsen. Deze zouden gebruikmaken van de elementaire bewerkingen van de laag [dao].
Het voordeel van deze oplossing is dat dezelfde laag [dao] zonder aanpassingen kan worden gebruikt met verschillende lagen [metier]. Dit brengt echter een uitdaging met zich mee bij het beheer van de transactie die updates omvat die op atomische wijze in de database moeten worden uitgevoerd:
- de transactie moet worden geïnitieerd door de laag [metier] voordat deze de methoden van de laag [dao] aanroept
- de methoden van de laag [dao] moeten op de hoogte zijn van het bestaan van de transactie om eraan deel te kunnen nemen indien deze bestaat
- de transactie moet worden beëindigd door de laag [métier].
Om ervoor te zorgen dat de methoden van de laag [dao] weten of er een transactie gaande is, zou men de transactie als parameter kunnen toevoegen aan elke methode van de laag [dao]. Deze parameter zal dan in de signatuur van de methoden van de interface verschijnen, waardoor deze aan een specifieke gegevensbron wordt gekoppeld: de database. De lokale gegevens van de thread bieden ons een elegantere oplossing: de laag [métier] plaatst de transactie in de lokale gegevens van de thread en daar haalt de laag [dao] deze op. De methodesignatuur van de laag [dao] hoeft dan niet te worden gewijzigd.
We implementeren deze oplossing met het volgende Visual Studio-project:
![]() |
![]() |
- in [1]: de oplossing in zijn geheel
- in [2]: de gebruikte referenties. Aangezien de database [4] een SQL Server Compact-database is, is de referentie [System.Data.SqlServerCe] vereist.
- in [3]: de verschillende lagen van de applicatie.
De database [4] is de SQL Server Compact-database die al in het vorige hoofdstuk werd gebruikt, met name in paragraaf 9.3.1.
![]() |
De klasse Artikel
Een rij uit de vorige tabel [articles] is ingekapseld in een object van het type Article:
namespace Chap8 {
public class Article {
// eigenschappen
public int Id { get; set; }
public string Nom { get; set; }
public decimal Prix { get; set; }
public int StockActuel { get; set; }
public int StockMinimum { get; set; }
// constructors
public Article() {
}
public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
Id = id;
Nom = nom;
Prix = prix;
StockActuel = stockActuel;
StockMinimum = stockMinimum;
}
// identiteit
public override string ToString() {
return string.Format("[{0},{1},{2},{3},{4}]", Id, Nom, Prix, StockActuel, StockMinimum);
}
}
}
Interface van de laag [dao]
De interface IDao van de laag [dao] ziet er als volgt uit:
using System.Collections.Generic;
namespace Chap8 {
public interface IDao {
int InsertArticle(Article article);
List<Article> GetAllArticles();
void DeleteAllArticles();
}
}
- regel 5: om een artikel in de tabel [articles] in te voegen
- regel 6: om alle regels van de tabel [articles] in een objectenlijst Article te plaatsen
- regel 7: om alle regels uit de tabel [articles] te verwijderen
Interface van de laag [metier]
De interface IMetier van de laag [metier] ziet er als volgt uit:
using System.Collections.Generic;
namespace Chap8 {
interface IMetier {
void InsertArticlesInTransaction(Article[] articles);
void InsertArticlesOutOfTransaction(Article[] articles);
List<Article> GetAllArticles();
void DeleteAllArticles();
}
}
- regel 5: om binnen een transactie een reeks artikelen in te voegen
- regel 6: idem, maar zonder transactie
- regel 7: om de lijst met alle artikelen op te vragen
- regel 8: om alle artikelen te verwijderen
Implementatie van de laag [metier]
De bedrijfsimplementatie van de interface IMetier ziet er als volgt uit:
using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;
namespace Chap8 {
public class Metier : IMetier {
// laag [dao]
public IDao Dao { get; set; }
// verbindingsreeks
public string ConnectionString { get; set; }
// een artikeltabel invoegen binnen een transactie
public void InsertArticlesInTransaction(Article[] articles) {
// de verbinding met de database wordt tot stand gebracht
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// verbinding openen
connexion.Open();
// transactie
SqlCeTransaction transaction = null;
try {
// transactie starten
transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
// de transactie wordt in de thread vastgelegd
Thread.SetData(Thread.GetNamedDataSlot("transaction"), transaction);
// artikelen invoegen
foreach (Article article in articles) {
Dao.InsertArticle(article);
}
// de transactie wordt bevestigd
transaction.Commit();
} catch {
// de transactie wordt teruggedraaid
if (transaction != null)
transaction.Rollback();
}
}
}
// een artikeltabel invoeren zonder transactie
public void InsertArticlesOutOfTransaction(Article[] articles) {
// artikelen invoeren
foreach (Article article in articles) {
Dao.InsertArticle(article);
}
}
// artikelenlijst
public List<Article> GetAllArticles() {
return Dao.GetAllArticles();
}
// alle artikelen verwijderen
public void DeleteAllArticles() {
Dao.DeleteAllArticles();
}
}
}
De klasse heeft de volgende eigenschappen:
- regel 9: een verwijzing naar de laag [dao]
- regel 11: de verbindingsstring waarmee verbinding kan worden gemaakt met de artikeldatabase
We bespreken alleen de methode InsertArticlesInTransaction, omdat alleen deze moeilijkheden oplevert:
- regel 16: er wordt een verbinding met de database tot stand gebracht
- regel 18: de verbinding wordt geopend
- regel 23: er wordt een transactie aangemaakt
- regel 25: deze wordt opgeslagen in de lokale gegevens van de thread, gekoppeld aan de sleutel "transaction"
- regels 27-29: de methode voor het invoegen van afzonderlijke records uit de laag [dao] wordt voor elk in te voegen artikel aangeroepen
- regels 21 en 32: het invoeren van de gehele tabel wordt gecontroleerd door een try/catch-blok
- regel 31: als we hier terechtkomen, betekent dit dat er geen uitzondering is opgetreden. We bevestigen dan de transactie.
- regels 34-35: er is een uitzondering opgetreden, de transactie wordt ongedaan gemaakt
- regel 37: we verlaten de clausule using. De verbinding die in regel 18 is geopend, wordt automatisch gesloten.
Implementatie van de laag [dao]
De DAO-implementatie van de interface IDao ziet er als volgt uit:
using System.Collections.Generic;
using System.Data;
using System.Data.SqlServerCe;
using System.Threading;
namespace Chap8 {
public class Dao : IDao {
// verbindingsreeks
public string ConnectionString { get; set; }
// query's
public string InsertText { get; set; }
public string DeleteAllText { get; set; }
public string GetAllText { get; set; }
// implementatie interface
// artikel toevoegen
public int InsertArticle(Article article) {
// is er een transactie bezig?
SqlCeTransaction transaction = Thread.GetData(Thread.GetNamedDataSlot("transaction")) as SqlCeTransaction;
// verbinding ophalen of aanmaken
SqlCeConnection connexion = null;
if (transaction != null) {
// verbinding ophalen
connexion = transaction.Connection as SqlCeConnection;
} else {
// de verbinding aanmaken
connexion = new SqlCeConnection(ConnectionString);
connexion.Open();
}
try {
// invoercommando voorbereiden
SqlCeCommand sqlCommand = new SqlCeCommand();
sqlCommand.Transaction = transaction;
sqlCommand.Connection = connexion;
sqlCommand.CommandText = InsertText;
sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
sqlCommand.Parameters["@nom"].Value = article.Nom;
sqlCommand.Parameters["@prix"].Value = article.Prix;
sqlCommand.Parameters["@sa"].Value = article.StockActuel;
sqlCommand.Parameters["@sm"].Value = article.StockMinimum;
// uitvoeren
return sqlCommand.ExecuteNonQuery();
} finally {
// als we niet in een transactie zaten, sluiten we de verbinding
if (transaction == null) {
connexion.Close();
}
}
}
// artikellijst
public List<Article> GetAllArticles() {
...
}
// artikelen verwijderen
public void DeleteAllArticles() {
...
}
}
}
De klasse heeft de volgende eigenschappen:
- regel 9: de verbindingsstring waarmee verbinding kan worden gemaakt met de artikeldatabase
- regel 11: de opdracht SQL om een artikel in te voegen
- regel 12: de opdracht SQL om alle artikelen te verwijderen
- regel 13: de opdracht SQL om alle artikelen op te halen
Deze eigenschappen worden geïnitialiseerd op basis van het volgende configuratiebestand [App.config]:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
</connectionStrings>
<appSettings>
<add key="insertText" value="insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"/>
<add key="getAllText" value="select id,nom,prix,stockactuel,stockminimum from articles"/>
<add key="deleteAllText" value="delete from articles"/>
</appSettings>
</configuration>
We lichten de methode InsertArticle toe:
- regel 20: we halen de eventuele transactie op die de laag [metier] in de thread heeft geplaatst
- regels 23-25: als de transactie aanwezig is, halen we de verbinding op waaraan deze is gekoppeld.
- regels 26-30: anders wordt er een nieuwe verbinding aangemaakt en geopend.
- regels 33-44: de invoeropdracht wordt voorbereid. Deze wordt geconfigureerd (zie regel g van App.config).
- regel 33: het object Command wordt aangemaakt.
- regel 34: deze is gekoppeld aan de huidige transactie. Als deze niet bestaat (transactie=null), komt dit neer op het uitvoeren van de opdracht SQL zonder expliciete transactie. We wijzen erop dat er in dat geval toch sprake is van een impliciete transactie. Bij SQL Server CE staat deze impliciete transactie standaard in de modus autocommit: de opdracht SQL is na uitvoering committé.
- regel 35: het object Command wordt gekoppeld aan de huidige verbinding
- regel 36: de uit te voeren tekst SQl wordt vastgelegd. Dit is de geparametriseerde query van regel g van App.config.
- regels 37-44: de 4 parameters van de query worden geïnitialiseerd
- regel 46: de query wordt uitgevoerd.
- regels 49-51: houd er rekening mee dat, als er geen transactie was, er een nieuwe verbinding met de database is geopend (regels 26-30). In dat geval moet deze worden gesloten. Als er wel een transactie was, mag de verbinding niet worden gesloten, omdat deze wordt beheerd door de laag [metier].
De twee andere methoden sluiten aan bij wat we in het hoofdstuk „Databases” hebben besproken:
// artikellijst
public List<Article> GetAllArticles() {
// artikellijst – aanvankelijk leeg
List<Article> articles = new List<Article>();
// verbinding verwerken
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// verbinding openen
connexion.Open();
// voert sqlCommand uit met een SELECT-query
SqlCeCommand sqlCommand = new SqlCeCommand(GetAllText, connexion);
using (SqlCeDataReader reader = sqlCommand.ExecuteReader()) {
// verwerking van het resultaat
while (reader.Read()) {
// verwerking van de huidige rij
articles.Add(new Article(reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2), reader.GetInt32(3), reader.GetInt32(4)));
}
}
}
// het resultaat wordt weergegeven
return articles;
}
// verwijdering van artikelen
public void DeleteAllArticles() {
using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
// verbinding openen
connexion.Open();
// sqlCommand uitvoeren met updateverzoek
new SqlCeCommand(DeleteAllText, connexion).ExecuteNonQuery();
}
}
De testapplicatie [console]
De testtoepassing [console] ziet er als volgt uit:
using System;
using System.Configuration;
namespace Chap8 {
class Program {
static void Main(string[] args) {
// verwerking van het configuratiebestand
string connectionString = null;
string insertText;
string getAllText;
string deleteAllText;
try {
// verbindingsreeks
connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
// overige parameters
insertText = ConfigurationManager.AppSettings["insertText"];
getAllText = ConfigurationManager.AppSettings["getAllText"];
deleteAllText = ConfigurationManager.AppSettings["deleteAllText"];
} catch (Exception e) {
Console.WriteLine("Erreur de configuration : {0}", e.Message);
return;
}
// laag aanmaken [dao]
Dao dao = new Dao();
dao.ConnectionString = connectionString;
dao.DeleteAllText = deleteAllText;
dao.GetAllText = getAllText;
dao.InsertText = insertText;
// laag aanmaken [métier]
Metier metier = new Metier();
metier.Dao = dao;
metier.ConnectionString = connectionString;
// er wordt een artikeltabel aangemaakt
Article[] articles = new Article[2];
for (int i = 0; i < articles.Length; i++) {
articles[i] = new Article(0, "article", 100, 10, 1);
}
// alle artikelen worden verwijderd
Console.WriteLine("Suppression de tous les articles...");
metier.DeleteAllArticles();
// de tabel wordt buiten de transactie om ingevoegd
Console.WriteLine("Insertion des articles hors transaction...");
try {
metier.InsertArticlesOutOfTransaction(articles);
} catch (Exception e){
Console.WriteLine("Exception : {0}", e.Message);
}
// de artikelen weergeven
Console.WriteLine("Liste des articles");
AfficheArticles(metier);
// alle artikelen verwijderen
Console.WriteLine("Suppression de tous les articles...");
metier.DeleteAllArticles();
// de tabel wordt in een transactie ingevoegd
Console.WriteLine("Insertion des articles dans une transaction...");
metier.InsertArticlesInTransaction(articles);
// de artikelen weergeven
Console.WriteLine("Liste des articles");
AfficheArticles(metier);
}
private static void AfficheArticles(IMetier metier) {
// de artikelen weergeven
foreach(Article article in metier.GetAllArticles()){
Console.WriteLine(article);
}
}
}
}
- regels 12-22: het bestand [App.config] wordt gebruikt.
- regels 24-28: de laag [dao] wordt geïnstantieerd en geïnitialiseerd
- regels 30-32: hetzelfde gebeurt voor de laag [metier]
- regels 34-37: er wordt een tabel met 2 artikelen met dezelfde naam aangemaakt. De tabel [articles] in de database SQL op de server [dbarticles.sdf] heeft een uniekheidsbeperking op de naam. Het invoegen van het tweede artikel wordt daarom geweigerd. Als de tabel buiten een transactie wordt ingevoegd, wordt het eerste artikel eerst ingevoegd en blijft het vervolgens staan. Als de tabel binnen een transactie wordt ingevoegd, wordt het eerste artikel eerst ingevoegd en vervolgens verwijderd tijdens de afsluiting van de transactie.
- regels 39-50: invoegen van de tabel met 2 artikelen buiten een transactie om en controle.
- regels 52-59: idem, maar binnen een transactie
De resultaten bij uitvoering zijn als volgt:
- regels 5-6: door het invoegen buiten de transactie is het eerste artikel in de database achtergebleven
- regel 9: bij het invoegen binnen een transactie is er geen enkel artikel in de database achtergebleven
10.9.3. Conclusie
Het voorgaande voorbeeld heeft het nut aangetoond van thread-specifieke gegevens voor transactiebeheer. Dit voorbeeld mag niet letterlijk worden nagebootst. Frameworks zoals Spring, NHibernate, ... maken gebruik van deze techniek, maar maken deze nog transparanter: het is mogelijk dat de laag [metier] transacties gebruikt zonder dat de laag [dao] hiervan op de hoogte hoeft te zijn. Er komt dan ook geen enkel Transaction-object voor in de code van de [dao]-laag. Dit wordt bereikt door middel van een proxytechniek die AOP (Aspects Oriented Programming) wordt genoemd. Ook hier kunnen we de lezer alleen maar aanmoedigen om deze frameworks te gebruiken.
10.10. Voor meer informatie...
Om je te verdiepen in het complexe onderwerp van thread-synchronisatie, kun je hoofdstuk Threading lezen uit het boek C# 3.0, waarnaar in de inleiding van dit document wordt verwezen. Daarin worden talrijke synchronisatietechnieken voor verschillende situaties beschreven.







