Totally Integrated Automation ing. Gaillez Michael
7 november 2004
Inhoudsopgave 1 Algemene begrippen 1.1 Totally Integrated Automation . . . . 1.2 SCADA . . . . . . . . . . . . . . . . . 1.3 Data Acquisition . . . . . . . . . . . . 1.4 WIN32 DLL’s, COM, DCOM, COM+, 1.4.1 WIN32 DLL’s . . . . . . . . . . 1.4.2 COM . . . . . . . . . . . . . . 1.4.3 DCOM . . . . . . . . . . . . . 1.4.4 COM+ . . . . . . . . . . . . . 1.4.5 OLE . . . . . . . . . . . . . . . 1.4.6 ActiveX . . . . . . . . . . . . . 1.5 Een totaal nieuwe wereld... .NET . . . 1.6 OPC . . . . . . . . . . . . . . . . . . . 1.7 Samenvattig . . . . . . . . . . . . . . .
. . . . . . . . . OLE . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Sockets 2.1 Wat zijn sockets? . . . . . . . . . . . . . . . 2.2 Sockets in .NET . . . . . . . . . . . . . . . 2.2.1 IPAddress class . . . . . . . . . . . . 2.2.2 IPEndPoint . . . . . . . . . . . . . . 2.2.3 Synchonious vs. Asynchronious . . . 2.2.4 TcpListener class . . . . . . . . . . . 2.2.5 TcpClient class . . . . . . . . . . . . 2.2.6 TcpListener en TcpClient toegepast 2.3 Sockets in Java . . . . . . . . . . . . . . . . 2.3.1 InetAddress class . . . . . . . . . . . 2.3.2 InetSocketAddress class . . . . . . . 2.3.3 ServerSocket class . . . . . . . . . . 2.3.4 Socket class . . . . . . . . . . . . . . 2.4 Een protocol implementeren . . . . . . . . . 2.4.1 Wat is een protocol . . . . . . . . . . 3 Delegates en events in .NET 3.1 Inleiding . . . . . . . . . . . . . . . . . . . 3.2 Baas-Werknemer Tightly Coupled . . . . 3.3 Baas-Werknemer met interfaces . . . . . . 3.4 Baas-Werknemer en Delegates . . . . . . . 3.5 Baas-Werknemer the final story. . . Events 1
. . . . .
. . . . . . en . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . . . . ActiveX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . .
. . . . . . . . . . . . .
6 6 6 8 8 8 9 9 9 9 9 9 10 11
. . . . . . . . . . . . . . .
13 13 14 14 15 16 16 18 20 22 23 23 23 24 25 25
. . . . .
26 26 27 28 31 35
INHOUDSOPGAVE
4 Threading 4.1 Inleiding . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Threading explained . . . . . . . . . . . . . . . . . . . 4.2.1 Multitasking . . . . . . . . . . . . . . . . . . . 4.2.2 Multithreading . . . . . . . . . . . . . . . . . . 4.2.3 Is it realy that simple? . . . . . . . . . . . . . . 4.2.4 Synchronization . . . . . . . . . . . . . . . . . 4.2.5 Signaling . . . . . . . . . . . . . . . . . . . . . 4.2.6 Thread States . . . . . . . . . . . . . . . . . . . 4.3 Threading in .NET . . . . . . . . . . . . . . . . . . . . 4.3.1 Thread en ThreadStart . . . . . . . . . . . . . 4.3.2 De ThreadPool . . . . . . . . . . . . . . . . . . 4.3.3 Synchronization een waaier aan mogelijkheden 4.3.4 Signaling . . . . . . . . . . . . . . . . . . . . . 4.4 Threading in Java . . . . . . . . . . . . . . . . . . . . 4.4.1 Implementing Runnable interface . . . . . . . . 4.4.2 Threading door inheritance . . . . . . . . . . . 4.4.3 Threading algemeenheden . . . . . . . . . . . . 4.4.4 Synchronization . . . . . . . . . . . . . . . . . 4.4.5 Signaling . . . . . . . . . . . . . . . . . . . . . 4.5 Samenvatting en conclusies . . . . . . . . . . . . . . . 5 Services 5.1 Inleiding . . . . . . . . . . . . . . . . . . . 5.2 NT-services in .NET . . . . . . . . . . . . 5.2.1 NT-service als hosting environment 5.2.2 Eigenschappen van een NT-service 5.2.3 Een NT-service bouwen . . . . . . 5.2.4 Installatie van NT-Services . . . . 5.2.5 Debuggen van NT-Services . . . . 5.2.6 Interactie met een NT-Service . . . 5.3 Java service wrappers . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . .
42 42 42 42 44 47 49 52 53 54 54 56 58 62 65 66 66 67 67 68 69
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
71 71 71 71 72 73 76 76 77 78
6 Asynchrone function calls in .NET 6.1 Inleiding . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Noden voor een Asynchroon systeem . . . . . . . . . 6.3 Delegates herbekeken . . . . . . . . . . . . . . . . . . 6.4 Asynchroon programming model . . . . . . . . . . . 6.4.1 BeginInvoke en EndInvoke gebruiken . . . . . 6.4.2 IAsyncResult interface . . . . . . . . . . . . . 6.4.3 AsyncResult class . . . . . . . . . . . . . . . 6.4.4 De Callback method gebruiken . . . . . . . . 6.5 Asynchrone calls zonder delegates . . . . . . . . . . . 6.6 Asynchrone calls en Synchronization . . . . . . . . . 6.7 Asynchrone calls en Windows Forms . . . . . . . . . 6.7.1 Windows Message Loop . . . . . . . . . . . . 6.7.2 Windows Message Loop vs. Asynchrone calls 6.7.3 Solving the problem met ISynchronizeInvoke 6.7.4 ISynchronizeInvoke ook voor threads . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
79 79 79 80 82 82 83 86 88 90 91 91 92 93 93 97
ing. Gaillez Michael
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
2
INHOUDSOPGAVE
7 Extending the GUI 99 7.1 GDI+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 7.2 Java swing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 8 Beyond .NET 100 8.1 Win32 libraries . . . . . . . . . . . . . . . . . . . . . . . . . . 100 8.2 COM-components . . . . . . . . . . . . . . . . . . . . . . . . 100
ing. Gaillez Michael
3
Inleiding In de industrie is men door de jaren heen steeds meer op zoek gegaan naar manieren om processen te gaan automatiseren. Uit deze zoektocht zijn heel wat tools en functionaliteiten ontstaan die ons staat stellen om dit te gaan doen, denk maar aan PLC’s, Data Aquisition Cards, . . . Maar door het ontstaan van al die tools is er een nieuw probleem gecreerd nl. Hoe kunnen we al deze componenten met elkaar gaan integreren? Deze vraag ligt aan de oorsprong van de term TIA of Totally Integrated Automation. Hierbij gaan we gebruik maken van standaard protocollen, databases en user interfaces om deze verschillende componenten aan elkaar te gaan linken. Op die manier bekomen we data- en controle netwerken die beschikbaar zijn van op de werkvloer tot bij de manager. Als we denken aan automatisering dan is het eerste wat bij ons opkomt, het zelfstandig laten verlopen van processen waarbij weinig of geen interactie meer van mensen nodig is. Maar automatiseren is veel meer dan dat alleen. Als we een eenvoudig productieproces hebben die van basis ingredi¨enten zoals bloem, water, . . . , volledig afgewerkte dozen met koekjes aflevert, dan willen we daar ook nog allerlei andere zaken aan gaan koppelen, bijvoorbeeld: • Hoeveel koekjes worden er geproduceerd in een welbepaalde tijd? • Hoe kunnen we door het invoeren van een ingredi¨entenlijst andere koekjes gaan maken? • Hoeveel dozen met koekjes zijn er in stock? • ... Uit deze lijst kunnen we een aantal basiselementen halen die van belang zijn bij automatisering. Procesautomatisatie: Het zelfstandig laten werken van machines, . . . Monitoring: Het opvangen van allerlei gegevens uit processen. Communcatie: Het uitwisselen van gegevens van processen. Manipulatie: Het veranderen van data van het proces op een eenvoudige manier. Visualisatie: Het weergeven van data van het proces op een duidelijke manier.
4
INHOUDSOPGAVE
Archievering: Het opslaan van data voor eventueel later gebruik. In de specialisatie Toegepaste Automatisering gaan we ons vooral toespitsen op deze topics. Enerzijds hebben we de module Industriele automatisering die zich vooral toelegt op de procesautomatisatie, monitoring en een stuk manipulatie. Anderzijds hebben we de module Totally Integrated Automatisation die zich gaat bezig houden met communicatie, visualisatie, archievering en eveneens een stuk manipulatie.
ing. Gaillez Michael
5
Hoofdstuk 1
Algemene begrippen 1.1
Totally Integrated Automation
Definitie 1 Totally Integrated Automation is het integreren van hard- en software producten door middel van standaard communicatietechnologi¨en, databases en userinterfaces. De voordelen van deze aanpak t.o.v. meer conventionele methoden is: • Gemeenschappelijke database. • Eenduidige platform voor communcitie. • Standaard programmeer omgeving. • Informatie kan gedeeld worden door werkvloeren, applicaties en afdelingen. • Gemeenschappelijke user interface. • Controle via het web • Geavanceerde evaluatie van een volledig systeem waardoor downtime kan gereduceerd worden. • Verticale integratie door het linken van SCADA (zie 1.2) met Management Execution Systems (MES) en andere top level enterprise systemen (ERP-pakketen zoals SAP).
1.2
SCADA
SCADA staat voor Supervisory Control And Data Acquisition software. De definitie van SCADA omvat een aantal termen zoals: Computer Based: Om te kunnen spreken van een SCADA-systeem moet mogelijkheid zijn om alle mogelijke types van interconnectie en integratie te gebruiken. Dit betekent concreet: seri¨ele poorten, ethernet,
6
1. Algemene begrippen
PCI-sloten, . . . en een breed gamma van applicaties en platformen. PLC’s en andere eenvoudige interfaces zijn meestal te beperkt in hun functionaliteit en mogelijkheden. Door gebruik te maken van computers heb je toegang tot vlotte userinterfaces, databases en noem maar op. Alarm en event monitoring: Een SCADA-systeem moet in staat zijn op alarmen en gebeurtenissen te detecteren, weergeven, en loggen. Als er zich problemen voor doen dan moet het SCADA-systeem in staat zijn om operators te waarschuwen zodat er actie kan ondernomen worden. Alarmen en events moeten ook gelogd worden zodat programmeurs en ingenieurs deze kunnen bekijken en eventueel voorkomen dat ze zich nog eens voordoen. Data Acquisition: SCADA moet data van PLC’s en andere hardware kunnen lezen om die achteraf grafisch te laten weergeven of te laten analyseren door de gebruiker. SCADA moet meerdere soorten van data kunnen lezen en schrijven. Operator Interface: Een SCADA-systeem verzamelt alle informatie van een proces. Dus moet een SCADA-systeem de data kunnen weergeven op een manier zodat de operator(user) kan begrijpen wat er gebeurt met het proces. Non Real-Time Control: In eenvoudige systemen zou SCADA in staat moeten zijn om alle taken uit te voeren i.p.v een PLC. Maar van zodra het om iets complexere systemen gaat, geven we de voorkeur aan een PLC om de real-time taken uit te voeren en SCADA de Non real-time taken. SCADA is dus als het ware het medium tussen de operator (user) en de real-time controller (PLC). Typische voorbeelden van non real-time taken zijn: het opstarten van een nieuwe batch, een nieuw recept inladen, . . . Databases en Data Logging: In de meeste gevallen hebben applicaties nood aan het loggen van data en andere manieren om databases te lezen en te schrijven. Een SCADA-systeem kan dus grote hoeveelheden data loggen om die pas later te bekijken. Dit is heel handig voor het oplossen van problemen of om het proces te verbeteren. Delen van Rapporten en Informatie: Wat is het nut van informatie verzamelen als je het niet kan delen met anderen? Sommige mensen krijgen hun data liever in database- of Excel-formaat en nog anderen dan weer in word, . . . Ook moet SCADA informatie kunnen delen met andere gebruikers door middel van netwerk, webserver en webservices. Deze laatste drie methodes laten toe dat bijna iedere computer in de wereld de data kan raadplegen (mits de nodige beveiliging natuurlijk). Conclusie: Als we het geheel van SCADA bekijken dan is dit vooral het domein waarbinnen wij actief gaan zijn nl. het verzamelen, opslaan, visualiseren en beschikbaar stellen van data.
ing. Gaillez Michael
7
1. Algemene begrippen
1.3
Data Acquisition
De acquisitie van gegevens is onmisbaar om de automatiseringsprocessen te controleren en te sturen. De opgetekende gegevens hebben betrekking op alle mogelijke fysieke variabelen die gemeten kunnen worden met behulp van sensoren. De functies die een volledig data-acquisitiesysteem kan bevatten zijn de volgende: • sensor signaalconditionering • isolatie • analoog naar digitaal conversie (A/D) • digitaal naar analoog conversie(D/A) • operator interface • data reductie en analyse • controle algoritmen • permanent data-geheugen. De eerste vier van deze bestanddelen zijn primaire functies van de dataacquisitie die door middel van hardware worden gerealiseerd. De laatste vier zijn meestal software-functies, of worden door middel van een programma aangewend.
1.4
1.4.1
WIN32 DLL’s, COM, DCOM, COM+, OLE en ActiveX WIN32 DLL’s
De WIN32 Application Programming Interface (API) omschrijft de methoden van het Windows Operating Systeem. Deze bieden toegang tot onder andere windows, dialogs, brushes, . . . Deze API’s vergen echter een groot inzicht in het Windows Operating Systeem waardoor ze niet echt vlot bruikbaar zijn voor ons. Ze vormen echter nog altijd de core van Windows en echte core programming dient te gebeuren door middel van deze API’s. Een typisch voorbeeld hiervan zijn hardware drivers. In toekomstige versie’s van Windows zou hier wel eens verandering in kunnen komen omdat die steeds meer gaan gebaseerd zijn op .NET technologie. Maar voor onze toepassingen wensen we het gebruik van deze WIN32 API’s zoveel mogelijk te vermijden. Verkeerd gebruik van deze API’s kan namelijk al snel leiden tot een instabiel systeem (de befaamde BSOD). We zullen er echter niet omheen kunnen om deze componenten soms te gebruiken.
ing. Gaillez Michael
8
1. Algemene begrippen
1.4.2
COM
COM of Component Object Model zijn discrete componenten met een unieke identity, die een aantal interfaces ter beschikking stellen. Deze interfaces laten op hun beurt toe dat andere applicaties en componenten hun functionaliteit kunnen gebruiken. COM componenten zijn breder inzetbaar dan WIN32 DLL’s omdat ze volledig taal onafhankelijk zijn. Je kan COM componenten gemakkelijk benaderen vanuit C++, VB en Java.
1.4.3
DCOM
DCOM of Distributed Component Object Model is een uitbreiding op COM. DCOM laat toe dat COM componenten met elkaar communiceren over een netwerk. Waar COM enkel communicatie kan handhaven tussen verschillende processen op ´e´enzelfde machine, kan DCOM communicatie voeren tussen processen op verschillende computers in een netwerk. DCOM maakt gebruik van het Remote Procedure Call (RPC) mechanisme om informatie te verzenden en ontvangen tussen verschillende componenten.
1.4.4
COM+
COM+ is eveneens een uitbreiding op COM. Grote veranderingen zijn dat resource management taken zoals threading en security nu standaard voor handen zijn, waar die met COM nog moesten geprogrammeerd worden.
1.4.5
OLE
OLE staat voor Object Linking and Embedding. In een notedop betekent OLE dat je volledige applicatie’s (bvb Word) kan onder brengen in uw eigen applicaties. OLE is is gebaseerd op COM.
1.4.6
ActiveX
ActiveX is een meer recente versie van OLE. ActiveX laat onder meer toe om componenten te distribueren over internet en te integreren met bijvoorbeeld een webbrowser.
1.5
Een totaal nieuwe wereld... .NET
Hoewel bovenstaande technologi¨en een grote bijdrage lever(d)en aan de huidige programmeertechnieken zijn ze voor de toekomst zo goed als ten dode opgeschreven! COM is een prachtige technologie maar heeft een paar gigantische nadelen:
ing. Gaillez Michael
9
1. Algemene begrippen
1. Elke COM-component heeft een unieke id in de registry van windows. Dit gaf aanleiding tot wat men noemt de DLL-Hell. Kort samengevat komt deze DLL-Hell er op neer dat je met COM in de problemen komt als je meer dan 1 versie van die component op een computer hebt draaien. Het simpelweg kopi¨eren van de nieuwe component volstaat niet! 2. COM is platform afhankelijk... Met de komst van .NET is een geheel nieuwe weg ingeslagen voor component development. Niet iedere component heeft een unieke id nodig. Je kan dit echter nog wel maar dan wordt hier wel rekening gehouden met versioning. Ook kan je door het uitschakelen van de unieke id uw nieuwe componenten en/of software dood´e´envoudig kopi¨eren en uitvoeren. Ook is .NET platform onafhankelijk dit betekent dat interactie tussen verschillende operating systemen gemakkelijker is geworden. Is .NET dan de heilige koe? Neen, vast en zeker NIET!!! Microsoft garandeert namelijk geen 100% backward en forward compatibility tussen de verschillende .NET versies. Dit heeft aanleidingen tot een volledig nieuwe hell. Je zal namelijk voor elke applicatie die je maakt moeten voorzien dat de juiste versie van het .NET framework op de client PC aanwezig is. Microsoft is namelijk niet van plan om alle frameworks standaard mee te nemen in de installatie van nieuwe windows systemen.
1.6
OPC
Vaak gebeurd het dat een PC-applicatie ontworpen is om data op te vragen uit ´e´en of andere databron (bv. een PLC). Voor elke applicatie moet er dan een telkens een driver ontworpen worden. Dit heeft echter een aantal grote nadelen: • we moeten vele keren het warm water uitvinden • conflicten tussen drivers • aanpassingen als er nieuwe hardware op de markt komt. • toegangsconflicten als meerdere applicaties gegevens willen ophalen uit dezelfde databron De oplossing werd in eerste instantie geboden door de fabrikanten zelf omdat die zelf hun drivers ter beschikking stelden. Dit leverde echter nog steeds een serieuse hindernis op om dat de verschillende applicaties andere protocollen gebruikten. Om alle voorgaande problemen grotendeels uit de wereld te helpen is men gekomen tot OPC of OLE for Process Control. OPC is gebaseerd op COM en DCOM en levert een set softwarecomponenten die de verbinding tussen hardware en business/office applicaties moet vereenvoudigen. OPC is gemaakt volgens het Client/Server-principe en dit betekent dat ook het probleem van de toegangsconflicten zo wordt opgelost. Er is namelijk maar ing. Gaillez Michael
10
1. Algemene begrippen
1 applicatie meer die de hardware aanspreekt, zijnde de OPC-Server. Alle applicaties spreken dan via de OPC-server de data aan uit de bron. Zo’n applicatie noemt men een OPC-Client. OPC heeft echter ook enkele grote nadelen: • De overall performance van OPC ligt stukken lager dan bij ActiveX of DLL. • OPC is gebaseerd op COM en om die als VB-programmeur te kunnen gebruiken moet je. beschikken over kant-en-klare interfaces (OPCClient) en die zijn vaak niet beschikbaar. • Kosten kunnen hoog oplopen voor licenties. • Klassieke OPC is niet echt geschikt voor .NET. De OPC-foundation heeft wel een OPC.NET versie uitgebracht maar dit zijn voorlopig enkel nog maar wrappers (API’s) omheen de COM componenten. Maar ook niet alles is slecht aan OPC! OPC is een grote stap vooruit in de standarisatie van de communcicatie tussen de verschillende componenten en kan is sommige gevallen een grote meerwaarde opleveren.
1.7
Samenvattig
In de loop der jaren zijn heel wat begrippen ge¨ıntroduceerd die in meer of mindere mate gerelateerd zijn met automatisering van processen in het algemeen. In grote lijnen zijn de krachttermen uit dit hoofdstuk integratie door: processautomatisatie, communicatie, monitoring, visualisatie, manipulatie en storage of archievering. In de volgende hoofdstukken zullen we elk van deze thema’s van naderbij belichten (niet in volgorde): • Processautomatisatie – Services • Communicatie – Sockets – RS232 – ... • Monitoring en visualisatie – Event logs – GDI+ – ... • Storage – Advanced Data-analyses • Manipulatie ing. Gaillez Michael
11
1. Algemene begrippen
– Web-based process control – ... Voor al deze topics zullen we de benadering in meer of mindere mate doen voor zowel Java als .NET.
ing. Gaillez Michael
12
Hoofdstuk 2
Sockets 2.1
Wat zijn sockets?
Het gebeurt wel vaker dat men data wil versturen over een netwerk (ofwel een privaat netwerk of het internet) en dat de bestaande protocollen niet echt volstaan. Je kan bijvoorbeeld geen resources downloaden door gebruik te maken van webservices of remoting. In deze situatie, is de beste oplossing om uw eigen protocol te ontwerpen door gebruik te maken van sockets. Definitie 2 Een socket is een software object die een applictie verbindt met een netwerk protocol en het peer-to-peer endpoint van netwerkcommunicatie vormt. Dit houdt in dat een programma TCP/IP-messages kan ontvangen en verzenden door een socket te openen en data te lezen en schrijven van of naar de socket. Een socket vergemakkelijkt programma ontwikkeling in die mate dat de programmeur enkel moet de socket kunnen manipuleren en voor de rest kan vertrouwen op het operating systeem om dan effectief de boodschappen correct te versturen over het netwerk. In hun meest basic vorm zijn alle netwerkprotocollen gebaseerd op sockets. Het principe ervan is heel ´e´envoudig: men creeert een poort op het ene uiteinde (Server) en men laat clients connecteren op deze poort op het andere uiteinde. Bijvoorbeeld HTTP werkt bijna altijd op poort 80. Dus een webserver opent een socket op poort 80 en wacht voor binnenkomende connecties. Webbrowsers maken dan een connectie op die poort 80 om een request te doen naar die webserver. Enkele bekende protocollen gebaseerd op sockets zijn: • HTTP(S) poort 80/81/443 • FTP poort 20/21 • NNTP poort 119 • SMTP poort 25 • ...
13
2. Sockets
Als je dus ´e´en van bovenstaande protocollen implementeerd met behulp van sockets kan je uw eigen webbrowser, webserver, ftp-client, ftp-server, newsreader, . . . gaan maken. Met behulp van sockets (en ook wel wat threading) en een zelfgebouwd protocol kunnen we om het even welke data versturen over een netwerk. Met ons eigen protocol hebben we volledige controle over de communicatie. Sockets werken op het client/server principe. Op de server hebben we een socket dat verbonden is met een welbepaalde poort. Op zich doet de serversocket niet veel meer dan wachten op een binnenkomende connectie van een client. Op de client weten we het IP-adres (rechtstreeks of onrechtstreeks via DNS) en de poort van de server. Als we dan connecteren met de server dan maakt de client een connectie verzoek op de poort van de server. In normale omstandigheden zal de server dan de connectie accepteren. Als de server de connectie accepteert dan zal hij een nieuwe socket creeren voor verdere communicatie. De oorspronkelijke socket heeft de server nog nodig om verder te kunnen luisteren naar binnenkomende verzoeken. Als de connectie succesvol is dan kunnen we nu de nieuwe socket op de server en de client socket gebruiken om data over en weer te sturen tussen client en server.
2.2
Sockets in .NET
In .NET zijn sockets geimplementeerd in de namespace System.Net.Sockets maar we maken hierbij ook vaak gebruik van de namespaces System.Net en System.IO voor de stream classes. Het .NET framework bevat een class voor het openen van sockets voor inkomende connecties System.Net.Sockets.TcpListener en een class voor de communicatie tussen 2 open sockets System.Net.Sockets.TcpClient. Zoals de naam al aangeeft zijn deze sockets specifiek voor TCP communicatie. Daarnaast zijn er ook nog 3 andere classes: UdpListener, UdpClient en Socket. De eerste 2 dienen voor communicatie via UDP te laten plaatsgrijpen ipv TCP. De raw Socket class is eigenlijk de meest algemene versie van de socket classes. Hiermee kan je client/server, TCP/UDP, . . . allemaal met ´e´en en dezelfde class. Maar de complexiteit van het gebruik van deze raw Socket class ligt iets hoger en is in onze applicaties nuttig als we willen werken met Asynchrone communcicatie. De Tcp/Udp-Listener/Client classes zijn eigenlijk een wrapper rondom de raw socket om het de programmeur gemakkelijker te maken. We gebruiken dan natuurlijk ook die functionaliteit want het is nogal zinloos om het wiel opnieuw uit te vinden maar in specifieke gevallen zullen we moeten gebruik maken van de raw Socket class.
2.2.1
IPAddress class
De IPAddress class uit de System.Net namespace stelt ons in staat om een IP-adres op te slaan. Dit IP-adres is afkomstig van een input string. De
ing. Gaillez Michael
14
2. Sockets
IPAddress class bevat een static/shared method Parse die een instantie aanmaakt van de IPAddress class op basis van een dotted IP-adres input string: // IPAddress based on IP - Address string IPAddress ipInfo = IPAddress . Parse ("192.168.0.1") ;
Listing 2.1: Creatie van een IPAddress-object op basis van IP input string Dikwijls worden we echter geconfronteerd met het feit dat een remote server niet word opgegeven door middel van zijn IP-adres maar door zijn domeinnaam of computer naam. Een andere situatie is als we het IP-adres van een computer wensen te weten, die zijn IP-adres heeft verkregen d.m.v DHCP. Hier komen de classes Dns en IPHostEntry uit de namespace System.Net goed van pas. Met de Dns class kunnen we IP en hostname informatie gaan opvragen met behulp van een aantal shared/static methods. De IPHostEntry class bevat dan weer een property AddressList en dit is een array van IPAddress objecten die horen bij een bepaalde hostname. // Getting IP - information by domainname IPHostEntry hostInfo = Dns . GetByHostName (" www . pih . be ") ←; foreach ( IPAddress ipInfo in hostInfo . AddressList ) { \ ldots } // Getting IP - information of a local computer string hostname = Dns . GetHostName () ; IPHostEntry hostInfo = Dns . GetByHostName ( hostname ) ; \ ldots
Listing 2.2: Creatie van een IPAddress-object op basis van de hostname
2.2.2
IPEndPoint
Voor socketcommunicatie hebben we niet voldoende aan het IP-adres van de server. We hebben ook nog het poortnummer nodig waarover de communicatie verloopt. Een IPEndPoint class bevat zowel het IP-adres als het poortnummer. We kunnen een IPEndPoint zelf opbouwen vanuit een IPAddress class en een poortnummer. // IPEndPoint based on IPAddress and portnumber IPAddress ipAdress = IPAddress . Parse ("192.168.0.1") IPEndPoint ipEP = new IPEndPoint ( ipAddress ,80)
Listing 2.3: Creatie van een IPEndPoint-object
ing. Gaillez Michael
15
2. Sockets
2.2.3
Synchonious vs. Asynchronious
Als we communicatie voeren dan kunnen we dat op 2 manieren gaan doen: synchroon of asynchroon. Bij synchrone communicatie kunnen we gedurende de communicatie niks anders meer doen in ons programma. Dus ´e´enmaal de communicatie gestart is dan blijft het programma enkel en alleen daar mee bezig tot het voltooid is. Bij asynchrone communicatie ligt dit iets anders. Daar gaan we door middel van een begin methode aangeven dat de communicatie moet starten en dan wordt de communicatie op de achtergrond uitgevoerd. Intussen kunnen we nog andere dingen gaan doen in ons programma. Als alle data ter beschikking is dan krijgen we een soort event (Callback) en kunnen we een End methode gaan oproepen of dan ook effectief de data te verwerken. Voor korte communicatie is synchrone communicatie uitermate geschikt omdat we meestal iets willen sturen en/of ontvangen en dit dan zo snel mogelijk verwerken. Maar in sommige gevallen kan het gebeuren dat de communicatie lang duurt, bvb een file transfer, dan is het niet echt wenselijk om zolang ons programma niks te laten doen. In zo’n gevallen kiezen we dan ook voor asynchrone communicatie.
2.2.4
TcpListener class
De TcpListener class is de server component van onze netwerkcommunicatie. In de constructor geef je het lokale IP-adres en poortnummer op waarop de socket moet luisteren voor binnenkomende connecties. De TcpListener class luistert enkel naar binnenkomende connecties en houdt zich NIET bezig met het sturen en ontvangen van data. Om dus data te kunnen versturen zal je op de server nog steeds een 2e socket nodig hebben (TcpClient). We kunnen door de methode Start van de TcpListener class aangeven dat de socket moet gaan wachten op binnenkomende connecties. In listing 2.4 zie je hoe je een TcpListener declareert in zowel C# als in VB.NET. De class bevat geen default constructor (dat is een constructor zonder argumenten) dus moeten we bij de initialisatie van onze socket steeds een argument meegeven. De mogelijke constructoren zijn: (int port): Hiermee geef je enkel een poortnummer mee bij de initialisatie. Deze constructor is echter obsolete dus je kan die beter niet meer gebruiken. (IPAddress localaddr, int port): Hier geef je zowel het IP-adres als het poortnummer op die de class moet gebruiken. (IPEndPoint localEP): Een IPEndPoint class met IP-adres en poortnummer. In C #: using System . Net . Sockets public class Server { private const int SERVICEPORT = 80;
ing. Gaillez Michael
16
2. Sockets
private string _ipAddress = "192.168.0.1"; private TcpListener _serverSocket ; // Default Constructor public Server () { IPAddress ipInfo = IPAddress . Parse ( _ipAddress ) ; _serverSocket = new TcpListener ( ipInfo , ←SERVICEPORT ) ; } } In VB . NET : Imports System . Net . Sockets Public Class Server Private Const SERVICEPORT As Integer = 80 Private _ipAddress As String = "192.168.0.1" Private _serverSocket As TcpListener ’ Default Constructor Public Sub New () Dim ipInfo As IPAddress = IPAddress . Parse ( ←_ipAddress ) _serverSocket = New TcpListener ( ipInfo , ←SERVICEPORT ) End Sub End Class
Listing 2.4: Declaratie van de TcpListener class Als een socket dan een connectie maakt met onze TcpListener dan wordt deze connectie in een queue geplaatst voor verdere afhandeling. Als we op de server nu de communicatie wensen verder af te handelen dan moeten we elke connectie in de queue ´e´en voor ´e´en gaan afhandelen. Er zijn 2 methodes om de verdere afhandeling mogelijk te maken: AcceptSocket: Hier krijgen we een raw data socket terug om de communicatie verder te kunnen voeren. AcceptTcpClient: Hier krijgen we een TcpClient object terug waar we dan verder mee kunnen gaan werken. Deze 2 methods zijn blocking methods. Bij blocking methods gaat de TcpListener class wachten tot er een connectie in de queue staat. Wanneer we met asynchrone communicatie willen werken is deze blocking niet gewenst want dit houdt in dat ons programma wacht tot er een connectie is en we intussen niks anders kunnen doen. Om toch asynchroon te kunnen werken is er nog een 3e methode voorzien nl. de Pending method. Deze Pending method keert een boolean terug die aangeeft of er al dan niet een connectie in de queue staat. ing. Gaillez Michael
17
2. Sockets
De laatste methode die van belang is bij de TcpListener class is de Stop method. Als we de Stop methode aanroepen dan worden geen nieuwe connecties meer aanvaard. In queue zullen alle inkomende connecties gezet worden zolang de Stop methode niet is aangeroepen of tot het maximum aantal connecties is bereikt. Het maximaal aantal connecties wordt bepaald door de MaxConnections property van de class. Belangrijk is dat de Stop methode reeds aanvaarde connecties niet sluit! Je bent dus zelf verantwoordelijk om die verder af te handelen en af te sluiten.
2.2.5
TcpClient class
De TcpClient class is de component die verantwoordelijk is voor het versturen/ontvangen van data tussen 2 endpoints of om te connecteren op een TcpListener class. Om de connecteren op een TcpListener zijn er verschillende mogelijkheden. Je kan de connectie gegevens meegeven in de constructor van de TcpClient class of je kan de connect methode aanroepen van een TcpClient object. Om een TcpClient object aan te maken kunnen we gebruik maken van de volgende constructors: Default: Geen argumenten dus connectie maken via connect method (IPEndPoint hostEP): Er word automatisch connectie gemaakt met het IP-adres en poortnummer uit de IPEndPoint (string hostname, int port) Automatische connectie met socket op die hostname en port. Er zijn nog meer mogelijkheden maar dit zijn de belangrijkste voor onze toepassingen. Je kan ook een connectie maken via de Connect method. De Connect method is overloaded dit betekent dat we ook hier een paar verschillende mogelijkheden hebben: • Connect(IPEndPoint hostEP) • Connect(IPAddress ipInfo,int port) • Connect(string hostname, int port) Ook hier zijn er nog meer mogelijkheden voor handen maar we beperken ons opnieuw tot de meest relevante. Listing 2.5 toont de initialisatie van een TcpClient object met enerzijds de constructor (Client1) en anderzijds met de Connect method (Client2) In C #: using System . Net . Sockets public class Client1 { private const int SERVICEPORT = 80;
ing. Gaillez Michael
18
2. Sockets
private string _ipAddress = "192.168.0.1"; private TcpClient _clientSocket ; // Default Constructor public Client1 { IPAddress ipInfo = IPAddress . Parse ( _ipAddress ) ; IPEndPoint hostEP = new IPEndPoint ( ipInfo , ←SERVICEPORT ) ; _clientSocket = new TcpClient ( hostEP ) ; } } public class Client2 { private const int SERVICEPORT = 80; private TcpClient _clientSocket ; // Default Constructor public Client2 { _clientSocket = new TcpClient () ; _clientSocket . Connect (" www . pih . be " , SERVICEPORT ) ; } } In VB . NET : Imports System . Net . Sockets Public Class Client1 Private Const SERVICEPORT As Integer = 80 Private _ipAddress As String = "192.168.0.1" Private _clientSocket As TcpClient ’ Default Constructor Public Sub New () Dim ipInfo As IPAddress = IPAddress . Parse ( ←_ipAddress ) _clientSocket = New TcpClient ( ipInfo , SERVICEPORT ) End Sub End Class Public Class Client2 Private Const SERVICEPORT As Integer = 80 Private _clientSocket As TcpClient // Default Constructor Public Sub New () _clientSocket = new TcpClient () _clientSocket . Connect (" www . pih . be " , SERVICEPORT ) End Sub
ing. Gaillez Michael
19
2. Sockets
End Class
Listing 2.5: Declaratie van TcpClient Om data te versturen/ontvangen met de TcpClient socket maken we gebruik van de GetSream method. Deze methode keert een stream object terug en meer bepaald een NetworkStream object. Deze NetworkStream class beschikt over een Read en Write method om data te ontvangen en versturen. De data die we versturen via deze methode is altijd een byte-array. Afhankelijk van het type data dat we dus willen gaan versturen gaan we nog conversies moeten uitvoeren. Om bvb. een string om te zetten naar een byte-aaray maken we gebruik van de Encoding class uit de System.Text namespace. Een voorbeeld hiervan vind je in listing 2.6. // Conversie van string naar een byte - array Byte [] stringInBytes = System . Text . Encoding . ASCII . ←GetBytes (" String die we willen omzetten in bytes ") ; // Conversie van de byte - array naar een string string originalText = System . Text . Encoding . ASCII . ←GetString ( stringInBytes , 0 , stringInBytes . Length ) ;
Listing 2.6: Conversie van string naar byte-array en omgekeerd Het feit dat we data steeds met een byte-array versturen brengt een kleine complicatie met zich mee. Het probleem is het volgende: als we de data versturen weten we perfect hoeveel data we gaan versturen aan de zijde van de zender. Maar aan de kant van de ontvanger weten we niet op voorhand hoeveel bytes er gaan binnen komen. Bij de creatie van een nieuwe bytearray moeten we echter de grootte van de array defini¨eren. Het feit dat we aan de zijde van de ontvanger nog niet weten hoeveel bytes er gaan komen is het dus moeilijk om in te schatten hoe groot deze array moet zijn. In dat geval gaan we een kleine byte array aanmaken (buffer) en de data in meerdere stappen binnenlezen tot alle data verwerkt is. Met behulp van stream classes kunnen we dan gemakkelijk deze aparte buffers terug aan elkaar kleven tot 1 geheel. Als alle transacties voltooid zijn kunnen we de connectie afsluiten door middel van de Close method.
2.2.6
TcpListener en TcpClient toegepast
Om het gebruik van TcpListener en TcpClient te demonstreren gaan we eenvoudige toepassing bouwen. We gaan beginnen met de server-side en daarna gaan we de client-side uitwerken. Om de server-side uit te werken moeten we een TcpListener class implementeren die luistert naar binnenkomende connecties. Daarna wordt de data ingelezen die komt van de client en naar het scherm geschreven. Als de data ontvangen is moet er een antwoord terug gestuurd worden naar de client.
ing. Gaillez Michael
20
2. Sockets
Using System . Net ; Using System . Net . Sockets ; public class Server { private const int SERVICEPORT = 10000; // Main methode wordt aangeroepen bij het starten van het programma [ StatThread () ] public void Main ( string args []) {
←-
// Creatie van IP - adres string ipAddress = "192.168.0.1"; IPAddress ipInfo = IPAddress . Parse ( ipAddress ) ; // Creatie van het TcpListener object TcpListener serverSocket = new TcpListener ( ipInfo , ←SERVICEPORT ) ; // Begin met wachten op connecties serverSocket . Start () ; // Een connectie van de queue ophalen TcpClient connection = serverSocket . ←AcceptTcpClient () ; // Data inlezen die van de client komt en naar het ←scherm schrijven NetworkStream nwstream = connection . GetStream () ; Byte [] receivedBytes = new Byte [256]; int numberOfBytes = nwstream . Read ( receivedBytes , ←0 , receivedBytes . Length ) ; String receivedString = System . Text . Encoding . ASCII ←. GetString ( receivedBytes , 0 , numberOfBytes ) ; Console . WriteLine ( receivedString ) ; // Een antwoord terug sturen naar de client string response = " Data received OK !"; Byte [] sendBytes = System . Text . Encoding . ASCII . ←GetBytes ( response ) ; nwstream . Write ( sendBytes , 0 , sendBytes . Length ) ; // Einde van de transactie dus server afsluiten serverSocket . Stop () ; } }
Listing 2.7: Een eenvoudige server socket applicatie Om de client-side van onze applicatie uit te werken maken we gebruik van onze TcpClient class om te connecteren op onze server. Daarna sturen we data naar de server en wachten op een antwoord.
ing. Gaillez Michael
21
2. Sockets
using System . Net ; using System . Net . Sockets ; public class Client { private const int SERVICEPORT = 10000; [ StatThread () ] public void Main ( string args []) { // Creatie van IP - adres string ipAddress = "192.168.0.1"; IPAddress ipInfo = IPAddress . Parse ( ipAddress ) ; // Creatie van het TcpClient object TcpClient client = new TcpClient () ; client . Connect ( ipInfo , SERVICEPORT ) ; NetworkStream nwstream = client . GetStream () ; Byte [] sendBytes = System . Text . Encoding . ASCII . ←GetBytes (" Hello World !") ; nwstream . Write ( sendBytes , 0 , sendBytes . Length ) ; Byte [] receivedBytes = new Byte [256]; int numberOfBytes = nwstream . Read ( receivedBytes , ←0 , receivedBytes . Length ) ; string serverresponse = System . Text . Encoding . ASCII ←. GetString ( receivedBytes , 0 , numberOfBytes ) ; Console . WriteLine ( serverresponse ) ; client . Close () ; } }
Listing 2.8: Een eenvoudige client socket applicatie
2.3
Sockets in Java
Sockets in Java zijn in vele oogpunten gelijkaardig aan die in .NET. Er is een licht verschil in syntax maar fundamenteel komt het op hetzelfde neer. Hier zijn ook geen standaard classes zoals TcpListener en TcpClient voor handen dus zullen we alles moeten doen met socket classes die voor handen zijn. De socket class in Java is terug te vinden in het java.net package. Deze socket class is vergelijkbaar met onze TcpClient class uit .NET in dat opzicht dat ze verantwoordelijk is voor het versturen en ontvangen van data. Daarnaast is er nog de ServerSocket class die op zijn beurt gelijkt op de TcpListener class uit .NET. De ServerSocket class wacht eveneens op binnenkomende connecties om die dan verder af te handelen.
ing. Gaillez Michael
22
2. Sockets
2.3.1
InetAddress class
De InetAddress class komt overeen met de IPAddress class uit .NET. Het gebruik ervan is volledig gelijkaardig nl. we kunnen door middel van static/shared methods vanuit een hostname of een IP-adres string een InetAddress object aanmaken. We vinden de InetAddress class terug in de java.net package: InetAddress ipInfo = InetAddress . getByName ←("192.168.0.1") ; of InetAddress ipInfo = InetAddress . getByName (" www . pih . be ←") ;
Listing 2.9: Een InetAddress object creeren.
2.3.2
InetSocketAddress class
De InetSocketAddress kunnen we op zijn beurt weer gaan vergelijken met IPEndPoint class uit .NET. De InetSocketAddress class heeft 3 constructoren: (int port): Hier wordt een Socket Address object gemaakt op bassi van het poortnummer en als IP-adres neemt hij een lokaal IP-adres van de machine waarop hij draait. (InetAddres addr, int port): Maakt een SocketAddress op basis van een IP-adres en een poort. (String hostname, int port): Maakt een SocketAddress object met hostname en poort. De hostname zal omgezet worden in een InetAddress object dus we kunnen hier een computernaam of een IP-adres string opgeven.
2.3.3
ServerSocket class
De ServerSocket class is volledig vergelijkbaar met de TcpListener class uit .NET. Deze class gaat ook niks anders gaan doen dan gaan wachten op binnenkomnde connecties voor verdere afhandeling. De ServerSocket class heeft 4 constructoren: (): De default constructor. Dit creert een ServerSocket object dat unbound is. We kunnen de socket actheraf nog bound maken met de Bind method (int port): Maakt een ServerSocket object op het lokale IP en het wordt automatisch gebind aan de opgegeven poort.
ing. Gaillez Michael
23
2. Sockets
(int port, int backlog): Maakt een ServerSocket object op het lokale IP en het wordt automatisch gebind aan de opgegeven poort. Backlog bepaalt het maximaal aantal connections dat mogelijk is (cfr. MaxConnections in .NET) (int port, int backlog, InetAddress bindAddr): Maakt een ServerSocket object dat bound is op het opgegeven IP-adres en poort. Backlog is opnieuw het maximum aantal connecties. We merken op dat de ServerSocket class geen Start method heeft zoals in .NET. Dit betekent dat de socket onmiddellijk begint te luisteren voor inkomende connectie van zodra hij bound is. In dit geval zal enkel bij de default constructor nog verdere actie nodig zijn om de ServerSocket te laten wachten op connecties. Alle andere constructoren zijn onmiddellijk bound vanaf de creatie. Om nu een object gemaakt met de default constructor bound te maken, gebruiken we de Bind method. Deze bind method heeft 2 overloaded versies: bind(SocketAddress endpoint): Maakt de socket bound op een IP-adres en poort. bind(SocketAddress endpoint, int backlog): Maakt de socket bound op een IP-adres en poort en stelt het maximum aantal connecties in. Als we in de java documentation zouden kijken dan zouden we zien dat SocketAddress een abstract class is. Dit wil zeggen dat we enkel kunnen erven van deze class en er geen instantie kunnen van aanmaken. De InetSocketAddress erft van deze SocketAddress class dus kunnen we hier voor endpoint een InetSocketAddress object meegeven. Om een connectie van de queue verder af te handelen maken we gebruik van de accept method. Deze accept method keert dan een Socket class terug en die kunnen we dan gaan gebruiken voor het verzenden en ontvangen. Net als bij de Accept-methods in .NET is deze method blocking dat wil zeggen dat ze een connectie ophaalt uit de queue en indien er geen is wacht tot er 1 is. Tenslotte is er ook een close method die de huidige ServerSocket afsluit.
2.3.4
Socket class
De Socket class is de evenknie van de TcpClient class in .NET. Deze class wordt dus gebruikt om te connecteren op een ServerSocket of om data over en weer te sturen tussen client en server. Volgende constructoren zijn beschikbaar om een Socket object aan te maken (): Default. Maakt een niet geconnecteerd Socket object aan. We moeten dus nog verdere stappen ondernemen om de connecteren en data te kunnen versturen. (InetAddress addr, int port): Maakt een Socket object en legt een connectie op het opgegeven IP-adres en poortnummer.
ing. Gaillez Michael
24
2. Sockets
(String host, int port): Maakt een Socket object en legt een connectie met de host die overeenkomt met de waarde uit de string en het opgegeven poortnummer. Er zijn nog meer constructoren beschikbaar en je kan die terug vinden in de Java API maar voor onze toepassingen zijn dit de belangrijkste. Als we een Socket aanmaken door middel van de default constructor dan moeten we nog een connectie leggen met een ServerSocket. Dit kunnen we doen door middel van de connect method: connect(SocketAddress endpoint): Maakt een verbinding naar het opgegeven SocketAddress. We kunnen ook hier geen SocketAddress object gebruiken om dat deze class abstract(MustInherit) is. Dus net als bij de ServerSocket gaan we hier een InetSocketAddress meegeven. Als we een connectie hebben dan kunnen we data gaan verzenden en ontvangen. Dit kunnen we gaan doen met de methoden getOutputStream en getInputStream. We krijgen dan repectievelijk een OutputStream of een InputStream object terug waarmee we dan effectief data kunnen gaan zenden en ontvangen. // Send some data String sendMessage = " Sending some data ..."; byte [] messageInBytes = sendMessage . getBytes () ; OutputStream streamOut = socket . getOutputStream () ; streamOut . write ( messageInBytes , 0 , messageInBytes . ←length ) ; // Receiving some data byte [] receivedBytes = new byte [256]; InputStream streamIn = socket . getInputStream () ; int numberOfBytes = streamIn . Read ( receivedBytes , 0 , ←receivedBytes . length ) ; String receivedMessage = new String ( receivedBytes , 0 , ←numberOfBytes ) ;
Listing 2.10: Datatransport met InputStream en OutputStream Tenslotte hebben we ook hier een close method om de socket af te sluiten.
2.4 2.4.1
Een protocol implementeren Wat is een protocol
In zijn meest eenvoudig vorm kunnen we stellen dat een protocol niets meer en niets minder is dan een set afspraken. In het geval van netwerkcommunicatie bepaalt het protocol hoe client en server met elkaar gaan communiceren. De meeste protocollen zoals we die kennen zijn vastgelegd in zogenaamde RFC’s (Request For Command).
ing. Gaillez Michael
25
Hoofdstuk 3
Delegates en events in .NET 3.1
Inleiding
In programmeren geven we vaak informatie door van het ene object naar het andere. We doen dit door properties of methods aan te roepen van het ene object en de informatie al dan niet op te vangen in of door te geven aan het andere object en vice versa. Het gevolg hiervan is dat de objecten heel sterk met elkaar gerelateerd zijn. We moeten dus op voorhand weten welke informatie aan welk object willen doorgeven en hoe. Dit noemen we tight coupling. In de praktijk is het echter dikwijls niet zo eenvoudig. Soms weten we wel hoe we informatie willen doorgeven maar nog niet aan wat. Neem het voorbeeld: in een bedrijf zijn is er een werknemer en zijn baas. De baas wil weten wanneer de werknemer aan een taak begonnen is en eveneens wanneer hij er mee klaar is. In tight coupling kunnen we het baasobject door geven aan het werknemer-object. Het werknemer-object kan dan properties en methodes van het baas-object gebruiken om zijn status te laten weten. Maar wat als de baas niet meer geinteresseerd is in wanneer de werknemer aan iets begonnen is en enkel nog wil verwittigd worden als de werknemer klaar is. Dan kunnen we de werknemer gaan herprogrammeren. En het kan nog erger wat als er nog een collega bij komt die ook wil weten wanneer de werknemer klaar is. Dan moeten we al een een baas-object EN een collega-object meegeven aan het werknemer object. En we kunnen zo blijven doorgaan met een klant, leverancier, etc. Je ziet al snel dat heel ingewikkeld kan worden en dat je het bos door de bomen niet meer ziet na verloop van tijd om nog maar te zwijgen van het aanpaswerk aan de werknemerclass. Zou het niet gemakkelijker zijn als de werknemer laat weten wanneer hij begint en wanneer hij klaar is aan ’iedereen’ en dat dan de personen die geinteresseerd zijn erop kunnen reageren als ze dat wensen. We kunnen dit doen door gebruik te maken van delegates en events. Om dit principe van delegates en events uit te leggen gaan we vertrekken van ons tightly coupled baas-werknemer voorbeeld en stapsgewijs de begrippen delegate en events invoeren.
26
3. Delegates en events in .NET
3.2
Baas-Werknemer Tightly Coupled
In ons voorbeeld willen we de baas verwittigen als we een werkje begonnen zijn, als we bezig zijn en als we klaar zijn. We gaan dus een Baas-class maken met 3 methodes: WerkGestart, WerkBezig, WerkKlaar. Maak hiervoor een nieuw console project aan in visual studio en voor een class Baas toe. public class Baas { public void WerkGestart () { Console . WriteLine (" Baas zegt : hmmmm , I don ’ t care ←...") ; } public void WerkBezig () { Console . WriteLine (" Baas zegt : nog bezig ? hmmmm , I don ’ t care ...") ; }
←-
public void WerkKlaar () { Console . WriteLine (" Baas zegt : Dat werd tijd !!!") ; } }
Listing 3.1: Baas tightly coupled Daarnaast hebben we een werknemer die de baas moet verwittigen bij aanvang, als hij bezig is en als hij klaar is. We moeten dus een Werknemer-class maken die een methode VoerWerkUit heeft en die in die methode de status doorgeeft aan de baas. We voegen dus een class Werknemer toe aan ons project als volgt: public class Werknemer { private Baas _baas ; public Werknemer ( Baas baas ) { _baas = baas ; } public void VoerWerkUit () { Console . WriteLine (" Werknemer zegt : Ik begin aan de ←opdracht .") ; // Verwittig de baas _baas . WerkGestart () ; Console . WriteLine (" Werknemer zegt : Ik ben bezig aan de opdracht .") ; // Verwittig de baas
ing. Gaillez Michael
←-
27
3. Delegates en events in .NET
_baas . WerkBezig () ; Console . WriteLine (" Werknemer zegt : Ik ben klaar ←!!!") ; // Verwittig de baas _baas . WerkKlaar () ; } }
Listing 3.2: Werknemer tightly coupled Tot slot hebben we nog een class nodig om de interactie tussen de baas en werknemer te testen. Voeg hiervoor een class Bedrijf toe en implementeer het volgende: public class Bedrijf { public static void Main ( string [] args ) { // C r e e r een baas object Baas baas = new Baas () ; // C r e e r een werknemer object en geef de baas mee ←om die te verwittigen . Werknemer werker = new Werknemer ( baas ) ; // Laat het werknemer object het werk uitvoeren . werker . VoerWerkUit () ; // Wachten op een enter invoer van de gebruiker zodat de output zichtbaar blijft Console . ReadLine () ;
←-
} }
Listing 3.3: Bedrijf tightly coupled Als we het programma compileren en runnen dan ziet de output er logischer wijze als volgt uit: Werknemer zegt : Ik begin aan de opdracht . Baas zegt : hmmmm , I don ’ t care ... Werknemer zegt : Ik ben bezig aan de opdracht . Baas zegt : nog bezig ? hmmmm , I don ’ t care ... Werknemer zegt : Ik ben klaar !!! Baas zegt : Dat werd tijd !!!
3.3
Baas-Werknemer met interfaces
Om het nadeel van ons tightly coupled concept te illustreren gaan we het volgende uitvoeren. Maak de huidige solution een nieuw class-library project aan. Verplaats het werknemer object van het console project naar het classlibrary project. Als we dit gaan compileren dan krijgen we een error in
ing. Gaillez Michael
28
3. Delegates en events in .NET
het console project dat hij de Werknemer class niet vindt. Geen probleem zou je denken. We voegen een reference toe aan het console project naar onze class library, importeren de namespace en alles werkt terug. Als we dit echter uitvoeren dan zien we dat dit niet alles oplost. Want de class library kan dan weer niet compileren omdat dit de Baas-class niet vindt. Een eerste reactie zou kunnen zijn, we voegen het een referentie toe aan het class-library project naar het console project. Als je dit zou proberen dan ga je merken dat .NET dit niet toelaat met andere woorden je kan dus geen dubbele referentie leggen tussen 2 projecten. Interfaces kunnen hiervoor een oplossing bieden. In ons class-library project maken we dus een nieuwe file met de volgende interface: public { void void void }
interface IRapporteer WerkGestart () ; WerkBezig () ; WerkKlaar () ;
Listing 3.4: IRapporteer interface Deze interface definieert dus dat de classes die het implementeren dat ze de methodes WerkGestart, WerkBezig en Werkklaar moeten implementeren. We kunnen nu het Baas object in onze Werknemer vervangen door ¨een¨object van het type IRapporteer. De class Werknemer wordt dus: public class Werknemer { private IRapporteer _ ra ppo rt ee rP ers oo n ; public Werknemer ( IRapporteer r apporte erPerso on ) { _rapporteerPersoon = rap porteer Persoo n ; } public void VoerWerkUit () { Console . WriteLine (" Werknemer zegt : Ik begin aan de ←opdracht .") ; // Verwittig de persoon _rapporteerPersoon . WerkGestart () ; Console . WriteLine (" Werknemer zegt : Ik ben bezig aan de opdracht .") ; // Verwittig de persoon _rapporteerPersoon . WerkBezig () ;
←-
Console . WriteLine (" Werknemer zegt : Ik ben klaar ←!!!") ; // Verwittig de persoon _rapporteerPersoon . WerkKlaar () ;
ing. Gaillez Michael
29
3. Delegates en events in .NET
} }
Listing 3.5: Werknemer met interfaces Als we nu het geheel compileren dan merken we dat we geen Baas-object meer kunnen meegeven aan de Werknemer constructor. Dit komt omdat de baas-class niet van het type IRapporteer is. We moeten dus ook de Baas class gaan aanpassen zodat deze hieraan voldoet. // Implementeer de interface public class Baas : IRapporteer { public void WerkGestart () { Console . WriteLine (" Baas zegt : hmmmm , I don ’ t care ←...") ; } public void WerkBezig () { Console . WriteLine (" Baas zegt : nog bezig ? hmmmm , I don ’ t care ...") ; }
←-
public void WerkKlaar () { Console . WriteLine (" Baas zegt : Dat werd tijd !!!") ; } }
Listing 3.6: Baas met interfaces Aangezien de Baas-class de 3 methodes uit de interface implementeert moeten we dus verder geen stappen meer ondernemen en als we dit uitvoeren dan zien we dat we terug de uitvoer als voorheen krijgen. Door gebruik te maken van interfaces kunnen we nu ook al aan andere classes gaan rapporteren. We kunnen nu in het console project ook een class Collega maken en die de interface IRapporteer laten implementeren. We kunnen nu in plaats van een Baas-object ook een Collega-object meegeven aan de werknemer om aan hem te rapporteren. Hoewel dit al een mooie oplossing lijkt voor ons probleem, toch is het nog niet ideaal. In het voorbeeld blijkt dat de baas eigenlijk niet echt geinteresseerd is in de eerste 2 stappen en enkel in de laatste stap interesse vertoont. Omdat we gebruik maken van de interface IRapporteer zijn we echter wel verplicht om de drie functies in de Baas class te implementeren, we kunnen die dus met andere woorden niet zomaar weg laten. Dit is dus nog geen ideale oplossing. Als we nog meer vrijheid willen dan moeten we gebruik gaan maken van delegates.
ing. Gaillez Michael
30
3. Delegates en events in .NET
3.4
Baas-Werknemer en Delegates
We hebben in .NET al veel gebruik gemaakt van referenties objecten. Telkens we een variabele aanmaken leggen we in onze applicaties een referentie naar iets, een object. Die referentie gebruiken we dan om dit object te manipuleren. Wat we nu willen is een referentie naar een method in plaats van naar een object. Dus als het ware een variabele om een method in op te slaan. De class in .NET die ons hiervoor de mogelijkheid heeft is een delegate. Een delegate is dus een manier om te verwijzen naar een method. Voor iemand die nog in C of C++ gewerkt heeft kan je een delegate vergelijken met een function-pointer. In C-Sharp declareren we een delegate als volgt: public delegate < return type > Naam ( < argument > , < ←argument > , ...) ;
Als we dit toepassen krijgen we: public delegate void Functie1 ( string s , int i ) ; public delegate int Functie2 () ;
We zien dus dat een delegate niets anders is dan omschrijven hoe een functie er moet uit zien, net zoals bij interfaces. Een delegate is dus niet genteresseerd in wat de functie doet maar enkel in de declaratie. Als we nu een delegate praktisch willen gebruiken dan moeten we aan de delegate ook daadwerkelijk een functie meegeven die voldoet aan de voorwaarden van de declaratie van de delegate: public delegate void EenMethod ( string s , int i ) ; public void MijnMethod ( string s , int i ) { Console . WriteLine (" Output : " + s + " " + i ) ; }
We hebben nu een delegate gedeclareerd en een method die voldoet die declaratie. We hebben nu wel nog geen enkele link tussen de delegate en de method. Als we dit willen zouden we het volgende kunnen doen: public static void Main ( string [] args ) { // Declareer een referentie van onze delegate EenMethod methodReferentie ; // Wijs de method MijnMethod toe aan de ←gedeclareerde delegate methodReferentie = new EenMethod ( MijnMethod ) ; }
De variabele methodReferentie bevat nu een verwijzing naar MijnMethod. Als we nu met onze delegate de methode MijnMethod willen aanroepen
ing. Gaillez Michael
31
3. Delegates en events in .NET
moeten we enkel nog het volgende toevoegen. public static void Main ( string [] args ) { // Declareer een referentie van onze delegate EenMethod methodReferentie ; // Wijs de method MijnMethod toe aan de ←gedeclareerde delegate methodReferentie = new EenMethod ( MijnMethod ) ; String str = " tekst "; int getal = 123; // Voer uit methodReferentie ( tekst , getal ) ; }
We hebben dus de method MijnMethod uitgevoerd door gebruikt te maken van onze delegate. Laat ons dit nu eens toepassen op ons voorbeeld. BaasWerknemer met delegates dus. . . Pas de werknemer class aan zodat ze eruit ziet als hieronder: public delegate void StartWerk () ; public delegate void DoeWerk () ; public delegate void EindeWerk () ; public class Werknemer { public StartWerk start ; public DoeWerk doe ; public EindeWerk einde ; public void VoerWerkUit () { Console . WriteLine (" Werknemer zegt : Ik begin aan de ←opdracht .") ; // Verwittig iets ... if ( start != null ) start () ;
Console . WriteLine (" Werknemer zegt : Ik ben bezig aan de opdracht .") ; // Verwittig iets ... if ( doe != null ) doe () ;
←-
Console . WriteLine (" Werknemer zegt : Ik ben klaar ←!!!") ; // Verwittig iets if ( einde != null ) einde () ; } }
ing. Gaillez Michael
32
3. Delegates en events in .NET
Het eerste wat ons opvalt hierbij is dat we helemaal geen verwijzing meer nodig hebben naar ons Baas object. Dus Werknemer kan dus gaan verwittigen aan iets zonder te hoeven weten wat dat iets is. Het 2e wat opvalt is dat we controleren of onze delegate geen null bevat. Herinner u een delegate is een referentie naar een method net als een variabele een referentie naar een object is. Een delegate verschilt hierin helemaal niet van een gewone variable dus we moeten controleren of er wel degelijk iets in zit, vooraleer we er effectief iets mee gaan doen. Als de waarde geen null bevat dat is er een method aan gekoppeld en kunnen we die bijgevolg gaan aanroepen (let op de haakjes!). We moeten nu nog de rest van de code gaan aanpassen zodat deze ook gebruik maakt van de delegates. In de Baas-class verwijderen we gewoon weer de IRapporteer: public class Baas { public void WerkGestart () { Console . WriteLine (" Baas zegt : hmmmm , I don ’ t care ←...") ; } public void WerkBezig () { Console . WriteLine (" Baas zegt : nog bezig ? hmmmm , I don ’ t care ...") ; }
←-
public void WerkKlaar () { Console . WriteLine (" Baas zegt : Dat werd tijd !!!") ; } }
In onze main method moeten we nu ook nog n en ander toevoegen zodat baas en werknemer terug gelinkt zijn. public class Bedrijf { public static void Main ( string [] args ) { // C r e e r een baas object Baas baas = new Baas () ; // C r e e r een werknemer object // Geen Baas object meer nodig bij createie Werknemer werker = new Werknemer () ; // Laat werker de baas verwittigen werker . start = new StartWerk ( baas . WerkGestart ) ; werker . doe = new DoeWerk ( baas . WerkBezig ) ; werker . einde = new EindeWerk ( baas . WerkKlaar ) ;
ing. Gaillez Michael
33
3. Delegates en events in .NET
// Laat het werknemer object het werk uitvoeren . werker . VoerWerkUit () ; // Wachten op een enter invoer van de gebruiker zodat de output zichtbaar blijft Console . ReadLine () ;
←-
} }
We zien hier duidelijk dat we geen baas object meer hoeven mee te geven aan de werknemer. Dus Baas en Werknemer staan volledig los van elkaar. We noemen dit dan ook loosely coupled! Tenslotte merken we ook op dat we methodes van het baas object meegeven aan de werker (let opnieuw op het gebruik van de haakjes!). Als we dit uitvoeren krijgen we opnieuw hetzelfde resultaat als voorheen. Wat is nu de meerwaarde??? Zoals al eerder gezegd lijkt de baas alleen genteresseerd in wanneer het werk klaar is. We kunnen dit nu heel gemakkelijk doen door de lijnen werker.start = . . . en werker.doe = . . . in commentaar te zetten. Als we dat uitvoeren zien we dat de baas enkel nog reageert wanneer het werk klaar is. We kunnen ook meerdere methodes koppelen aan ´e´en delegate door gebruik te maken van +=. Als we een methode willen verwijderen van een delegate dan kunnen we ofwel de delegate op null plaatsen of we kunnen gebruik maken van -= operator. De code kan er dan als volgt uit zien: public class Bedrijf { public static void Main ( string [] args ) { // C r e e r de baas objecten Baas baas1 = new Baas () ; Baas baas2 = new Baas () ; // C r e e r een werknemer object // Geen Baas object meer nodig bij createie Werknemer werker = new Werknemer () ; // Laat werker de bazen verwittigen werker . start += new StartWerk ( baas1 . WerkGestart ) ; werker . start += new StartWerk ( baas2 . WerkGestart ) ; werker . doe += new DoeWerk ( baas . WerkBezig ) ; werker . einde += new EindeWerk ( baas . WerkKlaar ) ; // Laat het werknemer object het werk uitvoeren . werker . VoerWerkUit () ; // Verwijder baas2 terug werker . start -= new StartWerk ( baas2 . WerkGestart ) ; // Laat de werknemer het werk opnieuw uitvoeren werker . VoerWerkUit () ; // Wachten op een enter invoer van de gebruiker zodat de output zichtbaar blijft
ing. Gaillez Michael
←-
34
3. Delegates en events in .NET
Console . ReadLine () ; } }
We hebben al een groot stuk flexibiliteit ingevoerd in ons voorbeeld programma tot nu toe. Er rest eigenlijk maar ´e´en probleem meer dat we moeten oplossen. Delegates zijn al een hele vooruitgang maar ze bieden maar weinig bescherming. We kunnen als de delegate public is, van buitenaf alle methodes die zijn toegevoegd simpelweg verwijderen door de delegate op null te plaatsen. Dit gedrag is niet echt wenselijk. Bovendien als een delegate public is kunnen we deze delegate ook uitvoeren van buiten de huidige class. Dus principieel zou je dit kunnen doen: public class Bedrijf { public static void Main ( string [] args ) { // C r e e r een werknemer object Werknemer werker = new Werknemer () ; werker . start () ; } }
Dit gedrag is in bepaalde gevallen al evenzeer niet gewenst. Wat we willen bereiken is, dat we de ’buitenwereld’ kunnen verwittigen van een welbepaalde gebeurtenis en dat de buitenwereld zich (indien dat nodig is) hierop kan inschrijven. Maar we willen niet dat iets uit de buitenwereld dit kan manipuleren of veranderen. Dit is waar events hun nut bewijzen.
3.5
Baas-Werknemer the final story. . . Events
Als events de ultieme oplossing zijn waarom hebben we ze dan niet onmiddellijk gebruikt? Wel. . . we hadden interfaces nodig om tot delegates te komen. En om events te gebruiken hebben we delegates nodig. Events en delegates gaan namelijk hand in hand. Een event is zoals het woord het zegt een gebeurtenis. Aan een event kunnen we nu door middel van delegates meerdere methodes koppelen. Door dan het event af te vuren kunnen we al deze methodes laten uitvoeren. Maar we eerst eens kijken naar de declaratie van een event: public event < delegate > EventNaam ;
Praktisch toegepast levert dit: public delegate void MijnEventHandler () ; public class MijnClass {
ing. Gaillez Michael
35
3. Delegates en events in .NET
public event MijnEventHandler MijnGebeurtenis ; }
Zoals je ziet lijkt dit zeer sterk op de declaratie van een delegate maar we voegen het keyword event toe. In de praktijk zal men voor de delegate die hoort bij een event meestal een naam kiezen die eindigt op xxxEventHandler. Dit geeft duidelijk aan dat het gaat om een delegate die gekoppeld is aan een of ander event. Daarnaast wordt ook meestal het eerste stuk van de naam dezelfde als de naam van het event zelf, het is geen noodzaak maar het maakt uw code wel beter leesbaar dus doen. . . : public delegate void ClickE ventHan dler () ; public event ClickEventHa ndler Click ;
Het afvuren van een event is zo volledig analoog aan het oproepen van een delegate: if ( MijnGebeurtenis != null ) MijnGebeurtenis () ;
De code hieronder toont nu hoe ik een methode(s) aan een event kan koppelen: public static void Main ( string [] args ) { MijnClass o = new MijnClass () ; o . MijnGebeurtenis += new MijnEventHandler ( DoeIets ) ; o . MijnGebeurtenis += new MijnEventHandler ( DoeNogIets ←); } private void DoeIets () { Console . WriteLine ("...") ; } private void DoeNogIets () { Console . WriteLine (" nog eens ...") ; }
Merk hierbij op dat we gebruik maken van += om een methode toe tevoegen. Dit moet ook zo want van buiten de class kan je de = operator niet gebruiken. Om een methode terug los te koppelen in code doe je dit volledig gelijkaardig als bij delegates nl. met de -= operator. Dit kan handig zijn als je slechts tijdelijk op een welbepaald event wil reageren. Bovendien kan je een event enkel afvuren binnen de class waarin hij gemaakt is, dus die class is dan ook de enige die volledige controle over het event heeft. Als we het keyword event zouden weglaten in dit geval zou dit functioneel niet veel veranderen. Desalniettemin is het toch van cruciaal belang. Zeker in het geval we iets
ing. Gaillez Michael
36
3. Delegates en events in .NET
schrijven in C# en dit later ook willen gebruiken in VB.NET applicaties. VB.NET maakt een groot onderscheid tussen events en delegates. Hoewel alle bovenstaande code perfect juist is, toch is dit NOT DONE. In de praktijk zal je altijd iets zien in de tendens van (object sender, EventArgs e) als parameters van de event delegate. Je geeft dus het object mee dat aan de oorsprong ligt van het event en een aantal argumenten in de vorm van een EventArgs-object. Het object dat aan de oorsprong ligt van het event geef je mee voor het geval een methode aan meerdere events gekoppeld is. Bijvoorbeeld in een form heb je 2 buttons en aan het Click-event van die beide buttons hang je 1 methode buttonClick. Je kan dan in die ene methode nagaan over welke button het precies gaat door gewoon het ’sender’ object op te vangen. Het is eveneens zo dat men zo goed als altijd voor het sender object effectief ook het type object definieert in de delegate. Indien nodig wordt dan gecast naar het juiste type in de method zelf. De EventArgs e zijn van het type EventArgs of een subclass hiervan. Als er geen argumenten zijn geeft men dan EventArgs.Empty mee als parameter bij het afvuren van een event. Als je uw eigen argumenten wenst mee te geven dan moet je een class maken die erft van EventArgs en daar de nodige fields en properties voorzien. Ook plaatst men de code die effectief het event afvuurt gegroepeerd in een protected virtual OnXXX(. . . ) method. De reden hiervoor heeft te maken met inheritance. Als je in een subclass van een class met events nog iets extra wil doen bij een bepaald event dan ga je die OnXXX overriden. Zo moet je voor subclasses geen methods koppelen aan events. Een tweede reden om het op dergelijke manier aan te pakken is de volgende: Je kan enkel events afvuren in de class waarin ze gemaakt zijn. Dus zou je principieel een event niet kunnen afvuren in een subclass. Om er toch voor te zorgen dat je een event kan afvuren vanuit een subclass kan je de OnXXX method implementeren en die method kan dan aangeroepen worden vanuit een subclass om zo het event af te vuren. Tenslotte (alsof het nog niet genoeg was) als je ook alles thread-safe wil maken dan kan je maar beter uw event eerst opslaan in een lokale variable. De reden hiervoor is als volgt: 2 threads kunnen tegelijk een event manipuleren. Als de ene thread nu een event wil afvuren en de andere verwijdert tegelijkertijd de methodes die eraan gekoppeld zijn dan kan dit tot fouten leiden. Event zijn gebaseerd op delegates en een belangrijke eigenschap van delegates is dat ze immutable objects zijn net zoals een string dat is. Wat wil dit nu zeggen. Nemen we het volgende stukje code als voorbeeld: String tekst = " Hello "; tekst += " world !";
Door ”Hello” toe kennen aan de variabele tekst wordt een nieuw object aan die variable toegekend met daar daarin de waarde ”Hello”. So far so good. Maar als we nu het stuk ” world!” toevoegen aan die tekst variabele gebeurt er iets speciaals. Er wordt achter de schermen een nieuw string object met de waarde ”Hello world!” aangemaakt en dit wordt dan in de variabele tekst
ing. Gaillez Michael
37
3. Delegates en events in .NET
geplaatst. We hebben dus een volledig nieuw string object gemaakt zonder het te beseffen en het oude object is gewoon weg. De waarde ” world!” wordt dus niet aan het bestaande object toegevoegd. Delegates vertonen nu exact hetzelfde gedrag nl. als een extra methode wordt toegevoegd of verwijdert door middel van de += of -= operator dan wordt hiervoor het oude object weggegooid en een volledig nieuw aangemaakt. Door een delegate nu te kopi¨eren in een tijdelijke variabele hebben we een exacte kopie van die delegate. Als er nu een tweede threads iets aan die delegate verandert dan zal dit geen invloed hebben op onze tijdelijke variabele want die bevat nog een verwijzing naar het oude object. Op die manier kunnen we fouten gaan vermijden. Laten we dit alles nu eens toepassen op ons voorbeeld. Voeg aan het classlibrary project een class StartEventArgs toe en vul ze aan met onderstaande code. public class StartEventArgs : EventArgs { private DateTime _startTijd ; public StartEventArgs ( DateTime startTijd ) { _startTijd = startTijd ; } public DateTime StartTijd { get { return _startTijd ; } } }
Voeg nog een 2e class EindeEventArgs toe: public class EindeEventArgs : EventArgs { private DateTime _eindTijd ; public EindeEventArgs ( DateTime eindTijd ) { _eindTijd = eindTijd ; } public DateTime EindTijd { get { return _eindTijd ; } } }
Wijzig nu de Werknemer class tot ze er uit ziet zoals hieronder: public delegate void S t a r t W e r k E v e n t H a n d l e r ( object ←sender , StartEventArgs e ) ; public delegate void Do eW e r kE v en t H an d l er ( object sender ←, EventArgs e ) ;
ing. Gaillez Michael
38
3. Delegates en events in .NET
public delegate void E i n d e W e r k E v e n t H a n d l e r ( object sender , EindeEventArgs e ) ;
←-
public class Werknemer { private string _naam ; public Werknemer ( string naam ) { _naam = naam ; } public string Naam { get { return _naam ; } } public event Sta rtWe r k E v e n t H a n d l e r StartWerk ; public event DoeWerkEv e nt H a nd l e r DoeWerk ; public event Ein deWe r k E v e n t H a n d l e r EindeWerk ; public void VoerWerkUit () { Console . WriteLine (" Werknemer zegt : Ik begin aan de ←opdracht .") ; // Vuur event start af ... OnStart () ;
Console . WriteLine (" Werknemer zegt : Ik ben bezig aan de opdracht .") ; // Vuur event doe af ... OnDoe () ;
←-
Console . WriteLine (" Werknemer zegt : Ik ben klaar ←!!!") ; // Vuur event einde af ... OnEinde () ; } protected virtual void OnStart () { StartEventArgs e = new StartEventArgs ( DateTime . Now ←); Sta rtWer kEven tHan dl e r tmpStart ; tmpStart = StartWerk ; if ( tmpStart != null ) { tmpStart ( this , e ) ; } } protected virtual void OnDoe () { DoeWerkEventHandler tmpDoe ;
ing. Gaillez Michael
39
3. Delegates en events in .NET
tmpDoe = DoeWerk ; if ( tmpDoe != null ) { tmpDoe ( this , EventArgs . Empty ) ; } } protected virtual void OnEinde () { Ein deWer kEven tHan dl e r tmpEinde ; EindeEventArgs e = new EindeEventArgs ( DateTime . Now ←); tmpEinde = EindeWerk ; if ( tmpEinde != null ) { tmpEinde ( this , e ) ; } } }
Verander de Baas class als volgt: public class Baas { public void WerkGestart ( object sender , ←StartWerkEventArg s e ) { Console . WriteLine (" Baas zegt : hmmmm , " + e . ←StartTijd . ToShortT imeStri ng () + " ‘ I don ’ t care ←...") ; } public void WerkBezig ( object sender , EventArgs e ) { Console . WriteLine (" Baas zegt : nog bezig ? hmmmm , I don ’ t care ...") ; }
←-
public void WerkKlaar ( object sender , ←EindeWerkEventArg s ) { Console . WriteLine (" Baas zegt : " + e . EindeTijd . ←ToShortTimeString () + " Dat werd tijd !!!") ; } }
Voeg nu in het console-project nog een class Collega toe: public class Collega { public void WerkKlaar ( object sender , EindeEventArgs e) { string naam = (( Werknemer ) sender ) . Naam ;
ing. Gaillez Michael
←-
40
3. Delegates en events in .NET
←-
Console . WriteLine (" Collega zegt : Ok " + naam + " , ik zal aan de volgende stap beginnen ...") ; } }
Rest ons nu nog ons programma aan te passen voor het gebruik van de events: public class Bedrijf { public static void Main ( string [] args ) { // C r e e r een baas en collega object Baas baas = new Baas () ; Collega collega = new Collega () ; // C r e e r een werknemer object Werknemer werker = new Werknemer (" Louiske ") ; // Laat werker de baas en collega verwittigen werker . start += new S t a r t W e r k E v e n t H a n d l e r ( baas . ←WerkGestart ) ; // Note += werker . doe += new D o eW e r kE v e nt H an d l er ( baas . ←WerkBezig ) ; // Note += werker . einde += new E i n d e W e r k E v e n t H a n d l e r ( baas . ←WerkKlaar ) ; // Note += werker . einde += new E i n d e W e r k E v e n t H a n d l e r ( collega . ←WerkKlaar ) ; // Note += // Laat het werknemer object het werk uitvoeren . werker . VoerWerkUit () ; // Verwijder terug een eventkoppeling werker . doe -= new D o eW e r kE v e nt H an d l er ( baas . ←WerkBezig ) ; // Note -= // Voer opnieuw uit werker . VoerWerkUit () ; // Wachten op een enter invoer van de gebruiker zodat de output zichtbaar blijft Console . ReadLine () ;
←-
} }
ing. Gaillez Michael
41
Hoofdstuk 4
Threading 4.1
Inleiding
De noden in computer hardware en software zijn door de jaren heen steeds ´ en van de eerste zaken waar een grote behoefte aan meer en meer gegroeid. E´ was, was de mogelijkheid van software en hardware om meerdere stukken functionaliteit simultaan uit te voeren. De twee technologin die hierop een antwoord proberen te bieden zijn multitasking en multithreading. Eerst was er niks, dan de oerknal en dan de computer. In zijn vroege jaren waren computers grote logge machines in vergelijking met de computer van vandaag. Het was in die tijd dan ook niet mogelijk om iedereen een afzonderlijk een computer te bezorgen. Men werkte met mainframes en supercomputers waaraan een serie terminals (niet veel meer dan een scherm en toetsenbord) verbonden waren waarop individuele personen op konden werken. Deze mainframes moesten dus meerdere ¨’programma’s¨’ tegelijkertijd kunnen uitvoeren voor de verschillende terminal. Het is dan ook niet verwonderlijk dat multitasking en multithreading daar zijn oorsprong vindt. Voorlopers hierin waren de UNIX based operating systemen. Naarmate de technologie vorderde en computer kleiner en kleiner werden en ook steeds krachtiger kon een deel van de rekenkracht verlegd worden van server (mainframe) naar de client (terminal). Dit was de aanleiding tot de PC of personal computer. PC’s werden ook steeds krachtiger en steeds meer onafhankelijk van een server zodat de nood voor multitasking en multithreading stillaan is doorgesijpeld tot op het niveau van de pc.
4.2 4.2.1
Threading explained Multitasking
´ en van de eerste echte client operating systemen was DOS. DOS was niet E´ veel meer dan een programma die u toegang verschafte tot de verschillende hardware en u de mogelijkheid gaf om een programma uit te voeren. De
42
4. Threading
programma’s bestemd voor DOS waren proceduraal, dit betekent dat je een programma had waarbij alles stap voor stap na elkaar werd uitgevoerd van begin tot einde. We zouden een zo’n programma kunnen vergelijken met het lezen van een boek. Als je een boek leest dan doet je dit regel voor regel en houdt je een bladwijzer bij om aan te geven waar je al gekomen bent. Bij software is dit niet anders. Code wordt regel per regel (tekst uit het boek) uitgevoerd en een execution pointer (bladwijzer) houdt bij waar je gekomen bent in het programma. Hoe krachtig, eenvoudig en logisch dit ook is al evenzeer is het extreem beperkt want je kan slecht ´e´en programma uitvoeren op een gegeven moment. Hoewel meerdere taken tegelijkertijd uitvoeren in een DOS programma theoretisch niet onmogelijk is, toch is het niet evident. Dergelijke programma’s leidden als snel tot heel ingewikkelde en obscure code. Daarom heeft men een laag bovenop DOS gemaakt die dit wel mogelijk maakte nl. Windows 3.0. Windows was veel meer dan een mooie user interface, het was een laag bovenop DOS die het begrip multitasking mogelijk maakte. Multitasking is de mogelijkheid om meerdere programma’s tegelijkertijd uit te voeren. Om terug de vergelijking met ons boek te maken. Multitasking kan je vergelijken met het lezen van twee boeken tegelijkertijd. Je zal eerst een stuk lezen uit boek A en daarna een stuk uit boek B. Daarna terug een stuk uit boek A enz. . . Deze complexe bezigheid zal er toe leiden dat je 2 bladwijzers nodig hebt namelijk ´e´en voor boek A en ´e´en voor boek B. Als we ons niet zouden beperken tot twee boeken maar een willekeurig aantal boeken dan zouden we evenveel bladwijzers nodig hebben als we boeken aan het lezen zijn. In software is dit niet anders, Windows in ons geval houdt voor ieder programma dat het uitvoerd een execution pointer bij om te weten waar het gekomen is bij elk programma. Bij het voorbeeld van ons boek kunnen we natuurlijk niet perfect tegelijkertijd twee boeken lezen (of toch te meeste normale mensen niet). We gaan de tijd verdelen zodat we afwisselend een stuk uit elk boek lezen. Ook dit principe kunnen we doortrekken naar software. Dit noemen we time slicing. We gaan de procestijd die een processor ter beschikking heeft verdelen over de verschillende programma’s. Bekijken we hiervoor eens de volgende figuren:
Stel dat we zes seconden processortijd zouden ter beschikking hebben. Als we slechts twee processen zouden hebben dan zouden elk van die processen 50% van de tijd voor zich nemen ofte drie seconden. Wanneer er een derde proces zou bij komen dan gaat de tijd zich herverdelen en neemt elk van de processen 1/3 van de tijd voor zijn rekening of twee seconden per process. Dit gaat dan als volgt in zijn werk. Proces A wordt voor twee seconden uitgevoerd en wordt na die twee seconden stopgezet. Daarna gaat Proces B ing. Gaillez Michael
43
4. Threading
voor twee seconden zijn gang gevolgd door proces C. Op het einde wordt deze cyclus opnieuw herhaald. Hieruit blijkt dat meerdere processen niet perfect tegelijkertijd worden uitgevoerd maar in stukjes na elkaar. Maar twee seconden is in computertermen is een eeuwigheid. In de praktijk zal dit principe zich enkele honderdduizenden of miljoenen keren sneller voor doen. Voor de mens als eindgebruiker is dit veel te snel om daar iets van te merken waardoor het lijkt alsof alles tegelijkertijd wordt uitgevoerd. Multitasking was dus een feit met de komst van windows 3.0. Bij unix ligt dit enigsinds anders omdat unix zijn oorsprong vindt in de mainframe en server wereld waarbij unix als quasi van in den beginne over multitasking beschikte.
4.2.2
Multithreading
Hoewel multitasking reeds een significante verbetering oplevert in de manier waarop we met onze computer kunnen werken, toch is dit nog niet voldoende. Bij multitasking verwerkt het operating systeem de processen al niet meer proceduraal af maar het process is en blijft wel nog proceduraal. Stel dat een computer meerdere processen draait waarvan er ´e´en dient als webserver. Hierbij zal het webserver proces wachten op een binnenkomend verzoek. Als er een binnenkomend verzoek is, zal dit dan afgehandeld worden waarna het proces terug gaat wachten op een volgend verzoek. Maar wat als er nu twee verzoeken elkaar heel kort opvolgen, zo kort zelfs dat het ene verzoek nog niet afgehandeld is voor het tweede binnenkomt. . . Dit zou min of meer betekenen dat het tweede verzoek moet wachten tot het eerste is afgehandeld vooral we er kunnen aan beginnen. Als het eerste verzoek dan om ´e´en of andere reden relatief lang duurt dan krijgen we pas echt een vervelende situatie. Om nog maar te zwijgen van het feit dat er enkele honderden of duizenden verzoeken kort op elkaar zouden zijn. Een mogelijke oplossing zou kunnen zijn dat we voor elk verzoek een apart proces laten opstarten en daar laten afhandelen. Dan kunnen alle verzoeken min of meer gelijktijdig worden afgehandeld. Maar dit zou op zijn beurt weer betekenen dat we gegevens moeten kunnen delen tussen al die verschillende processen en dit is zo goed als onmogelijk zoals we later nog zullen zien. Om aan deze problematiek tegemoed te komen werd in de opvolger van Windows 3.X nl. Windows 95 ook multithreading ingevoerd. Multithreading is het principe van multitasking dat op een volgend niveau is gebracht. Bij multithreading gaan er nu meerdere execution pointers zijn per proces. We gaan dus als het ware meerdere processen opstarten binnen ´e´en en hetzelfde proces. Het grootste verschil tussen processen en threads is dat threads ook ´e´en en dezelfde geheugenruimte delen wat bij processen niet het geval is maar we komen hier in de sectie van synchronization nog uitgebreider op terug. Bij multithreading gaan we dus nogmaals het principe van timeslicing toepassen binnen ´e´en proces. Volgende figuren tonen deze uitbreiding: Nu wordt bij het aanvangen van proces C eerst de code van thread 1 gedurende ´e´en seconde uitgevoerd. Na die seconde wordt thread 1 gestopt en krijgt thread 2 de kans om gedurende ´e´en seconde zijn dingen te doen. Daarna is proces A terug aan de beurt. Dit alles ook terug zovele malen versnelt, zorgt ing. Gaillez Michael
44
4. Threading
er voor dat binnen een process ook bepaalde routines zo goed als simultaan kunnen worden uitgevoerd. Het principe van multithreading levert een significante verbetering op in heel wat terreinen. Een eerste is in het voorbeeld van onze webserver. Multithreading biedt de mogelijkheid om meerdere zaken quasi parallel te verwerken. Een tweede is bij zogenaamde blocking subtaken. Blocking betekent dat een welbepaald stuk code wacht op ´e´en of andere gebeurtenis alvorens verder te gaan. Door dit naar een aparte thread te verschuiven (en die bijgevolg te laten wachten) kan je een process nog andere dingen laten zonder dat het proces geblockt wordt. Een derde is bij erg lange operaties in combinatie met een user interface. Stel je maar eens voor dat een user interface niet meer zou reageren telkens je: een file verstuurd met een ftp-programma, je een document afdrukt, je uw emails verstuurt en ontvangt via een mailclient, . . . Dit zijn allemaal voorbeelden waarbij je wil dat uw user interface blijft werken ook al is er een langere operatie aan de gang. Men zal dus typisch langdurige operaties op een aparte thread uitvoeren. Dit zijn allemaal voorbeelden van ’enhanced user experience’, waarbij threading er voor zorgt dat de user interface een meer vlotte en vloeiende indruk geeft. Het voorbeeld van onze webserver ziet er dan in het verloop van de tijd als volgt uit. Threading werd ingevoerd van Windows 95, NT en hoger. Omdat oudere programma’s voor Windows 3.X nog geen threading kenden werden speciale functies voorzien om toch nog dergelijke programma’s te kunnen runnen met de nieuwere operating systemen. Ook bij threading ligt dit bij unix enigsinds anders omdat threading ook al heel vroeg in de ontwikkeling van unix based systemen zat. De functies in windows 95 en volgende waren nodig om de volgende reden: Elke applicatie in een multithreaded omgeving beschikt over minstens ´e´en thread. De thread die wordt aangemaakt bij het opstarten van de applicatie noemen we de primary thread. Wanneer deze primary thread be¨eindigt, wordt ook het programma afgesloten. Een voorbeeld van een single threaded (slechts ´e´en thread) kan er nu als volgt uit zien: Bij het verwerken van de file zou de programmeur er nu kunnen voor kiezen om nog een tweede thread op te starten voor het uitvoeren van een spellingscontrole op de tekst. Dan krijg je het scenario uit de volgende figuur: Wanneer het programma nu zal be¨eindigen zal afhangen van de manier waarop de thread is aangemaakt. Er zijn nl. twee belangrijke soorten threads
ing. Gaillez Michael
45
4. Threading
ing. Gaillez Michael
46
4. Threading
zijnde user threads en daemon threads. User threads blijven doorlopen tot ze voltooid zijn ook al is de primary thread reeds be¨eindigd, dus zolang er user threads actief zijn zal ook uw programma blijven lopen. Actieve daemon threads daarintegen worden afgesloten op het moment dat de primary thread wordt be¨eindigd. Deamon threads worden ook wel eens background threads genoemd.
4.2.3
Is it realy that simple?
Om de basisprincipes van multitasking en multithreading te schetsen in de voorgaande secties hebben we natuurlijk een grove vereenvoudiging van de werkelijkheid gemaakt maar het basisidee blijft overeind zelfs als we details in rekening zouden brengen. Om toch een beter beeld van de werkelijkheid te geven gaan we een aantal topics wat bijschaven. Time slicing reviewed In de realiteit zal de processortijd natuurlijk niet zo mooi gelijkmatig verdeeld zijn als we in onze voorbeelden hebben aangenomen. Een eerste factor die hierin een rol speelt zijn proces en thread priority. Processen en threads met een hogere priority zullen meer processor krijgen toegewezen dan diegene met een lagere priority. Mooie voorbeelden hier van zijn de programma’s om mee te werken aan research project. Deze programma’s nemen alle beschikbare processortijd op om berekeningen door te voeren maar dit met een heel lage proces priority. Dit houdt in dat van zodra een andere applicatie zoals Word, Outlook, . . . naar processortijd vraagt dat dit research programma automatisch een pak minder processortijd krijgt om aan de noden van een andere applicatie te voldoen. Ook in threading wordt actief gebruik gemaakt van deze priority. In programma’s met een user interface gaat de thread die instaat voor de user interface over het algemeen een heel hoge prioriteit hebben omdat je wil dat die onmiddellijk kan reageren op de wensen van de gebruiker. Threads die instaan voor het versturen of opslaan van files gaan dan weer een veel lagere prioriteit krijgen omdat dit sowieso al trage routines zijn en voor de eindgebruiker bijgevolg een stuk minder belangrijk zijn. Naast de priority zijn er nog een pak andere algoritmes en principes die uiteindelijk bepalen hoe veel tijd een proces of thread krijgt. Cooperative vs. preemptive multitasking Cooperative of preemptive multitasking bepaalt hoeveel een proces zelf te zeggen heeft in het verdelen van de processortijd. Bij cooperative multitasking is het, het proces zelf die aangeeft of een ander proces mag uitvoeren ja dan nee. Dit is een heel gevaarlijke benadering van time slicing omdat je de verantwoordelijkheid van het vrijgeven van de processor in de handen legt van de programmeur van een applicatie en bijgevolg onderlegt aan zijn goodwill. Dit betekent als een applicatie alle tijd voor zich neemt dat het ing. Gaillez Michael
47
4. Threading
operating systeem hier niks aan kan veranderen. Of erger nog als een applicatie om welke reden dan ook vast loopt in een oneindige lus dan crasht het hele systeem mee met die ene applicatie omdat je geen enkele manier hebt om de applicatie te stoppen tenzij de applicatie zelf die gecrasht is. Dit was het geval in Windows 3.X. Dit is een van de redenen waarom die versie van Windows zo onstabiel was om in te werken. Windows was toen niet alleen kwetsbaar voor fouten in windows zelf maar ook voor de fouten in andere software. In de nieuwe versies van Windows gebruikt met het andere scenario namelijk preemptive multitasking. Hierbij is het, het operating systeem dat de lakens uitdeelt bij het switchen tussen processen. Processen hebben hierin weinig of niks meer in de pap te brokken. Hierbij zal een proces van het operating systeem gedurende een welbepaalde tijd de kans krijgen om zijn ding te doen en zal na die tijd willes nilles gestopt worden. Hierdoor is het operating systeem veel minder gevoelig aan crashes. Als een applicatie dan door het dolle heen gaat en in een oneindige lus draait ben je nog steeds in staat om hier een eind aan te breien door het proces af te breken met behulp van het operating systeem. Contextswitching Tot nog toe hebben we ons enkel en alleen nog maar gefocuseerd op de voordelen van threading. Threading kan inderdaad een gevoelige performance win opleveren voor uw applicatie maar zoals met alle leuke dingen geldt ook hier de regel ¨’overdaad schaadt¨’. Bij elke overgang van de ene thread naar de andere is er enige tijd nodig om die overgang te maken. Die overgang noemen we contextswitching. In een poging om uw programma zodanig te verbeteren door threads te gebruiken kan het nu gebeuren dat uw programma juist veel trager gaat lopen door het overdadig gebruik van threads. Hoewel contextswitching waarschijnlijk het meest performante en absoluut bugvrije onderdeel van uw operating systeem is (ja zelfs in windows) toch mag je dit niet uit het oog verliezen. We kunnen dit best illustreren met een figuur:
We bekijken de time slice van ´e´en proces. Binnen dit proces gaan we het aantal threads opvoeren. De gearceerde zone is de tijd die verloren gaat aan contextswitching en de witte zone is de nuttige tijd. We zien hier nu
ing. Gaillez Michael
48
4. Threading
duidelijk dat naarmate we het aantal threads opvoeren dat steeds meer tijd verloren gaat aan contextswitching. In de laatste balk is het overwicht zelfs contextswitching geworden en verliezen we dus meer tijd dan we nuttige dingen doen. Het komt er dus op neer om het juiste evenwicht te vinden in het aantal threads die je invoert. Er is geen stelregel die je kan opgeven over hoeveel threads je kan en mag opstarten om performant te blijven, het is vooral een kwestie van ervaring en testen.
4.2.4
Synchronization
Wat en waarom? We weten nu al wat multitasking en multithreading zijn maar we hebben tot nu toe in alle talen gezwegen over hoe al die processen en threads omgaan met het geheugen. In DOS was dit eenvoudig, er was maar ´e´en programma dus dit programma had dan ook zo goed als alle geheugen ter beschikking. Bij multitasking en multithreading ligt dit echter iets anders. Hier zal het geheugen moeten gedeeld worden of op een andere manier worden beheerd om geen ongewenste effecten te verkrijgen. Laten we beginnen met het geheugen management bij multitasking. Bij multitasking geldt bijna overal het principe van proces isolatie. Dit houdt in dat ieder proces zijn eigen stuk geheugen krijgt toegewezen en de verschillende processen hebben geen toegang tot elkaars geheugen. Dit is ook noodzakelijk voor de stabiliteit van het operating systeem. Als processen zomaar in elkaars geheugen kunnen schrijven dan is dit om problemen vragen. Het ene proces zou dan de geheugenstructuur van een ander proces over hoop kunnen gooien om nog maar te zwijgen van de security issues die hiermee zouden gepaard gaan. In Windows 3.X maar ook in Windows 95, 98 en Me werd af en toe wel eens een loopje genomen met deze regel. Dit maakte ook dat deze operating systemen vaak veel onveiliger waren en bij momenten heel onstabiel. In alle NT-based operating systemen (NT, 2000 en XP) is dit veel strikter en bijgevolg ook veel veiliger en stabieler. Ook in unix based operating systemen is dit heel strikt. We kunnen dus stellen dat we ons weinig zorgen moeten maken over memory management tussen processen omdat dit geregeld wordt door het operating systeem. Bij threads krijgen we een heel ander verhaal. Threads bevinden zich binnen ´e´en en hetzelfde proces en ze delen dus ook het geheugen binnen dat proces met elkaar. Dit houdt in dat twee of meerdere threads quasi tegelijkertijd een bepaald stuk geheugen kunnen manipuleren. Om fouten te vermijden gaan we onze code hier dus eigenhandig moeten tegen beveiligen. Deze beveiliging in code tegen foute manipulatie van het geheugen door threads noemen we synchronization. Stel dat 2 threads waarden kunnen weg schrijven naar een array en we een teller bijhouden op welke positie de volgende waarde moet gezet worden. Laten we vertrekken van de volgende situatie op posititie 0 en 1 zitten reeds de waardes p en q en de volgende positie om een letter te plaatsen is dus nu 2. ing. Gaillez Michael
49
4. Threading
array |p|q||||| teller = 2 Als thread 1 nu een waarde r plaatst op positie 2 dan moet hij de teller ook verhogen naar 3. Maar als thread 1 stopt (om welke reden dan ook) alvorens de teller te verhogen maar wel reeds een waarde r heeft geplaatst dan krijgen we de volgende situatie. array |p|q|r|||| teller = 2 Als thread 1 zou verder doen (nl. teller verhogen) dan zou er geen schade zijn maar neem nu dat thread 2 ook een waarde s wou toevoegen. Terwijl thread 1 aan het wachten is om nog eens te kunnen lopen kan thread 2 nu het volgende doen. array |p|q|s|||| teller = 3 Niet veel erg zou je zeggen ware het niet dat als thread 2 klaar is nu ook thread 1 terug kan verder doen en thread 1 moest nog enkel de teller verhogen dus krijgen we: array |p|q|s|||| teller = 4 We zien hier duidelijk dat als 2 threads dezelfde data delen dat er dan een relatief groot gevaar bestaat dat er iets fout loopt bij de manipulatie van die data. In ons geval is er gewoon een letter verdwenen uit de lijst. Hoe? Om data te kunnen beveiligen heeft zowat ieder object in een object geori¨enteerde taal een lock flag of sleutel waarmee je uw data kan afschermen van andere threads. Door gebruik te maken van specifieke keywords of attributen kunnen we nu vanuit code met deze sleutel interactie maken.
Door de sleutel met keywords op te vragen van het object, krijgt de code op een thread die de sleutel bezit exclusieve toegang tot dat object. ing. Gaillez Michael
50
4. Threading
Als we nu vanop een andere thread ook de sleutel van het object proberen op te vragen dan lukt dit niet meer omdat het object de sleutel niet meer bij zich heeft. Het gevolg is dat de code op de tweede thread zal blokkeren omdat het de sleutel niet kan verkrijgen. De code zal geblokkeerd worden tot de eerste thread de sleutel terug vrij geeft.
Het is belangrijk dat ALLE code op verschillende threads die gemeenschappelijke data of objecten gebruiken deze sleutel eerst opvragen. Als we dit niet doen dan kan de code ook niet blokkeren en dan is de data ook NIET beveiligd! Als een stuk code op een thread de sleutel van een object niet kan verkrijgen dan wordt het in een soort wachtrij geplaatst van threads die deze sleutel willen bemachtigen. Van zodra de sleutel terug beschikbaar is zal een thread uit die wachtrij dan de sleutel krijgen en zo zijn actie verder zetten. Deadlocking In de vorige sectie zagen we dat we variabelen kunnen ”locken”. Het is niet alleen belangrijk dat we effictief objecten beveiligen of synchronizen maar het is al evenzeer belangrijk dat we dit goed doen anders schuilt het gevaar van deadlocking. Deadlocking is het fenomeen waarbij ergens in de code de lock op een object wordt genomen door een thread maar dat die nooit meer
ing. Gaillez Michael
51
4. Threading
wordt vrijgegeven. Op die manier kan een applicatie vastlopen omdat de variabelen gelockt blijven en alle threads uiteindelijk in een geblockte status terecht komen. De kans op een deadlock is in .NET veel ree¨eler dan in Java. We zullen hier dan ook uitgebreider op terug komen bij de bespreking van threading in de twee talen. Er is ´e´en situatie waarbij deadlocking in de beide talen kan optreden en dit heeft te maken met nested synchronization. Nested synchronization is het proces waarbij eerst een lock wordt genomen op een object en binnen die lock wordt dan nog een een tweede lock genomen op een ander object.
In de bovenstaande situatie krijgen we kans op een deadlock. Als thread 1 en thread 2 quasi gelijk lopen dan kan het gebeuren dat thread 1 een lock neemt op object A en daarna stopt en dan thread 2 loopt en een lock neemt op object B. Thread 2 kan niet meer verder lopen omdat thread 1 de lock heeft object A. Maar als thread 1 nu wil verder gaan kan dit ook niet meer omdat thread 2 de lock heeft op object B. Het gevolg is dus dat de beide threads vast zitten en er dus een deadlock ontstaat.
Om dit probleem bij nested synchronization te vermijden moeten we er ten allen tijde voor zorgen dat de volgorde waarin de lock op objecten wordt genomen dezelfde is. In de situatie uit de bovenstaande figuur kan geen deadlock optreden omdat beide threads eerst de lock op A moeten hebben alvorens ze de lock op object B kunnen nemen.
4.2.5
Signaling
Over het algemeen worden threads gecre¨eerd om onafhankelijke taken apart van elkaar uit te voeren. Maar in sommige situaties moet er een vorm van interactie zijn tussen threads. Deze interactie noemt men signaling. Om het probleem te schetsen nemen we het voorbeeld van een taxichauffeur en zijn passagier. Als een passagier in een taxi stapt dan wil hij rustig plaats nemen tot de chauffeur de passagier verwittigd dat ze op de bestemming zijn aangekomen. Het zou heel vervelend zijn voor passagier en chauffeur als de ing. Gaillez Michael
52
4. Threading
passagier om de haverklap zou moeten vragen of hij al aangekomen is. Eveneens verwacht de chauffeur dat een passagier een teken geeft als hij een taxi wil. Het zou behoorlijk vervelend zijn voor de chauffeur als hij aan iedereen moet vragen of ze een taxi willen. De taxichauffeur wacht dus tot een passagier hem verwittigd dat hij ergens heen wil en schiet dan in actie. Omgekeerd wil de passagier rustig afwachten tot de taxichauffeur hem verwittigd dat ze aangekomen zijn en de passagier zijn weg dus kan verder zetten. Dit een eenvoudige vorm van interactie waarbij de ene gewoon afwacht tot de andere verwittigd dat er nood tot actie is. Dit principe bestaat ook bij threads door middel van wait en notify. Wanneer een thread in een wait state wordt gezet dan wordt de thread tijdelijk platgelegd (en gebruikt dus bijgevolg ook geen processortijd meer) tot een andere thread een notify signaal stuurt waardoor de thread terug actief wordt en zijn werk verder zet. Op die manier is er interactie tussen twee threads mogelijk.
4.2.6
Thread States
De combinatie van time slicing (of ook wel scheduling), synchronization en signaling zorgt er voor dat een thread zich steeds in een welbepaalde status bevindt. Deze mechanismen bepalen ook hoe de thread kan overgaan van de ene status naar de andere. Laten we er eens onderstaand schema bij halen.
De verschillende mogelijke statussen zijn de volgende: Nieuw : Dit is bij de creatie van een thread. Ook al is er een thread aangemaakt toch wordt er nog geen code uitgevoerd tot de thread gestart wordt. Dead : Dit is de toestand waarbij de taak van de thread is afgewerkt en dus niet langer meer iets zal uitvoeren tenzij er een een nieuwe start komt. Runnable : In deze toestand kan een thread code uitvoeren maar hij is hiervoor aan het wachten op zijn volgende timeslice. Dit is ook de
ing. Gaillez Michael
53
4. Threading
toestand waarin de thread terecht komt als zijn timeslice afgelopen is en terug moet wachten op de volgende time slice. Running : De thread voert daadwerkelijk zijn code uit. Blocked : Dit is een toestand waarbij de thread stil ligt door tal van redenen. De thread kan een pauze inroepen voor een bepaalde tijd (sleep) of de thread is aan het wachten op een bepaalde resource die moet beschikbaar komen zoals bvb. binnenkomende netwerkdata. Blocked door lock : Hier is de thread gestopt omdat hij de lock niet kan verkrijgen op een object en dus moet wachten tot die lock terug wordt vrijgegeven. Als de lock vrijkomt kan de thread dan proberen de lock te krijgen en zo in de runnable status terecht komen. Blocked door wait : In deze toestand kan een thread terecht komen door signaling. Als we een thread in een wait state plaatsen dan zal het wachten op een notify alvorens terug proberen verder te gaan. De pijlen in de figuur geven aan welke overgangen er tussen de verschillende states er mogelijk zijn. Het is belangrijk om een goed inzicht te hebben in dit model om het gedrag van threads te begrijpen en te optimaliseren.
4.3 4.3.1
Threading in .NET Thread en ThreadStart
De voornaamste manier om threads aan te maken in .NET is door gebruik te maken van de Thread class en de ThreadStart delegate. De ThreadStart delegate bevat een referentie naar de methode die op een andere thread moet worden uitgevoerd. Laten we eens kijken naar de definitie van Threadstart: delegate void ThreadStart () ;
Listing 4.1: Definitie van ThreadStart Hieruit zien we dat de methode die de code voor de thread bevat van het type void moet zijn en geen parameters mag bevatten. Om nu een nieuwe thread te maken moet een nieuw Thread object aangemaakt worden. De constructor verwacht een delegate van het type ThreadStart als parameter. Door de ThreadStart delegate mee te geven weet de thread welke code moet uitgevoerd worden op die thread. De creatie van een nieuw Thread object betekent niet dat het object onmiddellijk begint met de uitvoering van die code, daarvoor moet eerst de Start-method van het thread object worden uitgevoerd. Door Start komt de thread in de runnable state terecht. Hieronder zie je de code voor de creatie en het starten van een nieuwe thread: ... // Thread aanmaken en starten ... ThreadStart threadMethod = new ThreadStart ( Run ) ;
ing. Gaillez Michael
54
4. Threading
Thread trd = new Thread ( threadMethod ) ; trd . Start () ; ... public void Run () { // Hier komt de code voor thread }
Listing 4.2: Thread maken en starten ´ en van de belangrijkste properties van de Thread class is de IsBackground E´ property. Hiermee kan je de thread instellen als user of daemon thread. Belangrijk te weten hierbij is dat de IsBackground property moet ingesteld zijn voor dat de Start methode wordt aangeroepen. De Thread class heeft ook een aantal belangrijke methods naast de Start method: Abort : Stopt de thread onmiddellijk door het gooien van een ThreadAbortException. Na het aanroepen van deze methode kan de thread niet meer opnieuw gestart worden met Start! Interrupt : Hiermee kunnen we een thread terug wakker maken die zich in een Sleep, Wait of Join state bevindt. Deze methode gooit een ThreadInterruptedException op de thread. Door de exception kan hiermee ook een thread gestopt worden. Suspend : Hiermee kunnen we een thread stilleggen tot de Resume methode wordt aangeroepen. Dit is om threads te pauzeren... Resume : De thread die suspended was, wordt nu gewoon terug verder gezet. Join : Het aanroepen van deze methode zorgt er voor dat je op dat punt wacht tot de thread be¨eindigd is. Hoewel deze methodes zeer handig kunnen zijn moeten ze toch met een zekere omzichtigheid gebruikt worden. Een veel betere manier dan Thread.Abort of Thread.Interrupt om een thread te be¨eindigen is door gewoon de methode van de ThreadStart delegate te laten aflopen. Dit kan bevoorbeeld door gebruik te maken van een simpele bool variabele die aangeeft of de thread nog moet doorgaan ja of nee. Laat ons dit illustreren met een voorbeeldje waarbij we een oneindige lus gebruiken op onze thread: public class StopableThread { private bool _isCancelled = false ; public static void Main ( string [] args ) { // Thread maken en starten StopableThread st = new StopableThread () ; ThreadStart runner = new ThreadStart ( st . Run ) ; Thread trd = new Thread ( runner ) ;
ing. Gaillez Michael
55
4. Threading
trd . Start () ; // Thread stoppen st . Cancel () ; } public void Run () { while ( true ) { Thread . Sleep (1000) ; if ( _isCancelled == true ) break ; } } public void Cancel () { _isCancelled = true ; } }
Listing 4.3: Een thread stoppen In dit voorbeeld zal het aanroepen van de Cancel methode de boolean op true zetten waardoor de oneindige lus op de andere thread zal onderbroken worden. Ook Suspend en Resume kunnen heel aantrekkelijk klinken maar ook daar schuilt gevaar in het gebruik van deze methodes. Als een thread in suspend mode gaat terwijl hij nog een lock heeft op een variabele dan kan dit aanleiding geven tot een deadlock.
4.3.2
De ThreadPool
De Thread class gebruiken in .NET voor het maken en beheren van threads, is ideaal voor het geval je een klein aantal threads nodig hebt of als je specifieke controle wil over de threads. Als je een groter aantal threads wil dan is het meer aangewezen om de ThreadPool class te gaan gebruiken. Laten we nog eens terug komen op ons voorbeeld van een webserver die we reeds eerder gebruikten. Bij een webserver hebben we slechts heel kortstondig een connectie die de request afhandeld en daarna wordt de connectie terug verbroken. Deze connectie zou dan moeten gemaakt worden door middel van sockets en elke request moet dan ook op een aparte thread worden afgehandeld. In ons programma zouden we dus niks anders moeten doen dan threads cre¨eren om de requests af te handelen. Dit is een heel arbeidsintensief werkje dat bovendien veel resources verbruikt. Komt daar nog bij dat als er enkele duizenden requests tegelijkertijd zouden binnen komen dat er dan ook enkele duizenden threads zouden worden opgestart. In dit geval zouden we zeker geconfronteerd worden met ons contextswitching probleem dat we reeds eerder aanhaalden. In dit voorbeeld zou het gebruik van de ThreadPool class veel efficienter zijn. Bij thread pooling gaan we taken in een queue (wachtlijst) plaatsen die dan worden afgehandeld door threads die automatisch worden aangemaakt. De thread die wordt aangemaakt zal dan ing. Gaillez Michael
56
4. Threading
taken blijven uitvoeren uit de queue tot er geen meer zijn, pas dan wordt de thread gestopt. Het voordeel hierbij is dat we threads opnieuw kunnen gebruiken om verschillende taken uit te voeren en je dus de overlast van het continu aanmaken van threads al niet meer hebt. Bovendien zal de threadpool threads aanmaken in functie van hoeveel er nodig zijn en het systeem de kans geven om de time-slicing op de processor te optimaliseren. Het is dus niet omdat er 10 taken in de queue staan dat er daarom ook 10 threads zullen worden aangemaakt. Er kan ook slechts een maximum aantal threads worden aangemaakt (standaard is dit 25) waardoor er nooit een overdreven hoeveelheid threads aanwezig zijn en de applicatie last krijgt van context switching. Elke applicatie heeft slechts ´e´en ThreadPool ter beschikking en de ThreadPool begint maar te werken als hij de eerste keer wordt gebruikt. Men kan taken in de queue plaatsen door gebruik te maken van de static QueueUserWorkItem method van de ThreadPool class. Deze methode verwacht een delegate van het type WaitCallBack en optioneel een variabele van type object om een eventuele status meegegeven. Hieronder zie je de definite van de WaitCallBack delegate: public delegate void WaitCallBack ( object state ) ;
Listing 4.4: Definitie van WaitCallBack De WaitCallBack delegate is heel gelijkaardig aan de ThreadStart delegate met dat verschil dat we een state kunnen meegeven. Laten we dit nu eens praktisch uitwerken. ... WaitCallBack threadpoolTask = new WaitCallBack ( Task ) ; ThreadPool . QueueUserWorkIt em ( threadpoolTask ) ; ... public void Task ( object state ) { // Werk dat moet gedaan worden op een andere thread ←... }
Listing 4.5: De ThreadPool gebruiken Als we iets met de state willen gaan doen kunnen we ons voorbeeldje ietwat gaan uitbreiden: ... WaitCallBack threadpoolTask = new WaitCallBack ( Task ) ; ThreadPool . QueueUserWorkIt em ( threadpoolTask , 100) ; ... public void Task ( object state ) { // State ophalen int iterations = ( int ) state ; for ( int i = 0; i < iterations ; i ++)
ing. Gaillez Michael
57
4. Threading
{ // Doe iets ... } }
Listing 4.6: ThreadPool gebruiken met argumenten De ThreadPool class bevat ook nog een aantal andere static methods om bijvoorbeeld het maximum aantal threads aan te passen (maar 25 is een heel goed startpunt!!!) of om voor performance redenen bepaalde security checks te omzeilen maar dit zou ons te ver leiden. We zullen de ThreadPool ook nog gebruiken bij asynchrone method calls maar dit wordt dan op de achtergrond voor ons afgewerkt, we hoeven dit niet zelf te implementeren dan.
4.3.3
Synchronization een waaier aan mogelijkheden
In .NET hebben je aantal mogelijkheden om uw variabelen te beveiligen tegen het gelijktijdig manipuleren door meerdere threads. Deze ruime set mogelijkheden biedt enerzijds een stuk flexibiliteit naar hoe je gaat beveiligen, anderzijds is het zo dat het gebruik van bepaalde mogelijkheden toch zekere risico’s inhoud. We zullen beginnen met de twee minst gevaarlijke en daarna zullen we de meer flexibele mogelijkheden aanwenden. Tenzij er een specifieke reden is, is het altijd beter om de eerste twee methodes te gebruiken. Zo voorkom je onbetrouwbare toepassingen. Het lock statement Het lock statement beveiligd een bepaald object in een code block. Binnen dit code block kan dit object niet benaderd worden door andere threads. Een voorbeeldje: public class PersonList { private ArrayList _persons = new ArrayList () ; public void Add ( string person ) { lock ( _persons ) { if ( _persons . Contains ( person ) == false ) _persons . Add ( person ) ; } } }
Listing 4.7: Lock statement met variabele In dit voorbeeld kan een andere thread niks meer veranderen of opvragen van de ArrayList zolang we binnen het lock statement block zitten. De ArrayList ing. Gaillez Michael
58
4. Threading
is dus beveiligd tegen zowel lezen als schrijven. Meestal zal men het huidige object opgeven als zijnde het te beveiligen object, zo zijn alle uit dat object beveiligd. Dan krijgen we de volgende situatie: public class PersonList { private ArrayList _persons = new ArrayList () ; private int _counter ; public void Add ( string person ) { lock ( this ) { if ( _persons . Contains ( person ) == false ) { _persons . Add ( person ) ; _counter ++; } } } }
Listing 4.8: Lock statement met this Nu zijn zowel de ArrayList als de Integer beveiligd. Let wel lock kan enkel gebruikt worden in C# als we hetzelfde willen doen in VB.NET moet we het SyncLock statement gebruiken. Het MethodImpl attribute Je kan ook een volledige methode van een object beveiligen in plaats van een bepaald stuk van de methode door gebruik te maken van het MethodImpl attribute uit de System.Runtime.CompilerServices namespace. We kunnen dit nu toepassen op het voorgaande voorbeeld: public class PersonList { private ArrayList _persons = new ArrayList () ; private int _counter ; [ MethodImpl ( MethodImplOptio ns . Synchronized ) ] public void Add ( string person ) { if ( _persons . Contains ( person ) == false ) { _persons . Add ( person ) ; _counter ++; } } }
Listing 4.9: Synchronization met MethodImpl attribute
ing. Gaillez Michael
59
4. Threading
Ook hier zijn zowel de ArrayList als de Integer beveiligd maar nu voor de hele methode. Deze methode kan dus maar door ´e´en thread tegelijk worden aangeroepen. ReaderWriterLock gebruiken In sommige gevallen kan het gebeuren dat we iets meer controle willen over hoe we variabelen binnen een object willen beveiligen. Zo kan het zijn dat zoveel threads mogen lezen van de variabele zolang er geen schrijf operatie aan de gang is en dat er slechts kan geschreven worden zolang er geen leesoperaties bezig zijn door ´e´en of meerdere threads en er geen andere threads bezig zijn met schrijven. In dit geval kunnen we gebruik maken van de ReaderWriterLock class gaan gebruiken. We kunnen dit mss best illustreren aan de hand van een voorbeeld: public class PersonList { private ReaderWriterLock _rwLock = new ←ReaderWriterLock () ; private ArrayList _persons = new ArrayList () ; public string GetPerson ( int index ) { string name = null ; _rwLock . AcquireReaderLock ( -1) ; name = ( string ) _persons [ index ]; _rwLock . ReleaseReaderLock () ; return name ; } public void Add ( string person ) { _rwLock . AcquireWriterLock ( -1) ; _persons . Add ( person ) ; _rwLock . ReleaseWriterLock () ; } }
Als nu een of meerdere threads de GetPerson methode aanroepen dan zal de readerlock worden gezet voor als deze threads, maar alle threads gaan kunnen lezen. Als nu ook tegelijkertijd nog een thread probeert de Add methode aan te roepen dan zal deze thread moeten wachten bij AcquireWriterLock tot alle leesoperaties zijn voltooid. Bovendien als ook nog een tweede thread de Add methode aanroept zal deze moeten wachten tot de eerste thread klaar is met schrijven. Terwijl er een schrijfoperatie bezig is zullen threads die dan de GetPerson methode aanroepen moeten wachten bij AcquireReaderLock tot de schrijfoperatie voltooid is. Het gevaar bestaat er nu in dat als er iets fout loopt tussen de Acquire en de Release van de lock dat er een deadlock ontstaat waardoor er voorgoed niet meer kan gelezen of geschreven worden van en naar de methode. Bij het optreden van een exception be¨eindigd de methode dan zonder een release te ing. Gaillez Michael
60
4. Threading
doen en dit is gevaarlijk. Daarom gaan we in dit geval best altijd deze code in een try-catch-finally block zetten: public class PersonList { private ReaderWriterLock _rwLock = new ←ReaderWriterLock () ; private ArrayList _persons = new ArrayList () ; public string GetPerson ( int index ) { string name = null ; _rwLock . AcquireReaderLock ( -1) ; try { name = ( string ) _persons [ index ]; return name ; } finally { _rwLock . ReleaseRea derLock () ; } } public void Add ( string person ) { _rwLock . AcquireWriterLock ( -1) ; try { _persons . Add ( person ) ; } finally { _rwLock . ReleaseWri terLock () ; } } }
In dit geval zijn we zeker dat de lock altijd gereleast wordt. Het finally block wordt namelijk altijd uitgevoerd (ook als er een return in de try staat)! Monitor class gebruiken Wat we eerder deden met het lock statement kunnen we ook doen met de Monitor class, door gebruik te maken van de static Enter en Exit methods. Dit levert dan het volgende op: public class PersonList { private ArrayList _persons = new ArrayList () ; private int _counter ; public void Add ( string person )
ing. Gaillez Michael
61
4. Threading
{ Monitor . Enter ( this ) ; if ( _persons . Contains ( person ) == false ) { _persons . Add ( person ) ; _counter ++; } Monitor . Exit ( this ) ; } }
Ook hier dreigt weer hetzelfde gevaar als bij de ReaderWriterLock. Als er een exception optreed tussen Enter en Exit dan kunnen we een deadlock krijgen. Daarom ook hier gebruiken maken van het try-catch-finally block: public class PersonList { private ArrayList _persons = new ArrayList () ; private int _counter ; public void Add ( string person ) { Monitor . Enter ( this ) ; try { if ( _persons . Contains ( person ) == false ) { _persons . Add ( person ) ; _counter ++; } } finally { Monitor . Exit ( this ) ; } } }
Bij het lock (SyncLock in VB) statement wordt dit automatisch voor u gedaan. Daarom indien mogelijk ten allen tijde het lock statement gebruiken. Op die manier heb je het minst kans op fouten.
4.3.4
Signaling
Signaling met ManualResetEvent en AutoResetEvent Met de ManualResetEvent kunnen we een eerste thread in de wait state plaatsen tot een andere tweede thread de eerste thread terug activeert door een notify. De ManualResetEvent class bevat drie belangrijke methodes: WaitOne : Dit zal de thread tijdelijk blokkeren als de ManualResetEvent class in een non-signaled state staat. De thread zal weer verder gaan ing. Gaillez Michael
62
4. Threading
als ManualResetEvent signaled wordt. Set : Met deze methode kunnen we ManualResetEvent op signaled plaatsen. Reset : Met deze methode kunnen we ManualResetEvent op non-signaled zetten. Belangrijk om op te merken is dat de WaitOne methode geen enkel effect heeft zolang er een signaled state is. We moeten dus Reset gebruiken als we willen dat we terug naar non-signaled gaan. In de constructor van ManualResetEvent kunnen we ook de start waarde zijnde signaled of non-signaled opgeven. Een voorbeeldje: public class SignaledWorker { ManualResetEvent _paused = new ManualResetEvent ( ←false ) ; public static void Main ( string [] args ) { SignaledWorker worker = new SignaledWorker () ; ThreadStart runner = new ThreadStart ( worker . Run ) ; Thread trd = new Thread ( runner ) ; trd . Start () ; worker . Continue () ; worker . Continue () ; worker . Continue () ; } public void Run () { while ( true ) { _paused . WaitOne () ; // Doe iets ... _paused . Reset () ; } } public void Continue () { _paused . Set () ; } }
In dit voorbeeld zal de thread telkens stoppen bij paused.WaitOne en wanneer worker.Continue wordt aangeroepen zal WaitOne worden verlaten om terug verder te gaan met de thread. De thread wordt met andere woorden maar geactiveerd wanneer we hem nodig hebben. Als we de paused.Reset niet zou de thread continue in de oneindige lus blijven doorlopen zonder te stoppen bij WaitOne en bij gevolg te wachten op het aanroepen van Set.
ing. Gaillez Michael
63
4. Threading
AutoResetEvent heeft een identieke werking als ManualResetEvent met dit verschil dat het geen Reset method heeft omdat het resetten telkens automatisch gebeurd. Signaling met de Monitor class Met de Monitor class kunnen we nu hetzelfde doen als met de ManualResetEvent en AutoResetEvent maar met dat verschil dat we het nu op een object doen en binnen een lock statement, een methode met het MethodImpl attribuut of binnen een Enter/Exit block. De methodes van de Monitor class die we hiervoor nodig hebben zijn: Wait : Dit released de lock op het object en plaatst de thread in een wait state tot er een pulse komt. Pulse : Dit zal ´e´en thread die een wait state heeft op dat object uit zijn wait state halen. PulseAll : Dit zal alle threads die een wait state hebben op dat object uit hun wait state halen. Laten we dit nu toepassen op ons vorig voorbeeld: public class SignaledWorker { public static void Main ( string [] args ) { SignaledWorker worker = new SignaledWorker () ; ThreadStart runner = new ThreadStart ( worker . Run ) ; Thread trd = new Thread ( runner ) ; trd . Start () ; worker . Continue () ; worker . Continue () ; worker . Continue () ; } public void Run () { while ( true ) { lock ( this ) { Monitor . Wait ( this ) ; // Doe iets ... } } } public void Continue () { lock ( this ) {
ing. Gaillez Michael
64
4. Threading
Monitor . Pulse ( this ) ; } } }
Als we nu meerdere threads zouden hebben dan krijgen we het volgende: public class SignaledWorker { public static void Main ( string [] args ) { SignaledWorker worker = new SignaledWorker () ; ThreadStart runner = new ThreadStart ( worker . Run ) ; Thread trd1 = new Thread ( runner ) ; Thread trd2 = new Thread ( runner ) ; trd1 . Start () ; trd2 . Start () ; worker . Continue () ; worker . Continue () ; worker . Continue () ; } public void Run () { while ( true ) { lock ( this ) { Monitor . Wait ( this ) ; // Doe iets ... } } } public void Continue () { lock ( this ) { Monitor . PulseAll ( this ) ; } } }
Nu zullen beide threads gepauseerd worden bij Wait en hervat worden bij het aanroepen van PulseAll.
4.4
Threading in Java
In Java beschikt men over twee manieren om threads aan te maken. Enerzijds kan men gebruik maken van een object dat de Runnable interface ing. Gaillez Michael
65
4. Threading
implementeert, anderzijds kan men erven van de Thread class.
4.4.1
Implementing Runnable interface
Een eerste manier om threads aan te maken in Java is door gebruik te maken van de Runnable interface. De Runnable interface verwacht een methode van de signatuur public void run(). Binnen deze methode komt dan de code die op een aparte thread moeten worden uitgevoerd. Een object van het type Runnable is natuurlijk niet voldoende we hebben ook nog een object van het type Thread uit de packet java.lang nodig. De constructor van de Thread class verwacht dan een object van het type Runnable. public class TryThread implements Runnable { public static void main ( string [] args ) { TryThread runner = new TryThread () ; Thread trd = new Thread ( runner ) ; trd . start () ; } public void run () { // code voor de thread } }
4.4.2
Threading door inheritance
De tweede manier om threads te maken is door gebruik te maken van inheritance en te erven van de Thread class. De Thread class zelf is ook van het type Runnable waardoor ze ook reeds een method run bezit. Het komt er dus op neer dat we de methode run moeten overriden om zo onze thread aan te maken: public class TryThread extends Thread { public static void main ( string [] args ) { TryThread trd = new TryThread () ; trd . start () ; } public void run () { // code voor de thread } }
Belangrijk op te merken hierbij is dat we expliciet de start methode moeten aanroepen om de thread te starten. Als we gewoon de methode run zouden aanroepen dan zou de code uitgevoerd worden op de huidige thread en niet op een aparte thread. Dus dit levert niet het gewenste effect:
ing. Gaillez Michael
66
4. Threading
public class TryThread extends Thread { public static void main ( string [] args ) { TryThread trd = new TryThread () ; // FOUT !!!!! trd . run () ; } public void run () { // code voor de thread } }
4.4.3
Threading algemeenheden
Net als bij .NET bezit de Thread class een aantal methodes om de thread te manipuleren: setDaemon : Aan deze method kunnen we een boolean parameter meegeven die de thread insteld als user of daemon thread. Ook hier geldt dat deze waarde moet gezet worden alvorens de thread wordt gestart. interrupt : Haalt de thread uit een wait, sleep of join state door een InterruptException te gooien. Door de InterruptException moeten we een try-catch bouwen rondom de wait, sleep en join methodes. join : Wacht net als in .NET tot de thread afgelopen is. yield : Met deze methode kunnen we de time-slicing manipuleren. Door deze methode aan te roepen wordt de thread in de runnable state geplaatst waardoor andere threads de kans krijgen om uitgevoerd te worden. Hoewel de Thread class ook over een stop, suspend en resume beschikt gaan we hier niet dieper op in omdat deze methods deprecated zijn. Dit houdt in dat ze niet meer gebruikt zullen worden. En dit om de redenen zoals we die ook bij .NET vermeldt hebben. Ook hier is het dus aangewezen als we een thread willen stoppen dat we een boolean gebruiken om zo via gewone if-else logica de run methode te laten eindigen. De Thread class beschikt ook nog over een static sleep method. Deze methode stopt de methode voor een minimum aantal milliseconden dat is opgegeven als parameter van de sleep method.
4.4.4
Synchronization
Als we in Java variabelen willen gaan beveiligen voor thread dan kunnen we dit doen door gebruiken te maken van het keyword synchronized. Het keyword synchronized is de evenknie van het lock statement uit .NET. import java . util . ArrayList ;
ing. Gaillez Michael
67
4. Threading
public class PersonList { ArrayList _persons = new ArrayList () ; public void add ( string person ) { synchronized ( this ) { _persons . add ( person ) ; } } }
Indien we de volledige methode willen gaan beveiligen dan kunnen we hetzelfde keyword gaan gebruiken in de class definitie van de method. import java . util . ArrayList ; public class PersonList { ArrayList _persons = new ArrayList () ; synchronized public void add ( string person ) { _persons . add ( person ) ; } }
4.4.5
Signaling
Signaling is iets wat heel sterk ingebakken zit in het Java platform. Elk object in Java kan aan signaling gaan doen en dit om de eenvoudige reden dat alles uiteindelijk erft van de Object class. Dit betekent dat elke class in Java de volgende methodes heeft: wait : Dit gaat de thread in de wait state plaatsen voor dit object tot een andere thread de notify of notifyAll method aanroept. Belangrijk bij deze method is dat ze een InterruptedException kan gooien in het geval ergens de interrupt method van een thread wordt aangeroepen. We zullen dit dus altijd in een try-catch block moeten zetten. notify : Deze methode zal een thread die de wait methode heeft aangeroepen voor het huidige object herstarten. Als meerdere threads dit hebben gedaan heb je geen controle over welke thread zal herstart worden. In dat geval is het beter om notifyAll te gebruiken. notifyAll : Deze methode zal alle threads herstarten die de wait method hebben aangeroepen voor dit object. public class SignaledWorker implements Runnable { public static void Main ( string [] args ) { SignaledWorker worker = new SignaledWorker () ; ThreadStart runner = new ThreadStart ( worker ) ; Thread trd1 = new Thread ( runner ) ; Thread trd2 = new Thread ( runner ) ; trd1 . Start () ;
ing. Gaillez Michael
68
4. Threading
trd2 . Start () ; worker . Continue () ; worker . Continue () ; worker . Continue () ; } synchronized public void run () { while ( true ) { try { this . wait () ; } catch ( Interrupt e d E x c e p t i o n e ) System . err . println ( e ) ; } // Doe iets ... } } synchronized public void Continue () { this . notifyAll () ; } }
4.5
Samenvatting en conclusies
Threads aanmaken in zowel .NET als in Java is een relatief eenvoudige taak. Globaal gezien komt het er op neer dat je een methode moet voorzien met code die op een aparte thread moet worden uitgevoerd en dan het thread object aanmaken en starten. Java heeft geen ThreadPool zoals we die in .NET kennen maar deze ThreadPool kan in .NET applicaties significante verbetering en vereenvoudiging opleveren in de manier waarop we threads gaan beheren. In beide talen zijn methodes voorzien om threads te stop of te suspend/resumen maar in geen van beide talen is het aangeraden om deze te gaan gebruiken. Java gaat hier zelfs al een stap verder in door te zeggen dat deze methodes deprecated zijn en dus hoogst waarschijnlijk in volgende versies van Java niet meer zullen aanwezig zijn. Reden te meer om ze niet meer te gaan gebruiken dus. Een stuk complexer wordt het als we synchronization en signaling willen gaan toepassen. De beide termen vergen een goed inzicht in wat de processor juist gaat doen voor we deze zaken ook effectief goed kunnen aanwenden. Hoewel alles synchronized maken in een applicatie zowel in .NET als in Java een veilige oplossing lijkt, is het toch niet the way to go. Dit om de eenvoudige reden dat overdreven of onjuist gebruik kan leiden tot significant performantieverlies, zover zelfs dat het voordeel van threads gebruiken bijna verloren gaat. In .NET hebben we iets meer mogelijkheden om syning. Gaillez Michael
69
4. Threading
chronization te verfijnen maar dit gaat automatisch gekoppeld met een iets grotere complexiteit. Verfijnen in dit geval betekent beter inzicht hebben om te vermijden dat je fouten programmeert. In Java is dit heel rechtlijnig wat maakt dat het eenvoudiger is en bijgevolg ook minder kans op fouten heeft. De kans dat je uwzelf in een deadlock programmeert in Java is aanzienlijk kleiner dan dit in .NET het geval is. Voor signaling is dit niet anders. In .NET heb je weer meerdere mogelijkheden waardoor je de aanpak iets kan verfijnen maar de complexiteit daarmee ook verhoogd. In Java is signaling eigenlijk iets wat standaard ingebakken zit door het feit dat het al aanwezig is in de Object class waar iedere andere class uiteindelijk van erft.
ing. Gaillez Michael
70
Hoofdstuk 5
Services 5.1
Inleiding
Hedendaagse operating systemen moeten vaak applicaties draaien die ergens op de achtergrond werken. Bovendien moeten sommige van die applicaties beschikbaar blijven ook al is er op de machine niemand aangemeld. Denk maar aan de meeste servers die functioneren als webserver. Hier hebben we meestal een IIS of Apache die continue in de achtergrond blijft draaien. Bovendien willen we ook dat deze zaken opstarten van zodra de computer wordt opgestart en pas terug stoppen als de machine wordt afgesloten. Dergelijke applicaties noemen we services. Dergelijke applicaties hebben ook weinig of geen interactie met de gebruikers dus beschikken ze meestal ook niet over een user interface. In dit hoofdstuk gaan de dan ook eens bekijken hoe we dergelijke services kunnen aanmaken.
5.2 5.2.1
NT-services in .NET NT-service als hosting environment
NT-services wordt beschouwd als ´e´en van de drie hosting environments in Windows. Naast NT-services zijn er ook nog IIS/ASP.NET en COMservices. Onder hosting environment verstaan we hier niet het hosten van webapplicaties maar wel als een omgeving waarbinnen we een bepaalde functionaliteit continue kunnen ter beschikking stellen. Deze hosting environments verschillen op een aantal vlakken fundamenteel van elkaar. Zo is een service beperkt tot ´e´en proces waar IIS/ASP.NET en COM-services hun workload kunnen verdelen over meerdere processen. Hoewel alle drie de hosting environments hun functionaliteit continue ter beschikking stellen is er toch een substantieel verschil tussen IIS/ASP.NET enerzijds en NT en COM-services anderzijds. IIS/ASP.NET is vooral request based dit wil zeggen dat we een verzoek moeten versturen alvorens er een actie wordt uitgevoerd. Bij NT en COM-services kunnen we een actie zo goed als continue laten lopen. Een laatste onderdeel waarin ze sterk verschillen is de 71
5. Services
manier waarop er interactie mogelijk is met de hosting environment en ook de mogelijkheden die ze bieden. Bij NT-services is dit eerder beperkt terwijl bij COM-services en IIS/ASP.NET dit stukken uitgebreider is. NT-Services zijn naast IIS/ASP.NET het gemakkelijkst te implementeren. De keuze van hosting environment is niet altijd een eenvoudige keuze en hangt sterk af van de gekozen architectuur.
5.2.2
Eigenschappen van een NT-service
Om NT-services te kunnen maken is het belangrijk dat we een duidelijk zicht hebben op de verschillen tussen een NT-service en een standaard windows applicatie: • Een NT-service kan starten vooraleer een gebruiker aanmeldt. Windows houdt namelijk in de registry een lijst bij van welke services er zijn en of die moeten worden opgestart bij het booten. • Een NT-service kan opereren onder verschillende accounts die kunnen verschillen van de aangemelde gebruiker. Eerst en vooral omdat een NT-service zo goed als constant werkt en de service moet kunnen starten voor dat een gebruiker aanmeldt. Bovendien kan het zijn dat een service in een andere security context moet werken dan de aangemelde gebruiker. • Een NT-service heeft geen user interface omdat het geen interactie ondersteund met de desktop. NT-services werken in een ge¨ısoleerde omgeving waar geen GUI aan verbonden is. Als er toch een zeker vorm van interactie zou nodig zijn dan moet dit gebeuren via de windows API want dit zit niet ingebouwd in .NET. • We haalden reeds eerder aan dat Windows een lijst van services bijhoudt in de registry. Je kan dit zien in de registry editor onder: HKLM\SYSTEM\CurrentControlSet\Services Om uw eigen services ook in deze lijst op te nemen is een speciale installatie procedure nodig. Dus gewoon de .exe uitvoeren van de service zal niet volstaan. • Elke service werkt samen met de Service Control Manager. De Service Control Manager zorgt voor een interface naar de NT-service. Als je wil communiceren met de service bvb starten of stoppen dan moet dit gebeuren via deze Service Control Manager. De Service Control Manager is een programma dat draait op het niveau van het operating systeem maar het heeft een user interface die je kan benaderen via Services uit de Administrative tools van de Control Panel van het Windows OS. We kunnen ook via gespecialiseerde classes met deze Service Control Manager communiceren.
ing. Gaillez Michael
72
5. Services
5.2.3
Een NT-service bouwen
Om een NT-service te bouwen zijn er een aantal classes die van cruciaal belang zijn. Enerzijds is er de class die functionalteit van de service gaat moeten omschrijven. Anderzijds hebben we ook gezegd dat we een service moeten installeren dus gaan we classes moeten voorzien die de installatie van een service mogelijk maken. Hiervoor moeten referenties gelegd worden naar de assemblies System.Configuration.Install en naar System.ServiceProcess. Laten we deze classes eens overlopen: • System.ServiceProcess.ServiceBase: Deze class zorgt voor een base class voor elke service. Elke class die code voor een service bevat zal dus moeten erven van ServiceBase. We kunnen meerdere services aanmaken binnen ´e´en process maar daarvoor moeten we meerdere classes aanmaken die erven van ServiceBase. • System.Configuration.Install.Installer: Dit is een algemene class die installatie taken kan voltooien voor tal van zaken. We moeten in een service ´e´en class maken die erft van deze base class zodat we de service kunnen installeren onder windows. – System.ServiceProcess.ServiceProcessInstaller: Deze class bevat informatie die nodig is om de executable te installeren die de services bevat. – System.ServiceProcess.ServiceInstaller: Waar ServiceProcessInstaller informatie bevat om de executable in zijn geheel, bevat ServiceInstaller informatie over een specifieke service uit die executable. Als er meerdere services zitten in ´e´en executable zullen er ook meerdere ServiceInstaller objecten zijn nl. voor elke service ´e´en. Voor het overgrote deel kunnen we de creatie van al die installer classes overlaten aan Visual Studio. Het enige wat dus echt belangrijk zal zijn dat je weet voor wat welke property staat van deze classes. Bij ServiceBase ligt dit iets anders aangezien elke service erft van deze class en we dus moeten weten wat voor wat staat. De System.ServiceProcess.ServiceBase class De service base class is de class waar elke service die we maken in Visual Studio .NET van erft. De ServiceBase class bezit de volgende methoden die overridable zijn: OnStart(string[] args) : Deze methode wordt aangeroepen als de service wordt gestart. Hier plaats je de initialisatie logica van de service. De args string array bevat eventuele argumenten die je kan opgeven bij het starten van de service. OnStop() : Methode die wordt aangeroepen bij het stoppen van de service of een shutdown. Hier bevindt zich de eventuele cleanup die moet
ing. Gaillez Michael
73
5. Services
gebeuren. OnPauze() : Methode die wordt aangeroepen bij het pauzeren van de service. OnContinue() : Methode die wordt aangeroepen bij het verderzetten van de service. OnShutdown() : Methode die wordt aangeroepen indien de pc wordt afgesloten. Hier echter geen clean up logica plaatsen anders wordt die 2 maal uitgevoerd (OnStop wordt ook aangeroepen). Ideaal voor eventuele logging, . . . OnCustomCommand(int command) : Is een methode die wordt aangeroepen als een extern programma een custom command naar de service stuurt. Merk op dat je enkel een numerieke waarde kan meegeven aan deze methode. Dus uw commands moeten hard gecodeerd zitten in uw service. Boven dien moet deze numerieke waarde tss 128 en 256 liggen. Numerieke waarden onder 128 zijn gereserveerd voor het systeem! ´ en van Erven van deze class en de methods overriden is niet voldoende. E´ de classes moet ook een Main method bevatten zodat de services kunnen ge¨ınitialiseerd worden. Deze main method ziet er dan simpelweg als volgt uit: public static void Main ( String args ) { System . ServiceProcess . ServiceBase [] ServicesToRun ; ServicesToRun = new System . ServiceProcess . ←ServiceBase [] { new FirstService () , new ←SecondService () }; System . ServiceProcess . ServiceBase . Run ( ServicesToRun ) ←; }
Wat deze methode doet is behoorlijk eenvoudig. We maken een array van alle services die aanwezig zijn in de executable en daarna gebruiken we de static Run method van ServiceBase om deze te initialiseren. Als een service niet wordt ge¨ınitialiseerd door de Run method dan zal deze niet werken!!! De installers We hebben al vermeld dat we het maken van de installers gemakkelijk kunnen laten afhandelen door Visual Studio zelf. Het enige wat je hiervoor moet doen is rechts klikken op de designer van een service en daar de ”Add Installer” optie kiezen. Dit zal al het nodig werk voor u uitvoeren. De subclass van Installer behoeft meestal geen verder bewerking. De instanties van ServiceProcessInstaller en ServiceInstaller bevatten echter enkele
ing. Gaillez Michael
74
5. Services
belangrijke properties die we kunnen of moeten instellen. De properties voor ServiceProcessInstaller zijn: • Account: We kunnen via deze property instellen onder welk account type het proces gaat werken. We kunnen kiezen uit ´e´en van de systeem accounts zijnde: LocalSystem, LocalService of NetworkService of we kunnen kiezen voor User. Als we kiezen voor User moeten we ook de username en paswoord van de gewenste user ingeven. Deze laatste keuze kan handig zijn als je restricties wil opleggen over wat hij wel en niet kan doen of als je wil werken in een netwerkomgeving met bvb een domein. Een bijkomende opmerking is dat je best een speciale user aanmaakt om dit te doen. Als je een bestaande live user gebruikt kan dit wel eens tot problemen leiden bvb. als die gebruiker zijn paswoord wijzigt. • Username: Als we Account op User zetten dan bepaald dit welke gebruikersnaam zal gebruikt worden om dit process te runnen. Als we deze property leeg laten dan zal dit automatisch bij installatie gevraagd worden. • Password: Deze property bevat het paswoord dat hoort bij de gebruiker opgegeven in Username. Als we dit veld leeg laten zal dit paswoord gevraagd worden bij installatie. De ServiceInstaller heeft ook een aantal properties die we voor iedere service individueel kunnen instellen: • Displayname: Dit bevat de naam van de service zoals die zal worden weergegeven in de Service Manager van de Administrative tools. Deze naam kan verschillen van de classname van de service zelf en van de naam van de executable waarin de service zit. • StartType: Hiermee kunnen we aangeven hoe de service zal worden gestart. Dit kan enerzijds manueel gebeuren via de Service Manager of Automatisch bij het opstarten van Windows anderzijds. Je kan dit ook op disabled plaatsen waardoor de service niet kan gestart worden. Deze property kan je altijd nog achteraf instellen in de service manager indien gewenst. • ServiceName: Dit is de naam van de serviceclass waarmee de ServiceInstaller overeen komt. Als de naam van de service class wijzigt moet ook deze property wijzigen. • HelpText: Deze property kan eventuele extra informatie over de service bevatten. Je kan deze property niet meer setten in het .NET 1.1 framework. Als je dit toch wenst te doen zal je eerst moeten erven van ServiceInstaller en daar de HelpText property overriden en dan deze toevoegen aan uw Installer class. Hier kan je dus niet meer rekenen op de designer en je zal dit dan ook handmatig moeten doen.
ing. Gaillez Michael
75
5. Services
5.2.4
Installatie van NT-Services
Om een service te installeren moet worden gebruik gemaakt van InstallUtil in de Visual Studio command prompt. Als argument geef je de executable mee waarin uw service(s) zich bevindt. InstallUtil myservice . exe
Als de service correct ge¨ınstalleerd is zal deze verschijnen in de Service Control manager van Windows. Een service terug uninstallen kan dan als volgt in de command prompt: InstallUtil / u myservice . exe
Belangrijk hierbij op te merken is dat de versie van de ge¨ınstalleerde service en de versie van de executable waarmee men de uninstall uitvoert met elkaar overeen komen anders kunnen er problemen optreden. Als de uninstall niet werkt zoals gewenst kan men in uiterste nood nog altijd de entry wissen in de registry editor onder: HKLM\SYSTEM\CurrentControlSet\Services Kies daar dan de naam van de service en delete de volledige sleutel. Probeer dit echter te vermijden want de verkeerde service verwijderen op deze manier kan catastrofale gevolgen hebben.
5.2.5
Debuggen van NT-Services
Een service kan men niet debuggen zoals men dit doet met een ander Visual Studio Project. Dit komt omdat de executable van de service niet kan worden uitgevoerd zoals een gewone applicatie. Om een service te debuggen moet het project geopend zijn en de volgende stappen ondernomen worden: • Installeer en start de service • Selecteer Processes van het debug menu in Visual Studio • In het Processes venster moet het vinkje Show System processes gezet zijn. • Kies de exe van de service uit de lijst en klik op Attach. • Selecteer de Common Language Runtime uit de lijst en klik OK • Sluit nu terug het venster Visual Studio zal hierna in debugging mode gaan waardoor de service kan debugged worden zoals ieder andere applicatie in Visual Studio. Om het debuggen te stoppen kiest men Stop debugging uit het Debug menu van Visual Studio. Hoewel men op deze manier een service kan debuggen, is het toch aangeraden om de service eerst uit de werken als een gewone windows of console applicatie en zo te test en debuggen.
ing. Gaillez Michael
76
5. Services
5.2.6
Interactie met een NT-Service
We kunnen een service manipuleren via de Service Control Manager van Windows of via DOS commands. In de praktrijk zal men echter nog een Windows applicatie voorzien om te communiceren met de service. Voorbeelden hiervan zijn de SQL Service Manager van SQL-Server, IIS admin console, Printer Manager, . . . We kunnen een dergelijke applicatie maken voor onze eigen services door gebruik te maken van de ServiceController class uit de namespace System.ServiceProcess. We moet ook hiervoor een reference leggen naar de System.ServiceProcess assembly. In de constructor van deze class geven we de servicenaam mee waarop we wensen te connecteren. using System . ServiceProcess ; ... ServiceController myController = new ServiceController (" MyService ")
←-
...
Methods De ServiceController class beschikt over de volgende methods(belangrijkste): Start(string[] args) : Starten van de service met eventuele argumenten Stop() : Stoppen van de service Pause() : Pauzeren van de service Continue() : Verderzetten van de service Refresh() : Haalt de meest recente status waarden op van de service (Is nodig want de service kan ook gemanipuleerd zijn door een ander programma zonder ons medeweten). ExecuteCommand(int command) : Command met opgegeven nummer uitvoeren. Properties De belangrijkste properties die je kan raadplegen/zetten zijn de volgende: CanStop : Boolean die aanduid of je de service kan stoppen ServiceName : Naam van de service waarmee we connecten Status : Enumeration met de status van onze service. (Zie ServiceControllerStatus enumeration in Visual Studio Help voor de mogelijke waarden)
ing. Gaillez Michael
77
5. Services
ServiceType : Plaats deze property op ServiceType.Win32OwnProcess. De andere opties zijn voor meer complexe services zoals hardwardeservices geschreven in C++
5.3
Java service wrappers
Het maken van services in Java is niet zo voor de hand liggend zoals dat in vele andere talen zoals C++, C# en VB.NET wel het geval is. Bij C++ is dit haalbaar omdat we het operating systeem hiermee rechtstreeks kunnen benaderen en compileren naar native code. In de .NET talen hebben we het .NET framework die de interactie met het Windows voor ons oplost. Java is sterk platform onafhankelijk waardoor we niet zomaar het operating systeem rechtstreeks kunnen benaderen en bijgevolg iets afleveren in native code. Daarom gaan we moeten gebruik maken van een tussenlaag die de interactie maakt tussen de Java Virtual Machine en het OS. Deze laag vertaalt de native instructies van het OS naar Java en zo’n laag noemen we een wrapper. In grotere applicaties zal men er voor opteren om zelf een wrapper te programmeren. Voor kleinere applicaties kunnen we ons redden met bestaande open source Java service wrappers. Een voorbeeld van een dergelijk open source wrapper is die van Tanuki Software te vinden op http://wrapper.tanukisoftware.org/doc/english/introduction.html. Op deze site vind je een volledige uitgewerkte manual om een Java service te maken die kan gebruikt worden voor Windows als voor Unix based operating systemen.
ing. Gaillez Michael
78
Hoofdstuk 6
Asynchrone function calls in .NET 6.1
Inleiding
Als we een methode aanroepen dan moet de client applicatie meestal wachten tot de methode compleet is uitgevoerd alvorens de applicatie met een andere activiteit kan verder gaan. Maar er zijn heel wat gevallen waarbij men methodes asynchroon wil uitvoeren. Dit betekent dat je een methode start en onmiddellijk terug keert naar de applicatie terwijl de methode dan effectief wordt uitgevoerd op de achtergrond, als de methode volledig is uitgevoerd moet deze laten weten aan de applicatie dat ze be¨eindigd is. Een oplossing hiervoor is de methode doorgeven aan een andere thread en dan op een of andere manier terug keren naar de client als de thread be¨eindigt is. Hoewel deze manier van werken perfect werkbaar is heeft ze toch een groot nadeel. Dit zou namelijk inhouden dat voor elke methode die asynchroon moet uitgevoerd worden een thread moet worden gemaakt en dan functionaliteit moet ingebouwd worden om een callback te doen in de applicatie. Aangezien bijna iedere methode wel een andere signatuur heeft zou de programmeur telkens dit systeem moeten uitbouwen en dus het warm water vele malen opnieuw uitvinden. Om op deze problematiek een antwoord te bieden heeft Microsoft een standaard systeem ontwikkelt om dergelijke Asynchrone calls te doen.
6.2
Noden voor een Asynchroon systeem
Vooraleer we spreken over het hoe en wat van asynchrone calls in .NET moeten we eerst wat inzicht verwerven in enkele specifieke noden die optreden bij asynchrone calls: • Een eerste voorwaarde is dat een object of component dezelfde code moet gebruiken om zowel asynchrone als synchrone calls uit te voeren. Zo moet de programma logica maar ´e´en keer worden aangemaakt
79
6. Asynchrone function calls in .NET
onafhankelijk of we de code synchroon of asynchroon uitvoeren. • Een gevolg van de eerste voorwaarde is dat de client kan beslissen of het de code synchroon of asynchroon kan uitvoeren. • Een client moet ook meerdere asynchrone calls tegelijk kunnen uitvoeren zonder dat deze calls met elkaar in conflict komen of door elkaar gehaspeld worden. • Terugkeerwaardes zijn niet beschikbaar bij het starten van een asynchrone uitvoering dus moeten die waardes achteraf kunnen teruggewonnen worden. • Als er zich fouten voordoen dan moeten die ook teruggestuurd worden naar de client zodat ze eventueel kunnen worden afgehandeld. • Last but not least het systeem voor asynchrone calls moet simpel zijn om te gebruiken. De details van het opstarten van threads enz moet mooi afgeschermd zijn van de eindgebruiker.
6.3
Delegates herbekeken
We hebben reeds gezien dat we met een delegate een referentie naar een methode kunnen opslaan. Een delegate wordt gebruikt om het aanroepen van een methode door te geven van de client class naar de delegate class. Nemen we bijvoorbeeld een class Calculator: public class Calculator { public int Add ( int num1 , int num2 ) { return num1 + num2 ; } public int Subtract ( int num1 , num2 ) { return num1 - num2 ; } }
In plaats van de methode Add rechtstreeks te gebruiken zouden we nu kunnen een delegate Operation kunnen maken om dit te doen: public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { public int Add ( int num1 , int num2 ) { return num1 + num2 ; } public int Subtract ( int num1 , num2 )
ing. Gaillez Michael
80
6. Asynchrone function calls in .NET
{ return num1 - num2 ; } public static void Main ( string [] args ) { Calculator calc = new Calculator () ; Operation opp = new Operation ( calc . Add ) ; int result = opp (2 ,3) ; Console . WriteLine ( result . ToString () ) ; } }
In dit voorbeeld hebben we maar weinig verbetering aangebracht. Mocht de Add methode lang geduurd hebben dan zou de delegate ook moeten wachten hebben op het be¨eindigen van de methode. Maar op de achtergrond is er in feite meer gebeurd dan we wel zouden verwachten. De definitie van onze delegate heeft er eigenlijk voor gezorgd dat de compiler een speciale class heeft aangemaakt om met deze delegate iets te kunnen doen dus: public delegate int Operation ( int num1 , int num2 ) ;
Heeft er voor gezorgd dat de compiler de volgende class heeft aangemaakt: public class Operation : System . M ulticas tDelega te { public Operation ( object target , int methodPtr ) {...} public virtual int Invoke ( int num1 , num2 ) {...} public virtual IAsyncResult BeginInvoke ( int num1 , ←int num2 , AsyncCallBack callBack , object state ) {...} public virtual int EndInvoke ( IAsyncResult result ) {...} }
Door de creatie van deze class heeft .NET voor ons al het vuile werk opgeknapt om asynchrone calls te maken. En we kunnen nog steeds gewone synchrone calls maken ook via de delegate. Door opp(2, 3) in ons voorbeeld wordt in feite de Invoke methode van onze delegate class aangeroepen. We hoeven (en kunnen) dit niet zelf te doen omdat de compiler dit voor ons automatisch omzet. De methodes BeginInvoke en EndInvoke kunnen we nu gebruiken om asynchrone calls te doen.
ing. Gaillez Michael
81
6. Asynchrone function calls in .NET
6.4
Asynchroon programming model
Om methods asynchroon te kunnen uitvoeren zijn er meerdere threads nodig. Het zou echter een verspilling van resources zijn voor .NET als het voor elke asynchrone call een nieuwe thread moeten maken en uitvoeren. Het zou beter zijn als we reeds bestaande threads zouden kunnen hergebruiken en .NET kan dit doen door de ThreadPool class. Wat .NET dus doet is een thread uit de thread pool nemen om zo de methode op een andere thread uit te voeren en dit alles zit netjes verwerkt in het .NET framework. Het enige wat de BeginInvoke doet is de request in de threadpool plaatsen en daarmee is de kous. Dit zorgt dat client maar heel kortstondig moet wachten op BeginInvoke. Endvoke dan dient om parameters op te vangen of error handling te doen wanneer de methode volledig is uitgevoerd. Al de rest wordt geregeld door de thread uit de Thread pool.
6.4.1
BeginInvoke en EndInvoke gebruiken
De compiler maakt dus een BeginInvoke en een EndInvoke method die de volgende vorm aanneemt: public virtual IAsyncResult BeginInvoke ( < parameters > , ←AsyncCallback callback , object state ) ; public virtual < return value > EndInvoke ( IAsyncResult ) ;
BeginInvoke accepteert dus de parameters van de originele signatuur van de delegate. De return value van de originele delegate zit dus verwerkt in de EndInvoke method. BeginInvoke accepteert ook nog 2 extra parameters die niet in de originele signatuur van de delegate zitten namelijk AsyncCallback callback en object state. AsyncCallback is een delegate naar de methode die moet worden aangeroepen als de methode uitvoering afgehandelt is. De variabele state is om het even welk object met informatie die eventueel kan nodig zijn bij het afhandelen van de methode. Deze 2 laatste parameters zijn optioneel dus dit wil zeggen dat je hier null mag meegeven. Als we niet zouden ge¨ınteresseerd zijn in het resultaat van de opsomming zou ons eerder gebruikt voorbeeld er dan zou kunnen uitzien: public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { public int Add ( int num1 , int num2 ) { return num1 + num2 ; } public int Subtract ( int num1 , num2 ) { return num1 - num2 ; }
ing. Gaillez Michael
82
6. Asynchrone function calls in .NET
public static void Main ( string [] args ) { Calculator calc = new Calculator () ; Operation opp = new Operation ( calc . Add ) ; opp . BeginInvoke (2 , 3 , null , null ) ; } }
Het Calculator object weet hier niet dat de code asynchroon wordt uitgevoerd. Hetzelfde Calculator object kan dus nu gebruikt worden voor zowel synchrone als asynchrone afhandeling. Omdat we in delegate zowel static als instance methods kunnen opslaan, kunnen we BeginInvoke ook gebruiken op static methods asynchroon af te handelen. De vraag die nu blijft is hoe halen we het resultaat terug op? Hiervoor moeten we eerst eens wat dieper ingaan op de IAsyncResult interface en de AsyncResult class.
6.4.2
IAsyncResult interface
In de requirements hadden we gezegd dat we meerdere asynchrone calls moesten kunnen uitvoeren zonder dat de calls met elkaar conflicteren of door elkaar gegooid worden. Nemen we nu terug ons Calculator voorbeeld om dit probleem te schetsen: public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { public int Add ( int num1 , int num2 ) { return num1 + num2 ; } public int Subtract ( int num1 , num2 ) { return num1 - num2 ; } public static void Main ( string [] args ) { Calculator calc = new Calculator () ; Operation opp = new Operation ( calc . Add ) ; opp . BeginInvoke (2 , 3 , null , null ) ; opp . BeginInvoke (5 , 3 , null , null ) ; } }
We zien nu dat we de delegate gebruiken voor twee asynchrone calls met verschillende parameters. Als we nu het resultaat van die optelling zouden willen dan gaan we EndInvoke van de delegate moeten aanroepen. Hier knelt nu het schoentje. . . Als we EndInvoke aanroepen voor welke call doen we dit dan? Want beide calls worden met dezelfde delegate gedaan, dus we kunnen ing. Gaillez Michael
83
6. Asynchrone function calls in .NET
op basis daarvan geen onderscheid maken. We moeten dus bij EndInvoke iets hebben wat het onderscheid kan maken dus de eerste en de tweede call en hier komt de interface IAsyncResult bij kijken. Een object dat de interface IAsyncResult implementeerd, bepaalt uniek welke method werd gestart met BeginInvoke. Laten we eens de definitie van IAsyncResult bekijken: public interface IAsyncResult { object AsyncState { get ; } WaitHandle AsyncWaitHandle { get ; } bool C om ple te dSy nc hro n o u s l y { get ; } bool IsCompleted { get ; } }
We zien dat een object van het type IAsyncResult een WaitHandle bevat en net die WaitHandle bepaalt om welke call het ging. Daarnaast zien we ook nog dat we de state die we eventueel hebben meegegeven in AsyncState zit en dat we een bool waarde hebben die aangeeft of de asynchrone call vervolledigd is. We kunnen dit nu gaan toepassen. public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { // Code zoals voorheen ... public static void Main ( string [] args ) { Calculator calc = new Calculator () ; Operation opp = new Operation ( calc . Add ) ; IAsyncResult asyncResult1 = opp . BeginInvoke (2 , 3 , null , null ) ; IAsyncResult asyncResult2 = opp . BeginInvoke (5 , 3 , null , null ) ;
←←-
// Do something Thread . Sleep (1000) ; int result ; result = opp . EndInvoke ( asyncResult1 ) ; Console . WriteLine ( result . ToString () ) ; result = opp . EndInvoke ( asyncResult2 ) ; Console . WriteLine ( result . ToString () ) ; } }
Door het meegeven van de IAsyncResult objecten kunnen we nu uniek bepalen om welke call het ging en dus ook het resultaat achterhalen, ook al hebben we maar ´e´en delegate.
ing. Gaillez Michael
84
6. Asynchrone function calls in .NET
Er zijn echter enkele belangrijke opmerkingen bij het aanroepen van EndInvoke: • Eerst en vooral is het zo dat EndInvoke wacht tot de asynchrone call volledig is be¨eindigd. • EndInvoke kan maar ´e´en keer aangeroepen worden voor elke asynchrone call. Als je EndInvoke meerdere malen probeert aan te roepen met dezelfde IAsyncResult krijg je een InvalidOperationException. • We zagen reeds eerder dat we meerdere methodes kunnen toekennen aan ´e´en delegate met de += operator. Als je echter asynchrone calls wil gebruiken mag de delegate maar ´e´en target method bevatten. Als er toch meerdere target methods in de delegates zitten, wordt er een ArgumentException gegooid. • IAsyncResult mag alleen meegegeven worden aan EndInvoke van dezelfde delegate waarmee de call is gemaakt. IAsyncResult meegeven aan een andere delegate zal resulteren in een InvalidOperationException. De volgende code illustreert elk van deze fouten: public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { // Code zoals voorheen ... public static void Main ( string [] args ) { Calculator calc = new Calculator () ; Operation opp1 = new Operation ( calc . Add ) ; Operation opp2 = new Operation ( calc . Add ) ; IAsyncResult asyncResult1 = opp1 . BeginInvoke (2 , 3 , ←null , null ) ; // Do something Thread . Sleep (1000) ; int result ; result = opp1 . EndInvoke ( asyncResult1 ) ; // een tweede maal aanroepen geeft een I n v a l i d O pe r a t i o n E x c e p t i o n result = opp1 . EndInvoke ( asyncResult1 ) ;
←-
IAsyncResult asyncResult2 = opp1 . BeginInvoke (5 , 3 , ←null , null ) ; // IAsyncResult meegeven aan een andere delegate geeft een I n v a l i d O p e r a t i o n E x c e p t i o n result = opp2 . EndInvoke ( asyncResult2 ) ;
←-
opp1 = null ; opp1 += new Operation ( calc . Add ) ;
ing. Gaillez Michael
85
6. Asynchrone function calls in .NET
opp1 += new Operation ( calc . Subtract ) ; // Meerdere methodes koppelen resulteert in een ArgumentException opp1 . BeginInvoke (2 , 3 , null , null ) ;
←-
} }
We kunnen nu al met IAsyncResult en EndInvoke het resultaat van onze asynchrone call opvragen. Maar in onze code staan BeginInvoke en EndInvoke nog steeds in dezelfde method. Om asynchrone calls optimaal te kunnen gebruiken willen we BeginInvoke aanroepen en dan verwittigd worden wanneer de call be¨eindigd is. Hiervoor moeten we dus nog een stap verder gaan in onze redenering te beginnen met de AsyncResult class.
6.4.3
AsyncResult class
Vaak is het zo dat de ene methode de asynchrone call initieert door BeginInvoke en een andere methode de call afwerkt met EndInvoke. Dat zou dus betekenen dat we zowel de IAsyncResult als de delegate zouden moeten doorgeven aan de methode die EndInvoke moet aanroepen. Dit is logisch want we hebben IAsyncResult nodig voor de methode uniek te bepalen en de delegate moet dezelfde zijn als die waar we BeginInvoke hebben aangeroepen. Gelukkig is er hiervoor een iets gemakkelijkere oplossing beschikbaar want het IAsyncResult object heeft eigenlijk de delegate bij zich waarmee het gecre¨eerd is. Dit komt omdat de IAsyncResult referentie eigenlijk een instantie van de class AsyncResult bevat. Laten we eens kijken naar de definitie van zo’n AsyncResult class: public class AsyncResult : IAsyncResult , IMessageSink { // IAsyncResult implementatie public object AsyncState { get ; } public WaitHandle AsyncWaitHandle { get ; } public bool C omp le te d S y n c h r o n o u s l y { get ; } public bool IsCompleted { get ; } // Andere properties public bool EndInvokeCalled { get ; set ; } public object AsyncDelegate { get ; } // IMessageSink implementation ... }
We zien dus dat AsyncResult de interface IAsyncResult implementeert en we dus een AsyncResult object kunnen opslaan in een IAsyncResult referentie. We zien ook dat de AsyncResult class nog een belangrijke extra property heeft nl. AsyncDelegate. Die property bevat namelijk de delegate die de asynchrone call heeft gemaakt. Dit is belangrijk want hiermee kunnen we dus onze originele delegate terughalen en hoeven we hem dus ing. Gaillez Michael
86
6. Asynchrone function calls in .NET
niet mee te geven aan andere methodes. De AsyncResult class zit in de System.Runtime.Remoting.Messaging namespace dus we moeten ook deze importeren alvorens we de AsyncResult class gebruiken. Laten we dus ons voorbeeld wat aanpassen zodat we dit kunnen toepassen: using System . Runtime . Remoting . Messaging ; public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { public int Add ( int num1 , int num2 ) { return num1 + num2 ; } public int Subtract ( int num1 , num2 ) { return num1 - num2 ; } private static IAsyncResult _asyncResult ; public static void Main ( string [] args ) { Calculator calc = new Calculator () ; StartAsyncAdd (2 , 3 , calc ) ; int result = CompleteAsyncAdd () ; Console . WriteLine ( result . ToString () ) ; } public static void StartAsyncAdd ( int num1 , int num2 , ←Calculator calc ) { Operation opp = new Operation ( calc . Add ) ; _asyncResult = opp . BeginInvoke ( num1 , num2 , null , ←null ) ; } public static int CompleteAsyncAdd () { // Delegate is niet meer gekend dus we moeten het ophalen AsyncResult asyncResult = ( AsyncResult ) ←_asyncResult ; Operation opp = ( Operation ) asyncResult . ←AsyncDelegate ;
←-
// We hebben nu de delegate terug dus // nu nog EndInvoke aanroepen voor het resultaat return opp . EndInvoke ( _asyncResult ) ; } }
ing. Gaillez Michael
87
6. Asynchrone function calls in .NET
Uit het voorbeeld blijkt dat we dus onze delegate kunnen terughalen door IAsyncResult te casten naar een AsyncResult object. We houden de IAsyncResult referentie nu nog bij in een static member variabele, zodat we deze nog kunnen benaderen vanuit een andere method. Maar het liefst zouden we die member variabele ook nog zien verdwijnen en dit kunnen we doen door gebruik te maken van een callback. De methodes StartAsyncAdd en CompleteAsyncAdd en de variabele asyncResult zijn static omdat we deze willen aanroepen vanuit de Main method.
6.4.4
De Callback method gebruiken
We zouden via allerhande manieren controleren of de asynchrone call be¨eindigd is om dan EndInvoke aan te roepen maar de meest gemakkelijke manier is gewoon gebruik maken van een callback. Het principe van een callback is heel eenvoudig. We geven bij BeginInvoke een methode mee die moet worden uitgevoerd als de asynchrone method klaar is. We kunnen dus automatisch verwittigd worden door gebruik te maken van callbacks. Om callback te kunnen gebruiken moeten we een methode hebben met deze signatuur: < access modifier > void MethodName ( IAsyncResult asyncResult ) ;
←-
De reden hiervoor is eenvoudig. We kunnen namelijk met BeginInvoke een callback method meegeven via een delegate van het type AsyncCallback en de definitie van AsyncCallback is: public delegate void AsyncCallback ( IAsyncResult asyncResult ) ;
←-
We kunnen dus met callback het voorbeeld nu gemakkelijk gaan vereenvoudigen: using System . Runtime . Remoting . Messaging ; public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { public int Add ( int num1 , int num2 ) { return num1 + num2 ; } public int Subtract ( int num1 , num2 ) { return num1 - num2 ; } public static void Main ( string [] args ) { Calculator calc = new Calculator () ;
ing. Gaillez Michael
88
6. Asynchrone function calls in .NET
Operation opp = new Operation ( calc . Add ) ; AsyncCallback callback = new AsyncCallback ( ←OnComplete ) ; opp . BeginInvoke (2 , 3 , callback , null ) ; // Wachten tot de user iets intikt Console . Read () ; } public static void OnComplete ( IAsyncResult ←asyncResult ) { // Delegate is niet meer gekend dus we moeten het ophalen AsyncResult result = ( AsyncResult ) asyncResult ; Operation opp = ( Operation ) result . AsyncDelegate ;
←-
// We hebben nu de delegate terug dus // nu nog EndInvoke aanroepen voor het resultaat int sum = opp . EndInvoke ( asyncResult ) ; Console . WriteLine ( sum . ToString () ) ; } }
We zien dat we hier nergens meer onze IAsyncResult noch onze delegate moeten opslaan in een variabele. Deze worden beiden netjes doorgegeven via de callback. Het voorbeeld schets ook wat het basiswerk is wat we moeten vervolledigen voor het gebruiken van asynchrone calls met callback. We kunnen nu gemakkelijk dezelfde callback hergebruiken voor een aftrekking. De methode Subtract beantwoordt namelijk ook aan de delegate Operation: using System . Runtime . Remoting . Messaging ; public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { ... public static void Main ( string [] args ) { Calculator calc = new Calculator () ; Operation opp1 = new Operation ( calc . Add ) ; Operation opp2 = new Operation ( calc . Subtract ) ; AsyncCallback callback = new AsyncCallback ( ←OnComplete ) ; opp1 . BeginInvoke (2 , 3 , callback , null ) ; opp2 . BeginInvoke (5 , 3 , callback , null ) ; // Wachten tot de user iets intikt Console . Read () ; } public static void OnComplete ( IAsyncResult asyncResult ) {
ing. Gaillez Michael
←-
89
6. Asynchrone function calls in .NET
// Delegate is niet meer gekend dus we moeten het ophalen AsyncResult result = ( AsyncResult ) asyncResult ; Operation opp = ( Operation ) result . AsyncDelegate ;
←-
// We hebben nu de delegate terug dus // nu nog EndInvoke aanroepen voor het resultaat int sum = opp . EndInvoke ( asyncResult ) ; Console . WriteLine ( sum . ToString () ) ; } }
Zoals vermeldt, zijn er nog andere manieren om asynchrone calls af te handelen maar de methode met callback geniet de meeste voorkeur. Dit is omdat het systeem met callback dicht aanleunt bij event-driven toepassingen(zowat alle windows applicaties werken op dit systeem).
6.5
Asynchrone calls zonder delegates
Met delegate-based asynchrone calls zoals we die gezien hebben kunnen we elke method van een class asynchroon uitvoeren. Dit biedt een aanzienlijke meerwaarde maar je moet wel een delegate aanmaken waarvan de signatuur overeenkomt met de methode die je wil uitvoeren. Sommige operaties zoals netwerk of schijf-toegang, webrequests, webservice calls of messagequeing zijn over het algemeen trage operaties in vergelijking met de meeste code. In deze gevallen zal je heel vaak gebruik maken van asynchrone calls. Omdat je hiervoor niet speciaal een delegate zou moeten aanmaken, zit het asynchrone taken meestal ingebakken in de class door Begin
en Endmethodes. Deze methodes zijn heel gelijkaardig aan BeginInvoke en EndInvoke bij een delegate. Laten om dit principe te illusteren gaan we een voorbeeld met een file lezen gebruiken. using System . IO ; public class FileClient { pubic void AsyncRead () { bool useAsync = true ; FileStream fs = new FileStream (" myfile . txt " , ←FileMode . Open , FileAccess . Read , FileShare . None , ←1000 , userAsync ) ; AsyncCallback callback = new AsyncCallback ( ←OnComplete ) ; fs . BeginRead ( array , 0 , 10 , callback , fs ) ; } public void OnComplete ( IAsyncResult asyncResult ) { FileStream fs = ( FileStream ) asyncResult . AsyncState ←;
ing. Gaillez Michael
90
6. Asynchrone function calls in .NET
int bytesRead = fs . EndRead ( asyncResult ) ; fs . Close () ; // Vanaf hier zitten de gelezen bytes in de array ←... } byte [] array = new byte [2000]; }
BeginRead en EndRead vind je terug in elk Stream object dus dit geldt voor FileStream maar ook voor NetworkStream, MemoryStream,. . . Ook bij webservices worden dergelijke BeginXXX en EndXXX automatisch aangemaakt door de compiler.
6.6
Asynchrone calls en Synchronization
Het feit dat asynchrone calls op een thread van de thread van de threadpool worden uitgevoerd moet je dus ook rekening houden met synchronization. Als de method dus membervariabelen van een object gebruikt zal je dus locks moeten voorzien zodat er geen synchronization issues optreden. Boven dien moet je er rekening mee houden dan de callback method ook op de thread van de threadpool wordt en dus niet op de huidige thread. Zeker in het geval dat dezelfde callback methode wordt gebruikt voor meerdere asynchrone calls.
6.7
Asynchrone calls en Windows Forms
In de voorgaande sectie maakten we al een referentie naar thread-safety bij asynchrone calls. Dit is zeker het geval als we werken met een Windows applicatie. Stel dat het resultaat van een asynchrone call in een label op een form moet worden ingevuld. Hiervoor zou je dan wellicht in de callback code gebruiken die sterk gelijkt op het volgende: ... // Callback method public void OnComplete ( IAsyncResult asyncResult ) { ... int result = opp . EndInvoke ( asyncResult ) ; lblGetal . Text = result . ToString () ; ... } ...
Dit lijkt heel onschuldige code waar op het eerste zicht ook niks verkeerd mee is. Welnu niks is minder waar. Het feit dat de callback methode op een andere thread wordt uitgevoerd, heeft dit verstrekkende gevolgen voor ing. Gaillez Michael
91
6. Asynchrone function calls in .NET
WinForms. Dit heeft alles te maken met de Windows Message Loop die in elke Windows applicatie actief is.
6.7.1
Windows Message Loop
Een message loop is het hart van elke Windows applicatie, het is als het ware een pomp die de uitvoering van de applicatie stuurt. De delen van een Windows applicatie zoals forms, buttons, scroll bars, . . . communiceren met elkaar en ook met andere Windows applicaties en het operating systeem(bvb welke applicatie is er actief). Die communicatie gebeurt op basis van messages die worden doorgegeven aan een soort wachtlijst. Een message is niet veel meer dan een pakketje data die aangeeft aan een component dat er iets heeft plaatsgevonden. Dat iets noemen we events, dit kan onder andere een timer zijn die afvuurt, een druk op een toets op het keyboard, of het klikken of verplaatsen van de muis,. . . Een message omschrijft dan dat event en het Windows operating systeem bezorgt dit dan aan de gepaste applicatie door het in zijn wachtlijst te plaatsen. Een gigantische hoeveelheid van die messages worden doorgegeven aan een Windows applicatie zelfs als er niks lijkt te gebeuren. Als je in de taskmanager zou gaan kijken naar de performance indicators dan zou je merken dat het fel heen en weer bewegen van de muis al aanleiding heeft tot het verhogen van de processor activiteit. De message loop in de windows applicatie haalt deze messages ´e´en voor ´e´en van de wachtlijst en stuurt deze dan telkens door naar het juiste stuk code van de applicatie voor verdere verwerking. In de early days van Windows programmeren moest je deze message loop zelf gaan programmeren. In hedendaagse ontwikkel omgevingen zoals .NET, worden al deze dingen door het framework afgehandelt en hoeven we dit niet zelf te doen. Je kan dit nog altijd doen als je meer geavanceerde dingen wil maken maar voor de meeste toepassingen is dit niet nodig. Als je dus een WinForm als GUI wil dan moet je deze messageloop activeren door gebruik te maken van de Application.Run functie. Deze functie start de message loop en toont de opgegeven form. Bij een applicatie geschreven in C# zal je deze regel zien staan in de main method van de applicatie, bij VB.NET gebeurd dit op de achtergrond. De message loop blijft actief zolang de windows applicatie actief is. Deze message loop heeft nu natuurlijk enkele belangrijke implicaties waardoor we moeten opletten als we asynchrone calls of threads gebruiken. Het moet gezegd dat Microsoft hard werkt aan het elimineren van deze message loop door een nieuw concept WinFX genaamd. WinFX zal misschien al aanwezig zijn in de volgende versie van Windows nl. ”Longhorn”. Voorlopig kunnen we er nog niet onderuit dus moeten we er rekening mee houden.
ing. Gaillez Michael
92
6. Asynchrone function calls in .NET
6.7.2
Windows Message Loop vs. Asynchrone calls
De thread van de Windows Message Loop is verantwoordelijk voor alle updates van de GUI elementen van een Windows applicatie. Er ontstaat nu echter een probleem als we vanop een andere thread iets willen updaten aan onze GUI. Het is namelijk zo dat de Windows Message Loop thread de enige thread is die GUI mag en kan updaten zonder fouten. Als nu een update gebeurt van de GUI vanop een andere thread dan de Windows Message Loop thread kunnen er fouten optreden. Er zal niet altijd een fout optreden maar de kans bestaat wel dus moeten we dit proberen te vermijden. Aangezien asynchrone calls op een andere thread worden uitgevoerd, moeten we dus voorzichtig zijn als we de GUI willen updaten met resultaten van een asynchrone call. Wanneer we gebruik maken van een callback dan wordt die callback functie op een andere thread uitgevoerd dan de Message Loop thread. We zullen dus op ´e´en of andere manier de code die de GUI moet updaten zien door te geven aan de juiste thread, zijnde die van de Message Loop. We kunnen dit doen door gebruik te maken van een speciale interface ISynchronizeInvoke. In het .NET 1.0 en 1.1 framework kan je dit probleem negeren met de kans op fouten natuurlijk maar vanaf .NET 2.0 wordt dit strikter want als je daar een programma probeert uit te voeren zonder met het gestelde probleem rekening te houden, krijg je in debug mode zeker een foutmelding. Dus als we onze toepassingen met het oog op de toekomst willen maken dan moeten we nu reeds dit probleem oplossen in onze code. Voorkomen is altijd beter dan genezen.
6.7.3
Solving the problem met ISynchronizeInvoke
Als we zouden kijken in de class library zouden we zien dat elk WinForms element erft van een class Control. Dus Label, Button, TextBox, Form,. . . die erven allemaal op een gegeven punt van Control. Als we nu zouden kijken dan zouden we zien dat de class Control een interface ISynchronizeInvoke implementeert en bijgevolg implementeren ook Label, Button, TextBox, Form,. . . deze interface. Bekijken we nu eens de definitie van ISynchronizeInvoke: public interface ISynch ro ni zeI nv ok e { // Methods IAsyncResult BeginInvoke ( Delegate method , object [] args ) ; object EndInvoke ( IAsyncResult result ) ; object Invoke ( Delegate method , object [] args ) ;
←-
// Properties bool InvokeRequired { get ; } }
Uit deze interface zijn nu twee members van belang. Een eerste is de property InvokeRequired. Deze property controleert of de thread waarop de huidige
ing. Gaillez Michael
93
6. Asynchrone function calls in .NET
code wordt uitgevoerd de juiste thread is nl. de Windows Message Loop thread. Als we niet op de juiste thread zitten dan moeten we de code voor het updaten zien door te sturen naar de juiste thread. Dit kunnen we gaan doen met de methode Invoke van de ISynchronizeInvoke interface. We zien dat de Invoke methode twee parameters vraagt namelijk een delegate en een array van argumenten. We moeten dus de code voor het updaten van de GUI in een aparte methode zetten en deze koppelen aan een delegate. Eventuele parameters die dan moeten meegegeven worden aan die methode kunnen we dan in de argumenten array plaatsen. De doorgeven van een method tussen threads met behulp van een delegate noemen we marshalling. Om dit principe te illustreren kunnen we best vertrekken van een voorbeeld waarin we de GUI verkeerdelijk updaten. Stel dat we ons Calculator class willen gebruiken in een Windows applicatie dan kunnen we de code zodaning aanpassen dat we een Label meegeven in de constructor waarin het resultaat van een bewerking moet komen. using System . Runtime . Remoting . Messaging ; using System . Windows . Forms ; public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { private Label _lbl ; public Calculator ( Label lbl ) { _lbl = lbl ; } public int Add ( int num1 , int num2 ) { return num1 + num2 ; } public int Subtract ( int num1 , num2 ) { return num1 - num2 ; } public void Execute () { Operation opp = new Operation ( Add ) ; AsyncCallback callback = new AsyncCallback ( ←OnComplete ) ; opp . BeginInvoke (2 , 3 , callback , null ) ; } public void OnComplete ( IAsyncResult asyncResult ) { // Delegate is niet meer gekend dus we moeten het ophalen AsyncResult result = ( AsyncResult ) asyncResult ; Operation opp = ( Operation ) result . AsyncDelegate ;
ing. Gaillez Michael
←-
94
6. Asynchrone function calls in .NET
// We hebben nu de delegate terug dus // nu nog EndInvoke aanroepen voor het resultaat int sum = opp . EndInvoke ( asyncResult ) ; // Label updaten _lbl . Text = sum . ToString () ; } }
Het aanroepen van execute zorgt er nu voor dat de optelling asynchroon wordt uitgevoerd. Als de optelling afgewerkt is zal de methode OnComplete aangeroepen worden en daarin gaan we een Label gaan updaten. Dit kan nu aanleiding geven tot fouten omdat OnComplete niet op de Message Loop thread wordt uit gevoerd maar wel op een thread van de threadpool door de asynchrone call. We zullen dit moeten corrigeren door gebruik te maken van ISynchronizeInvoke. Het eerste wat we moeten doen is het updaten van het label in een aparte methode plaatsen, zodat we hier een delegate voor kunnen aanmaken. using System . Runtime . Remoting . Messaging ; using System . Windows . Forms ; public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { private Label _lbl ; public Calculator ( Label lbl ) { _lbl = lbl ; } public int Add ( int num1 , int num2 ) { return num1 + num2 ; } public int Subtract ( int num1 , num2 ) { return num1 - num2 ; } public void Execute () { Operation opp = new Operation ( Add ) ; AsyncCallback callback = new AsyncCallback ( ←OnComplete ) ; opp . BeginInvoke (2 , 3 , callback , null ) ; } public void OnComplete ( IAsyncResult asyncResult ) {
ing. Gaillez Michael
95
6. Asynchrone function calls in .NET
// Delegate is niet meer gekend dus we moeten het ophalen AsyncResult result = ( AsyncResult ) asyncResult ; Operation opp = ( Operation ) result . AsyncDelegate ;
←-
// We hebben nu de delegate terug dus // nu nog EndInvoke aanroepen voor het resultaat int sum = opp . EndInvoke ( asyncResult ) ; // Label updaten UpdateLabel ( _lbl , sum . ToString () ) ; } public void UpdateLabel ( Label lbl , string text ) { lbl . Text = text ; } } public delegate void SetString ( Label lbl , string text ) ←;
De delegate SetString kan nu gebruikt worden om een referentie naar de method UpdateLabel op te slaan. We hebben nu reeds het voorbereidend werk gedaan en een delegate gemaakt maar de code wordt nog steeds op de verkeerde thread uitgevoerd! Tijd om ISynchronizeInvoke erbij te halen dus: using System . Runtime . Remoting . Messaging ; using System . Windows . Forms ; public delegate int Operation ( int num1 , int num2 ) ; public class Calculator { private Label _lbl ; public Calculator ( Label lbl ) { _lbl = lbl ; } ... public void Execute () { Operation opp = new Operation ( Add ) ; AsyncCallback callback = new AsyncCallback ( ←OnComplete ) ; opp . BeginInvoke (2 , 3 , callback , null ) ; } public void OnComplete ( IAsyncResult asyncResult ) { // Delegate is niet meer gekend dus we moeten het ophalen AsyncResult result = ( AsyncResult ) asyncResult ;
ing. Gaillez Michael
←-
96
6. Asynchrone function calls in .NET
Operation opp = ( Operation ) result . AsyncDelegate ; // We hebben nu de delegate terug dus // nu nog EndInvoke aanroepen voor het resultaat int sum = opp . EndInvoke ( asyncResult ) ; // Label updaten ISynchronizeInvoke synchronizer = ( ←ISynchronizeInv ok e ) _lbl ; if ( synchronizer . InvokeRequired == true ) { SetString del = new SetString ( UpdateLabel ) ; object [] args = new object [2]; args [0] = _lbl ; args [1] = sum . ToString () ; synchronizer . Invoke ( del , args ) ; } UpdateLabel ( _lbl , sum . ToString () ) ; } public void UpdateLabel ( Label lbl , string text ) { lbl . Text = text ; } } public delegate void SetString ( Label lbl , string text ) ←;
Label is een WinForms element dus kunnen we de ISynchronizeInvoke opvragen. Daarna controleren we of we op de juist thread zitten. Als InvokeRequired true is dan zitten we op de foute thread en moeten we een marshal doen. Als InvokeRequired false is dan is er geen probleem en mogen we gewoon UpdateLabel aanroepen. In het geval van een marshal moeten we dus eerst een delegate aanmaken met een referentie naar UpdateLabel. Daarna moeten we de argumenten die we normaal zouden meegeven met UpdateLabel in een object array plaatsen en dit in dezelfde volgorde als de parameters van UpdateLabel. Tenslotte voeren we effectief de marshall uit door Invoke van het ISynchronizeInvoke object aan te roepen en met Invoke de delegate en de argumenten mee te geven. Dit zal er dus voor zorgen dat de UpdateLabel method wordt doorgegeven aan de Message Loop thread en daar correct zal worden uitgevoerd en de label updaten.
6.7.4
ISynchronizeInvoke ook voor threads
We hebben ISynchronizeInvoke tot nu toe enkel toegepast bij asynchrone calls maar we kunnen en moeten dit principe ook toepassen bij threads. Aangezien een asynchrone call voor ons op de achtergrond een thread opstart om het werk te doen en net het uitvoeren van code voor de GUI op die thread voor problemen zorgt, zullen we hier dus ook moeten rekening mee houden bij threads die we zelf aanmaken. Als we in de code van een zelf aangemaakte thread de GUI willen updaten (bvb een progressbar) dan zullen we ook daar ing. Gaillez Michael
97
6. Asynchrone function calls in .NET
het ISynchronizeInvoke principe op identieke wijze moeten toepassen, dus: • Methode maken voor het updaten van de GUI. • Een delegate aanmaken voor deze methode. • Bij het updaten van de GUI kijken of InvokeRequired true is. • Als InvokeRequired true is, dan doen we de marshall, anders roepen we gewoon de methode voor het updaten van de GUI aan.
ing. Gaillez Michael
98
Hoofdstuk 7
Extending the GUI 7.1
GDI+
7.2
Java swing
99
Hoofdstuk 8
Beyond .NET 8.1
Win32 libraries
8.2
COM-components
100
Listings 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 3.1 3.2 3.3 3.4 3.5 3.6 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9
Creatie van een IPAddress-object op basis van IP input string Creatie van een IPAddress-object op basis van de hostname . Creatie van een IPEndPoint-object . . . . . . . . . . . . . . . Declaratie van de TcpListener class . . . . . . . . . . . . . . . Declaratie van TcpClient . . . . . . . . . . . . . . . . . . . . Conversie van string naar byte-array en omgekeerd . . . . . . Een eenvoudige server socket applicatie . . . . . . . . . . . . Een eenvoudige client socket applicatie . . . . . . . . . . . . . Een InetAddress object creeren. . . . . . . . . . . . . . . . . . Datatransport met InputStream en OutputStream . . . . . . Baas tightly coupled . . . . . . . . . . . . . . . . . . . . . . . Werknemer tightly coupled . . . . . . . . . . . . . . . . . . . Bedrijf tightly coupled . . . . . . . . . . . . . . . . . . . . . . IRapporteer interface . . . . . . . . . . . . . . . . . . . . . . . Werknemer met interfaces . . . . . . . . . . . . . . . . . . . . Baas met interfaces . . . . . . . . . . . . . . . . . . . . . . . . Definitie van ThreadStart . . . . . . . . . . . . . . . . . . . . Thread maken en starten . . . . . . . . . . . . . . . . . . . . Een thread stoppen . . . . . . . . . . . . . . . . . . . . . . . . Definitie van WaitCallBack . . . . . . . . . . . . . . . . . . . De ThreadPool gebruiken . . . . . . . . . . . . . . . . . . . . ThreadPool gebruiken met argumenten . . . . . . . . . . . . . Lock statement met variabele . . . . . . . . . . . . . . . . . . Lock statement met this . . . . . . . . . . . . . . . . . . . . . Synchronization met MethodImpl attribute . . . . . . . . . .
101
15 15 15 16 18 20 21 21 23 25 27 27 28 29 29 30 54 54 55 57 57 57 58 59 59
Lijst van figuren
102