Threading in .Net (C#) 1. Inleiding Iedereen hoorde wel al eens van de obscure programmeer-/OS-/CPU-techniek threading of multithreading. Velen hebben er waarschijnlijk nog niet mee gewerkt, of worden afgeschrikt door de mogelijke moeilijkheidsgraad van het programmeren met threads. In feite heeft iedereen die al eens een programma schreef (bvb. Een Windows Forms Applicatie) al onbewust met minstens één thread gewerkt. En wat nog mooier is, het .Net Framework zorgt ervoor dat het werken met threads eenvoudiger is dan ooit. Nu, wat is een thread in feite? Een applicatie die opgestart wordt krijgt een proces toegewezen door het besturingssysteem. Een proces is een hoeveelheid computergeheugen, toegewezen aan een applicatie door het besturingssysteem, waarin de applicatie dan kan actief zijn. Een thread is een eenheid van uitvoering binnen een proces, waarvoor het besturingssysteem verwerkingstijd door de processor reserveert. Een proces heeft altijd minstens 1 actieve thread. Start bijvoorbeeld eens een nieuw C# Windows Application project, en ga kijken naar de code. De start van ons programma ligt binnen de Main-methode:
Application.Run ( new Form1() ) zal je Windows Form Applicatie opstarten in de huidige thread (zie Application.Run in de MSDN documentatie). Omdat hier in feite ons programma gestart wordt, zal ons programma dus uitgevoerd worden in de hoofdthread van het door het besturingssysteem toegewezen proces. Noot: In tegenstelling tot wat je zou denken, heeft het statement “[STAThread]” weinig te maken met deze hoofdthread binnen het applicatieproces. Deze statement is bedoeld voor COM interop. Als er geen COM componenten gebruikt worden, heeft deze geen effect. Meer info: http://msdn.microsoft.com/library/default.asp?url=/library/enus/cpref/html/frlrfSystemSTAThreadAttributeClassTopic.asp
Oké,… dus een programma loopt dus via een thread binnen een proces !? Waarom dan nog extra threads aanmaken? Neem bijvoorbeeld dat je een download manager programma wil schrijven. Het programma moet bestanden van Internet downloaden naar je lokale pc. Neem daarbij als voorbeeld dat het over een bestand van 200 megabytes gaat. Je programma maakt de nodige objecten en verbindingen aan om het bestand te downloaden, en je komt nu dus in een loop terecht waarbij je de data binnenhaalt. Ow… wat nu… mijn programma lijkt vast te hangen !? In feite hangt je programma niet, maar zit je vast in een loop die de data binnenhaalt naar je lokale pc. Doordat je in die loop blijft steken binnen de hoofdthread, kan je tijdelijk niks anders doen tot het volledige bestand binnengehaald is. Dit kan echter simpel opgelost worden door het binnenhalen van de data door een nieuwe thread te laten gebeuren. Het binnenhalen van de gegevens gebeurt dan via de nieuwe thread, en de hoofdthread blijft komt niet vast te zitten in de loop. Dat klinkt interessant, dan gebruiken we toch hier, daar en overal threads om allerlei acties uit te voeren? Het gebruik van threads heeft zowel voor- als nadelen. Een groot voordeel is dat je verschillende taken tegelijk kan laten uitvoeren, meerdere threads kunnen tegelijk door de processor worden uitgevoerd. Daar tegenover staan echter enkele nadelen. Iedere nieuwe thread zal geheugen en tijd vragen van het besturingssysteem. Het is belangrijk goed na te denken vooraleer hier, daar en overal threads te gebruiken. Een algemeen aanvaarde regel zegt zo weinig mogelijk threads te gebruiken. Bij netwerkapplicaties, ons voorbeeld van een download manager, een eigen webserver, etc. kan je bijna onmogelijk zonder threads werken, omdat er meerdere connecties gemaakt en tegelijk verwerkt moeten worden. Zo zal een webserver of netwerkapplicatie misschien meerdere clients moeten kunnen behandelen, en zal onze download manager meerdere bestanden tegelijk moeten kunnen downloaden.
Threads gebruiken brengt een verhoogde complexiteit met zich mee. Zo zal je er bijvoorbeeld voor moeten zorgen dat 2 threads niet gelijktijdig dezelfde data aanspreken, want dan kom je terecht in een zogenaamde deadlock. Hierbij wensen beide threads data aan te spreken die in gebruik is door de andere thread, waardoor de data niet vrijgegeven wordt aan de andere thread. Via thread synchronisatie (waarop we straks terug komen) is dit echter mooi op te lossen.
2. De Threading namespace Om met threads te kunnen werken in een .Net applicatie dien je een reference toe te voegen naar de System.Threading namespace. using System.Threading; Om een thread te definiëren maken we gebruik van de klasse Thread uit deze namespace. Naast de klasse Thread bevat de Threading namespace ook nog enkele belangrijke klassen zoals Monitor en Mutex, die we later gaan nodig hebben om onze threads goed te beheren en te synchroniseren. Een thread definiëren gaat als volgt: Thread myThread = new Thread( new ThreadStart( [object].Method ) ); Je merkt onmiddellijk iets vreemds, namelijk ThreadStart. Kijk eens naar de parameter die meegegeven wordt met de constructor van ThreadStart. Je geeft een methode mee als parameter. ThreadStart is een delegate (dus een reference naar een methode). Via ThreadStart specifieer je dus welke methode door de thread moet uitgevoerd worden.
(vervolgt)
We maken een klein voorbeeldprogramma. Start een nieuw C# Windows Application project en dubbelklik op de Form. Visual Studio brengt je nu automatisch naar de methode Form_Load in het codevenster. Voeg bovenaan je code de reference toe naar de Threading namespace (zie hoger), en voeg een private field “private Thread t;” toe. Voeg volgende methode toe aan je klasse Form1: private void TestPrint() { while( true ) { Console.WriteLine("Hello world"); Thread.Sleep(1000); } } (Ik kom meteen terug op het blijkbaar ongeinstancieerd gebruik van Thread.Sleep) Aan je Form_Load methode voeg je volgende code toe: t = new Thread( new ThreadStart( TestPrint ) ); t.Start(); Als je nu de applicatie start, zal er telkens met een interval van 1 seconde de tekst “Hello world” verschijnen in de Console van Visual Studio. Proficiat je allereerste simpele multithreading applicatie is een feit. Nu, omtrent het gebruik van Thread.Sleep in de methode TestPrint: We hebben geen object Thread aangemaakt, dus moet dit wel een statische methode zijn van de klasse Thread. Wanneer we Thread.Sleep aanroepen, dan hebben we het automatisch over de thread die op dit moment wordt uitgevoerd. We hebben onze thread gestart met de methode TestPrint als gedelegeerde methode, waardoor het gebruik van de statische methoden van de klasse Thread binnen deze methode enkel kan duiden op de thread die deze methode uitvoert. (Een soort van this voor threads als het ware) De statische methode Sleep( int millisecondsTimeout ) zegt de thread om een aantal milliseconden te wachten vooraleer verder te gaan met zijn uitvoering. Wat we nu doen is simpel, en werkt, maar is niet meteen “good coding practice”. We hebben niet gekeken of dit een background of foreground thread is, en we hebben de thread niet afgesloten bij het beëindigen van ons programma. Daarover meer in het volgende onderdeel…
3. Stoppen, Uitstellen en Hervatten van threads We wensen wat meer controle over onze threads. Zoals kort en onnauwkeurig uitgelegd op het einde van vorig onderdeel weten we bij onze vorige applicatie niet wat er gebeurde met onze thread na het sluiten van de applicatie. Foreground en background threads Een thread kan een foreground of een background thread zijn. Deze verschillen in feite weinig van elkaar, maar een foreground thread kan ervoor zorgen dat een proces niet afgesloten kan worden vooraleer de foreground thread afgelopen is. Een background thread worden afgesloten wanneer het proces wordt beëindigd. Standaard staat een nieuwe thread ingesteld op foreground. Als je in de voorbeeldapplicatie van deel 2 (zie hoger) de property IsBackground opvraagt van onze Thread t, dan staat die op false, wat betekent dat het een foreground thread is. Dit kan ervoor zorgen dat het door het besturingssysteem toegewezen proces niet afgesloten kan worden (zeker in ons geval, omdat we een while(true) loop gebruiken, die nergens wordt beëindigd). De systeembronnen die nodig waren voor het proces en de thread blijven in dit geval in gebruik, wat vertragingen of zelfs crashes van je besturingssysteem kan veroorzaken (onze Thread t zou niet zo enorm veel bronnen innemen en zou niet zoveel CPU-tijd eisen omdat die telkens een second slaapt, maar in ingewikkelder programma’s zou dit wel desastreuze gevolgen kunnen hebben). (Nu niet meteen panikeren, we werkten in debug mode met Visual Studio, die er bij het debuggen voor zorgt dat alles mooi geregeld wordt) Dit even tussendoor om het belang van het beheren van je threads aan te stippen.
(vervolgt)
Een thread stoppen Het manueel stoppen van een thread gebeurt via de methode Abort van de thread. Wat deze methode doet is een ThreadAbortException gooien (throw). Er wordt dus een Exception gegenereerd in ons voordeel teneinde de thread te beëindigen. Deze ThreadAbortException moet opgevangen worden, anders wordt die door ons programma als fout aanzien en vliegen we uit het programma. Je dient de code van onze methode TestPrint als volgt aan te passen: private void TestPrint() { try { while( true ) { Console.WriteLine("Hello world"); Thread.Sleep(1000); } } catch( ThreadAbortException tae ) { Console.WriteLine("Thread aborted"); } finally { Console.WriteLine("Thread cleanup"); } } Nu wordt de ThreadAbortException opgevangen, en wordt er een boodschap in de Visual Studio Console weergegeven. Om de thread te stoppen, plaats een button op je Form, en voeg volgende code toe bij het Click event ervan : private void button1_Click(object sender, System.EventArgs e) { t.Abort(); t.Join(); }
Wanneer je nu de applicatie start wordt de thread gestart via de Form_Load methode, en als je dan klikt op de knop, dan zal de thread beëindigd worden.
Wanneer we de methode Abort van onze thread uitvoeren, wordt dus een ThreadAbortException gegenereerd, die opgevangen wordt door ons catch-block. Voordat de thread eindigt zal ook nog de code in het finally-block uitgevoerd worden, hier kun je bijvoorbeeld “ cleanup” -code plaatsen die bronnen vrijmaakt die niet meer nodig zijn wegens het beëindigen van de thread. Je ziet ook dat we de Join methode van de te beëindigen thread aanroepen. Join zorgt er in ons voorbeeld voor dat de thread die het beëindigen van onze thread aanroept geblokkeerd wordt tot onze thread klaar is. Bij ons voorbeeld zal de hoofdthread van ons programma dus moeten wachten tot de code in het finally-block afgewerkt is. Uitstellen (Suspend) en Hervatten (Resume) van een thread De benaming van deze methoden spreekt zowat voor zichzelf. De methode Suspend() pauzeert als het ware de thread. De uitvoering gaat pas verder als de methode Resume() aangeroepen wordt. Blangrijke noot: Als je een suspended thread wil afsluiten (Abort), dan moet je eerst de thread hervatten (Resume) vooraleer je de methode Abort aanroept. De MSDN documentatie zegt dat dit automatisch gebeurt bij het aanroepen van de methode Abort, maar artikels op Internet van verschillende programmeurs spreken dit tegen. Blijkbaar gebeurt het niet altijd automatisch, en maak je er best een gewoonte van om de thread eerst te hervatten vooraleer je deze sluit.
(vervolgt)
De thread status We kunnen nu al een thread starten, pauzeren en hervatten, maar hoe weten we nu in welke status de thread op een gegeven moment verkeert? Een instantie van de klasse Thread heeft een enumeration property “ ThreadState” . Je kan te allen tijde de status van de thread via deze property evalueren. Mogelijke ThreadStates voor de Thread.ThreadState property: Aborted AbortRequested Background Running Stopped StopRequested Suspended SuspendRequested Unstarted WaitSleepJoin
Thread is gestopt De Abort methode werd uitgevoerd op deze thread, maar de ThreadAbortException is nog niet “ thrown” . Wordt uitgevoerd als background thread. Thread wordt momenteel uitgevoerd. De thread is gestopt. Het stoppen van de thread werd aangevraagd. De thread is uigesteld. Er werd de thread gevraagd Suspended te worden. De thread is nog niet gestart. De thread is geblokkeerd door aanroep van Wait, Sleep of Join.
Daar dit ThreadState property een enumeration is kan je bijvoorbeeld op volgende manier een code-block uitvoeren wanneer de thread actief is: if (myThread1.ThreadState != (ThreadState.Stopped | ThreadState.Unstarted)) { //thread is niet gestopt en is wel gestart //uit te voeren code }
(vervolgt)
De prioriteit van een thread Een thread heeft altijd een specifieke prioriteit. Standaard (als je zelf geen andere prioriteit opgeeft) staat de prioriteit van een thread ingesteld op “ Normal” . Het instellen van de prioriteit van een thread gebeurt via de enumeration property “ Priority” van een thread. Zoals eerder reeds aangehaald krijgt iedere thread een zekere CPU-verwerkingstijd toegewezen door het besturingssysteem. Wat ik nog niet vermeldde, is dat het besturingssysteem ook rekening houdt met de prioriteit van een thread om deze verwerkingstijd toe te wijzen. Zo gaan de threads met een hogere prioriteit vlugger en meer verwerkingstijd toegewezen krijgen. In .Net kan je 5 verschillende prioriteitsniveaus toewijzen aan een thread, namelijk “ Highest” , “ AboveNormal” , “ Normal” , “ BelowNormal” en “ Lowest” . Een thread met als prioriteit “ Highest” zal eerst en meest verwerkingstijd toegewezen krijgen van het besturingssysteem. AboveNormal zal gepland worden voor de threads met lagere prioriteit, maar na threads met “ Highest” prioriteit, enzovoort. Een thread met als prioriteit “ Lowest” zal pas na alle andere threads met hogere prioriteit gepland worden door het besturingssysteem. Mocht je nu een voorbeeldapplicatie maken, met laat ons zeggen 5 threads, waarvan iedere thread één van de verschillende prioriteiten heeft. Zorg ervoor dat de thread met laagste prioriteit eerst gestart wordt, daarna telkens de thread met hogere prioriteit startend. Je zal merken dat de thread met prioriteit “ Highest” eerst zal uitgevoerd worden en als eerste de status “ Stopped” zal bereiken, daarna de thread met prioriteit “ AboveNormal” , enzovoort.
(vervolgt)
Pas uit het vorige voorbeeld de methode TestPrint aan, zodanig dat de methode geen oneindige loop verwerkt (zet while (true) bijvoorbeeld in commentaar) en verwijder ook Thread.Sleep(1000). Een thread object heeft een string-property “ Name” . Deze gaan we gebruiken om onze threads te identificeren. Gebruik volgende code om de threads uit te voeren: x = new Thread( new ThreadStart( TestPrint ) ); x.Priority = ThreadPriority.Lowest; x.Name = "lowest_priority_thread"; w = new Thread( new ThreadStart( TestPrint ) ); w.Priority = ThreadPriority.BelowNormal; w.Name = "belownormal_priority_thread"; v = new Thread( new ThreadStart( TestPrint ) ); v.Priority = ThreadPriority.Normal; v.Name = "normal_priority_thread"; u = new Thread( new ThreadStart( TestPrint ) ); u.Priority = ThreadPriority.AboveNormal; u.Name = "abovenormal_priority_thread"; t = new Thread( new ThreadStart( TestPrint ) ); t.Priority = ThreadPriority.Highest; t.Name = "high_priority_thread"; x.Start(); w.Start(); v.Start(); u.Start(); t.Start(); Resultaat in de Visual Studio console (in chronologische volgorde): The thread ' high_priority_thread'(0xef4) has exited with code 0 (0x0). The thread ' abovenormal_priority_thread'(0xec8) has exited with code 0 (0x0). The thread ' normal_priority_thread'(0x8ec) has exited with code 0 (0x0). The thread ' belownormal_priority_thread'(0x968) has exited with code 0 (0x0). The thread ' lowest_priority_thread'(0xde4) has exited with code 0 (0x0). Niettegenstaande deze threads alle tegelijk werden gestart in stijgende volgorde van prioriteit, zien we dat de de threads met de hogere prioriteit toch eerst uitgevoerd worden.
Plaats nu eens de regel code Thread.Sleep (1000) terug in de methode TestPrint. De output naar de console wijzigt als volgt: The thread ’lowest_priority_thread’ (0x70) has exited with code 0 (0x0). The thread ’high_priority_thread’ (0x630) has exited with code 0 (0x0). The thread ’abovenormal_priority_thread’ (0xed4) has exited with code 0 (0x0). The thread ’belownormal_priority_thread’ (0xf48) has exited with code 0 (0x0). The thread ’normal_priority_thread’ (0xf18) has exited with code 0 (0x0). Omdat iedere thread gevraagd wordt om te slapen (Sleep), wordt de uitvoering doorgegeven aan de volgende thread met hoogste prioriteit vooraleer de huidige thread weer uitvoeringstijd krijgt. Zo kan je de uitvoering van threads met lagere prioriteit forceren. Natuurlijk is dit geen goede manier van werken. In het volgende onderdeel gaan we het hebben over synchronisatie van threads.
4. Threads synchroniseren Wanneer je een applicatie maakt die gebruik maakt van threads kan het makkelijk gebeuren dat 2 threads hetzelfde object of dezelfde methode van een object moeten aanspreken, wat desastreuze gevolgen kan hebben. Neem als voorbeeld een applicatie die werkt met een achterliggende database. We hebben een methode “ Update” , die gegevens aanpast in de database. Er wordt een thread gestart die onze methode “ Update” gebruikt. Kort daarna wordt een tweede thread gestart, die ook deze methode moet aanroepen, de eerste thread is echter nog niet klaar met de uitvoering van de Update. Het besturingssysteem zet de uitvoering van de eerste thread op uigesteld (Suspended), en start de tweede thread. De eerste thread was echter nog niet klaar, en het aangeroepen object verkeert in een ongeldige staat. Wanneer de tweede thread nu het object aanspreekt zal de applicatie natuurlijk crashen, of soms zelfs erger (verloren gegevens, data corruptie, etc). Je moet er rekening mee houden dat een thread uitgevoerd zal worden wanneer het besturingssysteem dit beslist, niet wanneer jij wil dat dit zal gebeuren. Het is dus noodzakelijk threads te synchroniseren, zeker als we met meerdere threads dezelfde data of objecten moeten aanspreken. Wanneer we het hebben over thread synchronisatie hebben we het ook over “ Thread Safety” (Bij de documentatie van vele objecten in de MSDN staat dan ook vermeld als ze al dan niet thread safe zijn). “ Thread Safe” betekent dat op elk moment dat een object dat aangeroepen of gebruikt wordt door een thread, dit object zich in een geldige staat bevindt. Ik haal een codevoorbeeld aan dat ik vond op internet van een zeker meneer Ben Hinton. Maak zoals bij de vorige voorbeelden een applicatie, die 2 threads start. Gebruik als gedelegeerde methode de methode “ WriteToFile” van een object FileIO (onderstaande code). Geef beide threads een naam (Name property), bijvoorbeeld “ thread 1” en “ thread 2” . De methode WriteToFile zal de threadnaam en enkele getallen naar het bestand SyncTest.txt proberen te schrijven.
using System; using System.Threading; using System.IO; using System.Text; class SyncTest { static void Main(String[] args) { // Create the object we want to use on the threads FileIO obj = new FileIO(); // Define and start the threads Thread t1 = new Thread(new ThreadStart(obj.worker1)); t1.Name = "Thread 1"; Thread t2 = new Thread(new ThreadStart(obj.worker2)); t2.Name = "Thread 2"; t1.Start(); t2.Start(); } } class FileIO { public void worker1() { WriteToFile(); } public void worker2() { WriteToFile(); }
}
private void WriteToFile() { // Open a file FileStream fs = new FileStream("C:\\Temp\\SyncTest.txt", FileMode.Append); // Get the current thread Thread t = Thread.CurrentThread; // Write some data to the file fs.Write(Encoding.ASCII.GetBytes(t.Name + "\r\n"), 0, t.Name.Length+2); for (int i=0; i<100000; i++) fs.Write(Encoding.ASCII.GetBytes(i + ","), 0, i.ToString().Length+1); // Close the file fs.Close(); }
Wanneer je de applicatie start, dan zal deze crashen. Beide threads spreken hetzelfde object aan, en wanneer de 2e thread het object aanspreekt zal het FileIO object zich in een ongeldige staat bevinden (Terwijl de eerste thread nog naar het bestand aan het schrijven is, wordt de thread uitgesteld (Suspended) door het besturingssysteem). Wat we moeten doen is het object vergrendelen (locking). We kunnen dit doen aan de hand van de klasse Monitor. We starten het “ monitoren” van een object via de statische methode “ Enter” van klasse Monitor en stoppen de lock via de statische methode “ Exit” ervan. Beide methoden nemen een object als parameter. We gebruiken this als parameter om op het object zelf te duiden. Hercompileer je applicatie met volgende aangepaste methode WriteToFile: private void WriteToFile() { // Obtain an exclusive lock on this object Monitor.Enter(this); // Open a file FileStream fs = new FileStream("C:\\Temp\\SyncTest.txt", FileMode.Append); // Get the current thread Thread t = Thread.CurrentThread; // Write some data to the file fs.Write(Encoding.ASCII.GetBytes(t.Name + "\r\n"), 0, t.Name.Length+2); for (int i=0; i<100000; i++) fs.Write(Encoding.ASCII.GetBytes(i + ","), 0, i.ToString().Length+1); // Close the file fs.Close();
}
// Release the lock on this object Monitor.Exit(this);
Je zal merken dat de applicatie niet meer crasht, en wat meer is, de applicatie zal nu zelfs perfect werken. De tweede thread zal nu moeten wachten tot wanneer de uitvoering van de aanroep van WriteToFile door de eerste thread afgelopen is. De Monitor klasse is een kleine klasse met slechts 6 methoden, die alle static methoden zijn. Straks kom ik terug op enkele andere methoden van deze klasse.
We hebben nu gezien hoe we een object kunnen vergrendelen opdat we geen deadlocks of crashes krijgen via het werken met threads. Maar wat nu als bepaalde code binnen een vergrendeld object een Exception zou genereren ? We zouden dus uit de methode vliegen, maar het object zou wel vergrendeld blijven, zodat geen enkele andere thread er nog toegang toe zal krijgen. Dit lossen we op door een try-catch-finally codeblock, we plaatsen Monitor.Exit(this); in het finally block, zodat het object niettegenstaande wat er ook mag gebeuren toch weer vrijgegeven wordt. private void WriteToFile() { try { // Object vergrendelen Monitor.Enter(this); // Doe iets DoSomething();
} catch (System.Exception e){ // Exception handling Console.WriteLine(e.Message); } finally { // Object vrijgeven Monitor.Exit(this); } }
Noot: Zijn C# en .Net niet fantastisch !?
5. De Monitor klasse We zagen in hoofdstuk 4 reeds het gebruik van de statische methoden Enter en Exit om threads te synchroniseren. De klasse Monitor heeft echter nog 4 andere methoden. De methode TryEnter Gelijkaardig aan de methode enter. Deze methode probeert een object te vergrendelen, maar in tegenstelling tot de methode Enter geeft deze methode een boolean waarde terug (true voor geslaagd, false voor mislukt). Deze methode heeft 3 overloads: Overload List (uit MSDN documentatie) Attempts to acquire an exclusive lock on the specified object. public static bool TryEnter(object); Attempts, for the specified number of milliseconds, to acquire an exclusive lock on the specified object. public static bool TryEnter(object, int); Attempts, for the specified amount of time, to acquire an exclusive lock on the specified object. public static bool TryEnter(object, TimeSpan); Net als bij de Enter methode moet je een object reference meegeven voor het object dat moet vergrendeld worden, maar via een overload kun je ook opgeven voor hoelang er moet geprobeerd worden om het object te vergrendelen. De methode Wait Als je een object vergrendelde met Monitor.Enter, maar je wenst er nu toch een andere thread toegang tot geven, dan kan je de methode Wait oproepen. Deze zal de huidige thread uitstellen tot de nieuwe thread (die nu toegang zal krijgen tot het object) het object terug vrijgeeft.
(vervolgt)
De methode Wait heeft ook een reeks overloads: Overload List (uit de MSDN documentatie) Releases the lock on an object and blocks the current thread until it reacquires the lock. public static bool Wait(object); Releases the lock on an object and blocks the current thread until it reacquires the lock or a specified amount of time elapses. public static bool Wait(object, int); Releases the lock on an object and blocks the current thread until it reacquires the lock or a specified amount of time elapses. public static bool Wait(object, TimeSpan); Waits for notification from an object that called the Pulse or PulseAll method or for a specified timer to elapse. This method also specifies whether the synchronization domain for the context (if in a synchronized context) is exited before the wait and reacquired. public static bool Wait(object, int, bool); Releases the lock on an object and blocks the current thread until it reacquires the lock, or until a specified amount of time elapses, optionally exiting the synchronization domain for the synchronized context before the wait and reacquiring the domain. public static bool Wait(object, TimeSpan, bool); De methode Pulse Deze methode kan enkel door de thread die de rechten op het object beheerst uitgevoerd worden. Deze methode zal aan de thread die wacht (bijvoorbeeld een thread die Wait aanriep, en zo het object tijdelijk vrijgaf) op het vrijkomen van het object te kennen geven dat het object nu vrij is om te gebruiken. Deze methode heeft geen overloads en neemt slechts de reference naar het object als parameter. De methode PulseAll Deze methode verschilt van de methode Pulse in die mate dat deze methode aan ALLE actieve threads te kennen zal geven dat het object ontgrendeld is, en dus niet enkel aan de thread die via een aanroep van Wait wacht op het vrijkomen van het object. Ook deze methode heeft geen overloads, en neemt een reference naar een object als parameter.
Ik neem terug voorbeeldcode van de vriendelijke meneer Hinton om dit te demonstreren: using System; using System.Threading; public class RWSync { public static void Main(string[] args) { // Create a SyncTest object SyncTest st = new SyncTest(); // Define two threads Thread t1 = new Thread(new ThreadStart(st.ThreadWrite)); Thread t2 = new Thread(new ThreadStart(st.ThreadRead)); // Start the threads t1.Start(); t2.Start();
}
}
// Call Join() to make sure they have terminated t1.Join(); t2.Join();
public class SyncTest { // Private boolean flag to determine if we should read or write private bool isRead = false; // Private variable to hold the ingeger being written to/read from int data = 0; // Public method to run on worker thread public void ThreadRead() { for (int i=0; i<20; i++) ReadLoop(); } // Public method to run on worker thread public void ThreadWrite() { for (int i=0; i<20; i++) WriteLoop(i); }
// Read the current number then use Pulse to release the lock public int ReadLoop() { try { // Obtain an exclusive lock Monitor.Enter(this); // If we are not meant to be on a read - then Wait! if (!isRead) Monitor.Wait(this); // Read the contents of the data variable to the console Console.WriteLine("Read : {0}", data); // Set isRead to false, to prepare for performing a Write // and alert the other thread so it can continue isRead = false; Monitor.Pulse(this);
}
} catch (SystemException e) { // Catch exceptions here... Console.WriteLine("Exception caught: " + e.Message); } finally { // Release the exclusive lock on the object Monitor.Exit(this); } return data;
public void WriteLoop(int i) { try { // Obtain an exclusive lock Monitor.Enter(this); // If the flag is set to true (ie Read) then Wait if (isRead) Monitor.Wait(this); // Set the data variable and write to the console data = i; Console.WriteLine("Write : {0}", data);
// Set the isRead flag to true and alert the other thread isRead = true; Monitor.Pulse(this);
}
}
} catch (SystemException e) { // Catch exceptions here... Console.WriteLine("Exception caught: " + e.Message); } finally { // Release the excluseive lock on the object Monitor.Exit(this); }
Deze code lijkt meteen heel wat ingewikkelder. Neem uw tijd en bestudeer de code aandachtig, want deze code is van hetzelfde niveau als waarop we tot nochtoe werkten. Er is een klasse SyncTest gedefinieerd, die 4 methodes heeft. 2 ervan (ThreadRead en ThreadWrite) gaan de gedelegeerde methoden zijn voor de 2 threads die we in dit voorbeeld gaan gebruiken. Deze 2 methoden roepen respectievelijk de 2 andere methoden (ReadLoop en WriteLoop) aan. Zoals je meteen ziet, gaan de 2 threads gesynchroniseerd zijn door het gebruik van Monitor.Enter en Monitor.Exit in deze 2 ‘loop-methoden’. Als je kijkt naar de fields van de klasse SyncTest zie je een boolean variabele “ isRead” . Deze zal als flag gebruikt worden door de threads om te zien of er moet gelezen of geschreven worden naar de variabele data. Aan de hand van de boolean variabele “ isRead” weten de threads dat er eerst moet geschreven worden, de Thread t1 zal dus eerst rechten op het SyncTest object krijgen. Via de methode WriteLoop zal t1 een integer waarde in de variabele “ data” plaatsen. Meteen daarna wordt de “ isRead” flag op true gezet en wordt Monitor.Pulse aangeroepen voor deze thread. Zo weet de tweede thread (t2) dat het object vrijkwam. Nu krijgt Thread t2 de mogelijkheid (het object is ontgrendeld) via de methode ReadLoop de waarde die t1 in de variabele data schreef af te drukken naar de console. Thread t2 zet daarna terug “ isReady” op false enzovoort. Als output krijg je afwisselend Write n en Read n (met n de integer variabele die op dat moment in variabel data stak.
6. Het lock statement Het vergrendelen van objecten (Monitor klasse) voor gebruik met threads is heel belangrijk, er is zelfs een lock statement gedefinieerd in .Net, waarmee je op kortere schrijfwijze hetzelfde kan doen als met het in hoofdstuk 4 beschreven try-catchfinally codeblock in samenwerking met Monitor.Enter en Monitor.Exit. Zo kan je de voorbeeldcode in hoofdstukje 4 ook korter als volgt schrijven (het trycatch-finally codeblock met Monitor.Enter en Monitor.Exit wordt dan op de achtergrond door de compiler gegenereerd): private void WriteToFile() { lock (this) { // Doe iets DoSomething(); } }
(Uit de MSDN documentatie) The lock statement obtains the mutual-exclusion lock for a given object, executes a statement, and then releases the lock. lock-statement: lock ( expression ) embedded-statement The expression of a lock statement must denote a value of a reference-type. No implicit boxing conversion is ever performed for the expression of a lock statement, and thus it is a compile-time error for the expression to denote a value of a value-type. A lock statement of the form lock (x) ... where x is an expression of a reference-type, is precisely equivalent to System.Threading.Monitor.Enter(x); try { ... } finally { System.Threading.Monitor.Exit(x); }
Mogelijke uitbreiding: het Mutex object.
Veel plezier. Kris.