Threading in.net (C#)



Vergelijkbare documenten
Programmeren in Java 3

Ondanks het feit dat we ons specifiek richten op het.net

Een gelinkte lijst in C#

Dergelijke functionaliteit kunnen we zelf ook aan eigen code toevoegen.

File Uploads met standaard ASP.NET

Datatypes Een datatype is de sort van van een waarde van een variabele, veel gebruikte datatypes zijn: String, int, Bool, char en double.

Zelftest Programmeren in Java

Streams, Formatters en Serialization in.net (Tutorial gebaseerd op tutorials van Richard Grimes, het MSDN en anderen)

Lezen van en schrijven naar het Windows Registry

Visual Basic.NET. Visual Basic.NET. M. den Besten 0.3 VB. NET

Flexibele oplossing om de eid kaart aan te spreken vanuit.net (en Delphi, Visual Basic, C++ etc)

Uitwerking Tweede deeltentamen Imperatief programmeren - versie 1 Vrijdag 21 oktober 2016, uur

Object Oriented Programming

Modelleren en Programmeren

Werken met ActiveX (COM) componenten in.net

Uitwerkingen Tweede deeltentamen Imperatief programmeren Vrijdag 15 oktober 2010, uur

Universiteit van Amsterdam FNWI. Voorbeeld van tussentoets Inleiding programmeren

Tentamen Objectgeorienteerd Programmeren

NSPYRE LEGO MINDSTORMS UITDAGING (JAVA) INLEIDING. DOEL: SIMULATOR:

Versturen van vanuit een Delphi VCL toepassing

MyDHL+ Van Non-Corporate naar Corporate

Tentamen Object Georiënteerd Programmeren TI oktober 2014, Afdeling SCT, Faculteit EWI, TU Delft

Een eerste applicatie

DIAGNOSTISCHE TOETS Softwaresystemen UITWERKING

Maak een pivot uit een Generic.List

INFORMATICA 1STE BACHELOR IN DE INGENIEURSWETENSCAPPEN

Informatica. Objectgeörienteerd leren programmeren. Van de theorie met BlueJ tot een spelletje met Greenfoot... Bert Van den Abbeele

Kleine cursus PHP5. Auteur: Raymond Moesker

Informatica: C# WPO 2

Teamhandleiding DOMjudge (versie 2.2.0muKP) 31 mei 2008

Dieper in Visual Basic.Net

Opdrachten herhalen. public void tekenscherm (object o, PEA pea) { int x; x = 1; zolang de voorwaarde geldig is

Modelleren en Programmeren

Modelleren en Programmeren

Handleiding Installatie ADS

Uitwerking Aanvullend tentamen Imperatief programmeren Woensdag 24 december 2014, uur

Modelleren en Programmeren

ASP.NET Test Jan Van Ryckeghem

DOMjudge teamhandleiding

Tentamen Imperatief Programmeren

vbg.vbnet.beginner Foutafhandeling binnen Visual Basic.NET

Even 20 jaar terug in de tijd. Ik zit op m n Commodore Amiga

Mijn eerste ADO.NET applicatie

Variabelen en statements in ActionScript

Programmeren in C# Exceptions. Hoofdstuk 17

ICARUS Illumina E653BK on Windows 8 (upgraded) how to install USB drivers

Overerving & Polymorfisme

Contents. Introduction Problem Definition The Application Co-operation operation and User friendliness Design Implementation

Chris de Kok TDI 3. Vak: Software Architectuur Datum: Docent: Fons van Kesteren

Versie 2: B C D D A C D A C C. Versie 3: C D A A B D A B D D. Versie 4: A D C C B D C B D D. Versie 5: B A D D C A D C A A

Programmeren Het gesloten boek examen 1.1

Parallelle Poort Aansturen Met Visual Basic

ALGORITMIEK: answers exercise class 7

Release Notes. Afdrukdatum: 2008/11/13

Voorbeeld: Simulatie van bewegende deeltjes

Voor de database wordt een Access 2000 bestand gebruikt, met voorlopig 1 tabel:

Klassen & objecten, overerving, abstracte klassen, debuggen, interfaces, formulieren, polymorfie, statische methoden, event-handlers

Ingebouwde klassen & methodes

Datum, Tijd en Timer-object

Inleiding C++ Coding Conventions

Real-Time Software (RTSOF) EVMINX9 Week 2

Settings for the C100BRS4 MAC Address Spoofing with cable Internet.

Het relaas van de beginnende programmeur. Het hoe en waarom van de assistent

JShell in Java 9 - De eerste officiële Java REPL

Uitwerking Tentamen Modelleren en Programmeren - versie 1 Woensdag 1 februari 2017, uur

Vereiste kennis. 1 Java-editor. 2 Het compileren van een programma

Opdracht 7a. Applicatiebouw 2014/2015

Datastructuren Werkcollege Intro

Uitwerking Tentamen Modelleren en Programmeren - versie 1 Woensdag 1 februari 2017, uur

Veel succes! 1. Deze opgave bestaat uit een aantal vragen. Houd het antwoord kort: één of twee zinnen per onderdeel kan al genoeg zijn.

Tentamen Object Georiënteerd Programmeren TI januari 2013, Afdeling SCT, Faculteit EWI, TU Delft

Modelleren en Programmeren

Hier volgt als hulp wat technische informatie voor de websitebouwer over de werking van de xml web service.

Programmeerstructuren met App Inventor

Kwis (3) class X { public void a() { System.out.println("x"); public static void main(string[] args) { X x = new X();

Java Programma structuur

FOR DUTCH STUDENTS! ENGLISH VERSION NEXT PAGE

Objectgericht programmeren 1.

Visual Basic 2005 nieuwe taalelementen André Obelink, MCSD, MVP

Een unit test is geen integratie test. Niet het hele systeem, maar onderdelen van een systeem worden getest.

NHibernate als ORM oplossing

Tentamen Objectgeorienteerd Programmeren IN1205 Voorbeeld

Omschrijf bij ieder onderdeel van de methode de betekenis ervan. Java kent twee groepen van klassen die een GUI kunnen maken: awt en swing.

Inleiding Software Engineering! Unit Testing, Contracten, Debugger! 13 Februari 2014!

Unit testen met Rhino mocks Twee handen op één buik

Dynamiek met VO-Script

Javascript oefenblad 1

Leren programmeren in C# Deel 2 - Gegevens

Vakgroep CW KAHO Sint-Lieven

RECEPTEERKUNDE: PRODUCTZORG EN BEREIDING VAN GENEESMIDDELEN (DUTCH EDITION) FROM BOHN STAFLEU VAN LOGHUM

In de tweede regel plaatsen we in het gereserveerde stukje geheugen een getal.

HOOFDSTUK 3. Imperatief programmeren. 3.1 Stapsgewijs programmeren. 3.2 If Then Else. Module 4 Programmeren

Analyse KeyLock versie Door MartinJM

Derde deeltentamen Imperatief programmeren - versie 1 Vrijdag 9 november 2018, uur

MyDHL+ ProView activeren in MyDHL+

Als een PSD selecties bevat, deelt de lijn van het programma zich op met de verschillende antwoorden op het vraagstuk.

Programmeren in C# Samenvatting van C#

Hoe met Windows 8 te verbinden met NDI Remote Office (NDIRO) How to connect With Windows 8 to NDI Remote Office (NDIRO

Het beheren van mijn Tungsten Network Portal account NL 1 Manage my Tungsten Network Portal account EN 14

Modelleren en Programmeren

Transcriptie:

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 2 e 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.