Gedistribueerd programmeren

Maat: px
Weergave met pagina beginnen:

Download "Gedistribueerd programmeren"

Transcriptie

1 Gedistribueerd programmeren

2

3 Gedistribueerd programmeren Collegedictaat, maart 2011 Gerard Tel Instituut voor Informatica en Informatiekunde Universiteit Utrecht

4 Opmaak: Gerard Tel, Utrecht c Copyright Gerard Tel / Universiteit Utrecht.

5 Voorwoord Dit is het dictaat van het vak Gedistribueerd Programmeren zoals dat gegeven wordt in het voorjaar van 2011 aan de Universiteit Utrecht. Het vak werd in 1999 voor het eerst gegeven en dit dictaat is (o.a. naar aanleiding van opmerkingen van gebruikers) in de volgende jaren steeds aangepast en verbeterd. Deel I, de hoofdstukken 1 tot en met 4, behandelt klassieke synchronisatiemethoden voor multi-threaded programma s die communiceren via variabelen onder read-write atomicity. Deel II, de hoofdstukken 5 tot en met 9, behandelt message passing algoritmen. De meeste onderwerpen in dit deel hebben te maken met fouttolerantie, en daarom is hier ook Hoofdstuk 9 (ontleend aan [Tel91]) opgenomen. Deel III, de hoofdstukken 10 tot en met 12, behandelt moderne wachtvrije synchronisatietechnieken. Het hoofdstuk over Multicore processors door Alexander Melchior is aan het eind geplaatst, maar sluit inhoudelijk het dichtst aan bij Deel I. Natuurlijk zijn er over deze onderwerpen ook boeken. Het eerste deel put vooral uit Burns en Davies [BD93], het tweede deel uit Tel [Tel00], en het derde uit Attiya en Welch [AW98]. Een uitgebreidere literatuurlijst is achterin opgenomen. Voor de programmafragmenten is geprobeerd de syntax van Java in enige mate na te volgen, maar het volgen van een daadwerkelijk bestaande syntax is ondergeschikt aan de begrijpelijkheid van de uitgedrukte algoritmen. Wij hopen studenten en andere lezers met dit boekje aangename, en vooral leerzame uren te bezorgen. De opgaven in dit dictaat zijn een voorbeeld van hoe tentamenvragen er uit kunnen zien. Sommigen stimuleren de student tot het nader bestuderen van een onderwerp, anderen vereisen een behoorlijk diep inzicht in de stof. Er zijn er een paar bij, waarop ik zelf (nog) geen pasklaar antwoord heb: 1.9, 2.2, 3.1, 3.14 met semaforen, 4.6, 5.17, 8.12, 8.15, Ideeën zijn welkom! Omdat er ook veel tentamenvragen letterlijk zijn overgenomen, zit er soms tussen de verschillende vragen overlap, en komen vragen zelfs dubbel voor. Tentamenvragen zijn niet altijd eenduidig in een hoofdstuk te plaatsen; soms is voor de beantwoording ook kennis van een eerder of later hoofdstuk nodig. Eeerdere versies van dit dictaat kunnen ook worden gebruikt, al kan de nummering van secties en opgaven verschillen. Toegevoegd in 2009 zijn de hoofdstukken 9 en 13. Annet Roodenburg ( heeft de illustraties in kaders 3.4 en 5.2 gemaakt. In 2011 zijn geen nieuwe wijzigingen gemaakt. Gerard Tel, Utrecht, voorjaar v

6 Inhoudsopgave Voorwoord Inhoudsopgave v vi 1 Inleiding: Concurrency Concurrency Waarom concurrency bestuderen? Processen, communicatie en taal Atomiciteit Scheduling Granularity Nogmaals scheduling Synchroniteit Fairness Opgaven bij hoofdstuk Mutual exclusion Probleemstelling Vier eenvoudige algoritmen Eerste poging: bezetvlaggen Tweede poging: de safe sluice De safe sluice met na-u Een Salomonsoordeel: om de beurt Dekkers algoritme Lamports bakery-algoritme Samenvatting en conclusies Opgaven bij hoofdstuk Synchronisatieprimitieven Semaforen voor programmeurs Binaire en algemene semaforen Mutual exclusion met semaforen Semaforen voor systeembouwers Toepassing in de begrensde buffer Monitors Begrensde buffer met monitors Samenvatting en conclusies Opgaven bij hoofdstuk vi

7 Gedistribueerd programmeren vii 4 Dining Philosophers Een eenvoudige doch voedzame maaltijd Probleemomschrijving Eisen aan de oplossing Toepassingen Maaltijd met hindernissen Geen synchronisatie: vorkenconflict Synchronisatie per vork Globale synchronisatie Globale synchronisatie met token Veel eten en toch hongerige gasten Progress en no starvation Deadlock Wait-for-graphs Priority scheduling Deadlock detection and recovery Deadlock avoidance: servetten Deadlock prevention: ordening van resources Samenvatting en conclusies Opgaven bij hoofdstuk Electie Berichtuitwisseling Probleemstelling: electie Maak 0 de leider Wie is kleiner dan zijn voorganger Circuleer een token Enkele oplossingen Het algoritme van LeLann Het algoritme van Chang en Roberts Het algoritme van Peterson Een bewijs van ondergrens Samenvatting en conclusies Opgaven bij hoofdstuk Fouttolerantie en consensus Fouttolerant programmeren Foutmodellen Beslisproblemen Voorbeeld: flexibele electie Consensus-algoritmen Een triviale oplossing De replicated server De zwakke broadcast De sterke broadcast Onmogelijkheid van consensus Eisen op een consensus-oplossing Modelvorming Bivalentie in runs

8 viii Inhoudsopgave Bewijs en discussie Samenvatting en conclusies Opgaven bij hoofdstuk Randomisering Anonieme electie Het probleem en deterministische onmogelijkheid Een terminerende oplossing: Monte Carlo Een probabilistisch terminerende oplossing: Las Vegas Terminerend met faalkans Coordinatie over een broadcast-kanaal Probleemstelling Niet-randomiserende oplossingen Een randomiserende oplossing: de vervalprocedure Ben-Ors consensus-algoritme Uitleg van het algoritme Bewijs van overeenstemming en geldigheid Bewijs van convergentie Samenvatting en conclusies Opgaven bij hoofdstuk Foutdetectie Motivatie: synchrone consensus Definities Implementaties van foutdetectors Synchrone systemen: perfecte detectie Partiëel synchrone systemen: uiteindelijk perfecte detectie Ondersteuning door het besturingssysteem: incompleet Consensus met zwakke accuratesse Uiteindelijke zwakke accuratesse Samenvatting en conclusies Opgaven bij hoofdstuk Stabilisatie Begrippen rond stabilisatie Systemen en Berekeningen Specificaties Pseudostabilisatie Gesloten eigenschappen en normen Uitsluiting: Niet-Uniforme Oplossingen Een Gerichte Oplossing Een Compacte Oplossing Uitsluiting: Een Uniforme Oplossing Routering in MANETs Orientaties Link Reversal en t-gerichtheid Samenvatting en conclusies Opgaven bij hoofdstuk

9 Gedistribueerd programmeren ix 10 Wachtvrije implementaties met registers Voordelen van wachtvrije synchronisatie Probleemstellingen modelleren als objecten Definities van gebruikte objecten Implementaties Registers: van single naar multiple reader Hoe het niet moet Oplossing: readers helpen elkaar Het snapshotobject Samenvatting en conclusies Opgaven bij hoofdstuk Wachtvrije consensus Consensusobject met registers Consensus met test-and-set, queue en compare-and-swap Consensus met test-and-set Queue implementeert 2-consensus Queue implementeert geen 3-consensus Het consensusgetal Consensusobject met compare-and-swap Wachtvrije hiërarchieën Consensus met randomisering Samenvatting en conclusies Opgaven bij hoofdstuk Universaliteit van consensus Compare-and-swap implementeert test-and-set Fetch-and-increment Compare-and-swap implementeert fetch-and-increment Test-and-set implementeert fetch-and-increment Universaliteit Consensus is universeel Werking van de universele implementatie Bewijs van de universele implementatie Reflectie Universaliteit van naamgeving Samenvatting en conclusies Opgaven bij hoofdstuk Multi-core Systemen (Alexander Melchior ) Een stukje geschiedenis Hardware UniProcessor Multiprocessor Multi-core Software Tegenwerking Locking: TAS vs TTAS Tot slot

10 x Inhoudsopgave Bibliografie 202 Index 205

11 Hoofdstuk 1 Inleiding: Concurrency Het doel van dit hoofdstuk is een eerste overzicht te geven van een aantal begrippen die in gedistribueerd (of concurrent) programmeren van belang zijn. Aan de orde komen onder andere: concurrency, non-determinisme en randomisering, atomiciteit, scheduling en berekeningsbomen, granularity en fairness. 1.1 Concurrency De term concurrency is, hoewel het woord letterlijk gelijktijdigheid betekent, niet strikt gereserveerd voor situaties waar meerdere dingen tegelijk gebeuren. We spreken al van concurrency wanneer in een programma de onderlinge volgorde van deelberekeningen niet is aangegeven. Een sequentieel programma daarentegen bevat instructies en geeft ook aan in welke volgorde ze uitgevoerd worden. Een voorbeeld is programma 1.1, een fragment dat een binomiaalcoëfficiënt int binco (int n, int k) { int uitkomst if (k==0 or k==n) { uitkomst = 1 } else { int links, rechts links = binco(n-1,k) rechts = binco(n-1,k-1) uitkomst = links+rechts } return uitkomst } Programma 1.1: Berekenen van binomiaalcoefficienten. 1

12 2 1 Inleiding: Concurrency int binco (int n, int k) { int uitkomst if (k==0 or k==n) { uitkomst = 1 } else { int links, rechts cobegin { links = binco(n-1,k) } { rechts = binco(n-1,k-1) } coend uitkomst = links+rechts } return uitkomst } Programma 1.2: Berekenen van binomiaalcoefficienten. uitrekent op basis van de bekende formule ( n k ) 1 ( ) ( ) als k = 0 of k = n = n 1 n 1 + als 0 < k < n k k 1 ( ) n De binomiaalcoëfficiënt geeft het aantal verschillende deelverzamelingen met k elementen van een verzameling met n elementen. Programma 1.1 rekent deze formule precies uit; van k belang is hier het else-deel. Conform de formule wordt gespecificeerd dat er een linker en een rechterstuk moet worden berekend en dat die moeten worden opgeteld. Maar het programma zegt ook, dat eerst links en dan pas rechts wordt uitgerekend, in de tijd dus zo: Links Rechts Andersom had natuurlijk ook gemogen, de twee statements worden dan gewoon omgedraaid en de executie wordt zo: Rechts Links Maar waarom zouden we eigenlijk een volgorde specificeren? In programma 1.2 is het else-gedeelte vervangen door een concurrent gedeelte. cobegin en coend constructie, als in Met de cobegin { links = binco(n-1,k) } { rechts = binco(n-1,k-1) } coend

13 1.1 Concurrency 3 i = 5 ; cobegin { i = 6 } // I { j = i } // J coend return j Programma 1.3: Twee concurrente toekenningen. wordt aangegeven dat links en rechts in willekeurige volgorde of zelfs overlappend kunnen worden berekend. Dan zijn de beide genoemde executies mogelijk, maar ook zoiets als dit: Links Rechts of dit: Links Rechts waarbij de deelberekeningen elkaar overlappen. Je weet natuurlijk nooit, welk van de twee deelberekeningen langer duurt. Een programma of systeem waarbij de executievolgorde niet vast ligt noemen we een concurrent programma of systeem. De termen parallel en gedistribueerd worden ook gebruikt maar sommigen hechten er een iets andere betekenis aan Waarom concurrency bestuderen? Je kunt bij het programmeren concurrency gebruiken, bv zoals boven, maar er zitten een paar verschrikkelijke adders onder het gras. Dit heeft ermee te maken dat je als programmeur feitelijk een stuk controle over wat er gebeurt uit handen geeft. Terwijl je bij het eerste fragment weet wat er gaat gebeuren (de eerste executie), zijn er bij het tweede fragment meerdere mogelijkheden. In welke volgorde of mate van overlap de deelberekeningen worden uitgevoerd wordt niet langer door de programmeur bepaald. Er is sprake van non-determinisme en dit kan heel makkelijk leiden tot programma s die meerdere uitkomsten kunnen hebben. Bij programma 1.2 gebeurt dit niet omdat de twee deelberekeningen geheel onafhankelijk zijn, maar bekijk nu programma 1.3. De twee delen I en J die concurrent worden uitgevoerd refereren aan een gemeenschappelijke variabele i (die in J wordt gelezen en in I wordt geschreven). Een aantal personen compileert programma 1.3 op z n computer en draait het 20 keer, dit zijn de uitkomsten die ze vinden: Anneke Bob Carla Dick Erik Freek

14 4 1 Inleiding: Concurrency Kader 1.4: Multithreading in de Netscape-browser De Netscape-browser is een voorbeeld van een programma dat meerdere taken tegelijk kan verrichten. Je kunt bijvoorbeeld met je browser-window een groot bestand downloaden en ondertussen met het mailwindow je ophalen. Er kunnen een groot aantal windows tegelijk open zijn. Het programma kan ook crashen en stuurt dan tussen al het andere werk door een foutmelding naar de fabrikant. Bij het maken van zo n programma wordt veel gebruik gemaakt van concurrency. Meestal is er per window al een thread, en voor het downloaden van een bestand wordt weer een nieuwe thread opgestart. Door het opstarten van een nieuwe thread wordt bereikt dat, terwijl de taak wordt uitgevoerd, het window in staat blijft om nieuwe opdrachten (zoals het afbreken van de download) te verwerken. Het idee dat een programma (of programmafragment), met eventueel een gegeven invoer, één mogelijke uitkomst heeft is blijkbaar voor concurrente programma s niet geldig. Een specificatie van een programma is daarom nooit de vastlegging van één specifieke uitkomst die bij de gegeven invoer hoort, maar een karakterisering van het toegelaten gedrag. Concurrency introduceert een enorm lastig probleem, namelijk: hoe ervoor te zorgen dat het programma in alle mogelijke executies correct is. Als deze problematiek zo lastig is dat wij daar een heel college over moeten praten, waarom zouden we dan concurrency überhaupt toepassen? Concurrency wordt niet door bovengebruikte programma-constructies geïntroduceerd, maar bestaat gewoon. Immers, in een computer zijn altijd meerdere processen gelijktijdig actief, waarvan de instructies non-deterministisch door elkaar geklutst worden. De processen moeten resources delen, bv printers en filesysteem, en hierdoor ontstaan altijd de situaties waarin er tussen de verschillende delen interacties bestaan (als in Programma 1.3). Bezig zijn met concurrency scherpt het denken over programmeren. Bij denken over concurrency is het altijd nodig te denken in termen van specificaties van objecten, en te abstraheren van bepaalde implementaties. Bovendien, tegenover de genoemde nadelen staan ook grote voordelen van het daadwerkelijk toepassen van concurrency in programmatuur. Van moderne, muisgestuurde programmatuur verwacht je dat er commando s kunnen worden gegeven terwijl het vorige nog wordt verwerkt; zie kader 1.4. Dit gedrag vereist concurrency tussen het uitvoeren van verschillende taken.

15 1.2 Atomiciteit 5 Concurrency vereist minder programmeerwerk. Over alles wat je in een programma vastlegt moet je nadenken. Leg je geen volgorde vast, dan hoef je ook over de volgorde niet na te denken. Non-determinisme (dus ook concurrency) is flexibel. Door in een fase van het ontwerpproces keuzes open te laten, kun je in een latere fase, of bij onderhoud aan de programmatuur, nog mogelijkheden ter verbetering open houden. Concurrency geeft vrijheid aan de compiler. Door verschillende volgorden toe te staan kun je een slimme compiler die volgorde laten kiezen die hij het efficiëntst kan laten uitvoeren. Concurrency is efficiënt op een uniprocessor. Stel dat de computer begint met het uitrekenen van een zekere deelberekening maar even moet wachten, bv op disk-io. Concurrency staat dan toe dat de wachttijd wordt gevuld met het rekenen aan een andere deelberekening. Vrijheid van volgorde staat ook interleaving toe, dat wil zeggen, dat er afwisselend instructies van de verschillende threads worden uitgevoerd. Concurrency is efficiënt op een multiprocessor. Er wordt gesproken van parallellisme wanneer de diverse instructies daadwerkelijk tegelijkertijd worden uitgevoerd, met als doel de verwerkingssnelheid te verhogen; zie kader Grote instellingen als het KNMI gebruiken supercomputers met 32-voudig (of soms meer) paralellisme om de verwachting voor volgende week nog deze week af te krijgen Processen, communicatie en taal Onder een proces wordt bij programmeren meestal verstaan: een stuk code dat wordt uitgevoerd met een eigen namespace. Als je binnen dat proces concurrency gebruikt (zoals boven) en de verschillende delen hebben toegang tot dezelfde variabelen (objecten) dan spreekt men doorgaans van threads. Maar op andere momenten is in een systeem sprake van verschillende processen die toch met elkaar communiceren door gemeenschappelijke variabelen of objecten te lezen en te schrijven. Omdat bij interactie tussen processen dezelfde problemen optreden als bij de interactie tussen threads, zullen we het verschil tussen processen en threads niet zo nauw nemen. Naast gedeelde variabelen is er nog een andere veelvoorkomende manier van communicatie tussen processen: messages. Processen hebben dan sockets waar ze informatie in kunnen schrijven (zenden) of uit lezen (ontvangen). Een socket garandeert dat elke waarde die erin wordt geschreven ook exact eenmaal kan worden gelezen. Deze vorm van communicatie staat centraal in de hoofdstukken 5 tot 8 van dit boek. 1.2 Atomiciteit Bij concurrency mogen instructies in verschillende volgorden worden uitgevoerd of zelfs overlappen. Wat is het resultaat van overlappende instructies? We voeren deze discussie even aan de hand van programma 1.3 en programma 1.5 dat twee concurrente secties bevat. De threads werken onafhankelijk, maar we willen in de gedeelde variabele c tellen hoe vaak een bepaalde procedure wordt aangeroepen. In de wereld van sequentiële programma s betekent c=c+1 natuurlijk hetzelfde als c++. Laten we zeggen dat de linkerthread dit driemaal en de

16 6 1 Inleiding: Concurrency c = 0 cobegin {... { c = c c = c+1... c = c+1 c = c+1... c = c+1 c = c c = c+1 } } coend Programma 1.5: Overlappende updates. rechter het viermaal doet. De executies van deze statement binnen één thread liggen in de tijd gezien natuurlijk na elkaar: Links c=c+1 Rechts Tussen de threads onderling is er sprake van een min of meer willekeurige overlap. Wat is na afloop de waarde van variabele c? Het antwoord (tenzij speciale maatregelen zijn genomen): we weten het niet! Het ons bekende gedrag van variabelen en objecten is namelijk hun sequentiële specificatie, dat wil zeggen dat deze objecten zich gedragen zoals we verwachten wanneer er slechts één operatie gelijktijdig actief is. Onder concurrency geldt de sequentiele specificatie niet, en we hebben dus geen specificatie van het gedrag van c in deze executie. En aangezien we geen specificatie hebben berust elk antwoord dat we geven op onze intuïtie cq. kennis van de werking van de operatie c = c+1. Zo n antwoord is daarom onacceptabel; bij het programmeren mogen alleen conclusies die kunnen worden afgeleid uit specificaties als geldig worden aangenomen. Om uit sequentiële specificaties van instructies of objecten iets te concluderen over concurrent gedrag gebruiken we een axioma en een definitie. Axioma 1.1 (Onafhankelijkheidsaxioma) Operaties die geen gemeenschappelijke variabelen gebruiken beïnvloeden elkaar niet. Dwz., de concurrente uitvoering van a=7 en b=c geeft hetzelfde resultaat als a=7 ; b=c of b=c ; a=7 (mits er hier geen aliassen in het spel zijn). De correctheid van programma 1.1 kan worden beredeneerd met dit axioma. (Natuurlijk moet wel grondig worden gecontroleerd dat er niet ergens verstopt in een subroutine toch gemeenschappelijke variabelen worden gebruikt!) De situatie dat concurrente programma s geen variabelen gemeen hebben is blijkens dit axioma niet zo moeilijk te bestuderen. Inderdaad zijn in moderne computers de diverse programma s zo mooi van elkaar afgeschermd, dat je als programmeur geen last hebt van programma s die gelijktijdig met het jouwe draaien.

17 1.2 Atomiciteit 7 Kader 1.6: Atomiciteit in IBM System/360 en 370. In 1964 lanceerde IBM zijn nieuwe processorarchitectuur System/360 [GS87], die over een performance ratio van 1 op 20 bruikbaar moest zijn. Het Model 40, geïntroduceerd in april 1964 (foto), had een kloksnelheid van 1.6MHz en 256kB geheugen. Model 600E uit 1987, vrijwel geheel compatibel met Model 40, had twee CPU s met een kloksnelheid van 58MHz en 1024MB geheugen. De eerdere modellen hadden maar één processor; omdat er allen maar tussen twee instructies van thread geswitched kan worden, is het dan niet van belang je af te vragen wat er precies tijdens een instructie gebeurt. Een Test-and-Set instructie bestond uit twee geheugenoperaties die onlosmakelijk verbonden waren. Met de introductie van multi-processoren werd alles subtieler. De vroege machines deden een instruction retry wanneer er tijdens een instructie iets mis ging, maar een tweede processor kon hierbij (foute) tussentijdse waarden zien. Zie ook Kader Nu zal er in de werkelijkheid vrijwel altijd in één of andere vorm sprake zijn van interactie, ook met andere programma s, bijvoorbeeld doordat resources (disk, modem) worden gedeeld. Dit gebruik is dan mooi afgeschermd via system-calls, maar er kan ook bewust interactie worden gecreëerd op een manier als in programma 1.3. Bij het redeneren over zulke programma s is het prettig te mogen redeneren alsof de instructies van de diverse threads in één of andere volgorde na elkaar worden uitgevoerd. De basis voor een dergelijke redeneerwijze ligt in de volgende definitie. Definitie 1.2 (Atomiciteit) Een operatie (of collectie operaties) is atomair als het resultaat van een aantal, mogelijk overlappende, uitvoeringen ervan altijd gelijk is aan het resultaat dat ontstaat wanneer deze operaties na elkaar worden uitgevoerd; elk in een ondeelbaar kort moment dat ligt tussen het begin en einde van de daadwerkelijke uitvoering. Merk allereerst op, dat atomiciteit een definitie betreft en geen impliciete aanname; per situatie moet worden bekeken of bepaalde operaties wel of niet atomair zijn. Verder is van belang op te merken, dat atomiciteit van operatie niet betekent dat de altijd na elkaar worden uitgevoerd, maar alleen dat het resultaat hetzelfde is als wanneer ze na elkaar worden uitgevoerd. De ontwerpers van de IBM System/360 architectuur (Kader 1.6) formuleerden al, dat het niet relevant is of situaties van elkaar verschillen, maar can you prove to me that any program could ever detect that difference [GS87]. Het tweede deel van de definitie beperkt de volgorde waarin de operaties denkbeeldig na elkaar mogen worden geplaatst, en voegt daarmee een eis aan de betekenis van atomiciteit toe. Ter illustratie toont figuur 1.7 een schrijfactie w (write x, 1) die de waarde 1 in variabele x schrijft, waarbij tijdens de uitvoering twee leesacties ra en rb op x worden gedaan. Dat

18 8 1 Inleiding: Concurrency tijd Waarde van x bij aanvang is 0. rb w read x: 1 write x, 1 ra read x: 0 Figuur 1.7: Overlappende lees- en schrijfacties. leesactie rb eerder begint en eerder eindigt dan schrijfactie w, maar toch de nieuwgeschreven waarde oplevert is niet strijdig met de aanname dat lezen en schrijven atomair zijn. De waarde 1 wordt immers opgeleverd door een leesactie die niet met de schrijfactie overlapt, maar er geheel na plaatsvindt. Evenmin is het strijdig met de atomiciteit dat leesactie ra met het schrijven overlapt en de oude waarde 0 oplevert. Deze oude waarde is immers correct voor een leesactie die in zijn geheel voor de schrijfactie plaatsvindt. De gecombineerde executie waarin beide leesacties voorkomen is echter wel strijdig met de atomiciteit en dit komt door de eis dat contractiepunten zijn aan te wijzen binnen het interval van daadwerkelijke uitvoering. De uitkomsten van alle acties zijn juist voor deze volgorde van uitvoering: ra, w, rb; het is dus mogelijk om bij figuur 1.7 een volgorde van de instructies te vinden waarvoor het resultaat juist is. Maar in deze volgorde komt ra voor rb en omdat het interval van daadwerkelijke uitvoering van rb in zijn geheel voor het interval van daadwerkelijke uitvoering van ra ligt, is het niet mogelijk om binnen deze intervallen contractiepunten te kiezen in deze volgorde. De executie van figuur 1.7 kan daarom bij atomaire lees- en schrijfoperaties niet voorkomen. Atomiciteit is een aaname die je over bepaalde instructies kunt maken of die je voor bepaalde instructies kunt implementeren. Kort gezegd, een executie van een atomaire instructie mag je je voorstellen als geconcentreerd op een punt binnen het tijdsinterval waarin hij wordt uitgevoerd. Neem weer de executie van programma 1.5 en neem aan dat de operatie c = c+1 als atomair mag worden beschouwd. (In code wordt dit soms aangegeven met punthaken: <c = c+1>). De atomiciteit zegt dat de collectie overlappende executies hetzelfde resultaat heeft als de uitvoering van 7 instructies c = c+1 na elkaar. Na afloop is de waarde van c dan 7. Realiseer je op dit punt echter, dat operaties bepaald niet vanzelf atomair zijn! Links c=c+1 Rechts Ook heeft de programmeur geen invloed op de onderlinge volgorde van de contractiepunten. Veronderstel, dat een operatie lees c wordt uitgevoerd, overlappend met de eerste twee operaties c=c+1. De uitkomst van de operatie kan 0, 1 of 2 zijn en dit kan per executie verschillen.

19 1.3 Scheduling 9 Begin I J J I Figuur 1.8: Executieboom van programma Scheduling Laten we nu aannemen dat ons programma is opgebouwd uit atomaire instructies; volgens de definitie is de executie (zelfs als de verschillende instructies elkaar daadwerkelijk overlappen) equivalent aan de uitvoering van de instructies in een of andere volgorde. Instructies die na elkaar in dezelfde thread zitten komen ook in die volgorde na elkaar, maar hoe zit het met de onderlinge volgorde van instructies uit verschillende threads? Beschouw weer even programma 1.3 en neem aan dat de twee instructies elk atomair zijn. Een compact overzicht van de mogelijke executies geeft de executieboom in figuur 1.8. Deze boom geeft voor elke mogelijke toestand van het systeem aan, welke stappen mogelijk zijn. In de beginsituatie is een instructie van hetzij thread I, danwel thread J mogelijk. Wordt de instructie van thread I uitgevoerd, dan is in de volgende situatie alleen de instructie van thread J nog maar mogelijk. Een executie van het programma komt nu overeen met een enkel pad vanaf de wortel in deze boom. Het non-determinisme komt tot uiting in het vertakken in de diverse knopen (in figuur 1.8 alleen in de beginknoop). De boom in figuur 1.8 is nog vrij eenvoudig van structuur omdat elk pad precies dezelfde instructies bevat, waarbij alleen de volgorde kan verschillen. Een eerste ding dat we ons gaan realiseren is dat weliswaar vanuit het standpunt van de programmeur verschillende executies mogelijk zijn, maar dat we geen enkel zicht hebben op welke mogelijkheid gekozen zal worden. Dit maakt het gevaarlijk af te gaan op testen van het programma. Verder, dat we zelfs geen zicht hebben op het mechanisme dat dit gaat bepalen. Dit maakt het gevaarlijk af te gaan op onze intuïtie betreffende de werking. Een paar voorbeelden. Anneke heeft een wat simpele compiler die de concurrency er vrijwel uitcompileert: de gegenereerde code begint altijd met de eerstgenoemde thread en maakt de tweede pas actief als de eerste klaar is of niet verder kan. Annekes programma geeft dus altijd 6 als antwoord. Ook bij andere concurrente programma s wordt altijd het eerstgenoemde deel zoveel mogelijk eerst uitgevoerd. Bobs compiler is slim: die bedenkt dat er een machineinstructie kan worden bespaard door het tweede statement eerst uit te voeren, en in dit geval wordt er code gegenereerd die juist altijd het tweede statement eerst uitvoert. Bobs programma geeft dus altijd 5 als antwoord. Andere concurrente programma s worden misschien zo gecompileerd dat het eerstgenoemde deel eerst wordt uitgevoerd. Carla s compiler is wat moderner en genereert non-deterministische machinecode. Op het

20 10 1 Inleiding: Concurrency Programma A: Programma B: i=0 i=0 while ( i == 0 ) while ( i == 0 ) { cobegin { i=0 } { i = floor( rnd() * 2) { i=1 } coend print i print i } } Programma 1.9: Non-determinisme versus randomness. moment dat de twee threads worden uitgevoerd, kijkt het OS welke thread het eerst uitgevoerd kan worden. Dit verschilt per executie! Een redelijk afwisselend patroon is het gevolg, totdat er (zonder dat Carla dit door heeft) een fax binnenkomt, waardoor thread 2 in het voordeel komt en dus veel vaker gekozen wordt. Dick heeft ook zo n compiler en hij krijgt geen fax. Bij hem is meestal thread 1 in het voordeel, maar na een paar executies wordt een deamon 1 actief die zijn disk opschoont. Tijdens die activiteit is het efficiënter om thread 2 eerst uit te voeren, en het OS doet dat. Non-determinisme is dus bepaald niet hetzelfde als randomness! Om het verschil wat verder op scherp te zetten: deze twee fragmentjes kunnen allebei i zowel op 1 als op 0 zetten. cobegin { i=0 } { i=1 } coend i = floor( rnd() * 2) In programma 1.9 worden deze stukjes herhaald tot er een keer 1 komt. Is er verschil tussen programma s A en B? De uitvoer is een rij nullen met een 1: Bij A Uitvoer Bij B Kans mogelijk 1 mogelijk 1/2 mogelijk 01 mogelijk 1/4... mogelijk mogelijk (lengte k) mogelijk 1/2 k... mogelijk mogelijk 0 Het blijkt (niet verrassend) dat de programma s dezelfde verzameling van mogelijke executies hebben. Echter, door gebruik van randomizering in B kunnen we daar over de diverse executies een kansverdeling opstellen. Bij A is zo n kansverdeling op de verzameling executies niet mogelijk, er is slechts een platte verzameling executies. Wat betekent het dat een programma termineert? (Dan wel, een of andere andere eigenschap vertoont.) Het betekent, dat alle executies eindig zijn. Noch A, noch B heeft deze eigenschap, want beide hebben een oneindige executie. Maar kan die voorkomen? Bij A is het een executie als alle andere, er is tusssen de executies onderling geen verschil. Bij B hebben de eindige executies samen kans 1, en de oneindige heeft kans 0. Daarom zeggen we dat 1 Zie voor de herkomst van dit, hier correct gespelde, woord.

21 1.4 Granularity 11 programma B termineert met kans 1. Terminatie met kans 1 is een strikt zwakkere eigenschap dan terminatie, en wordt ook wel probabilistische terminatie of convergentie genoemd. Randomisering wordt verder bestudeerd in hoofdstuk Granularity Doorgaans is een operatie als c=c+1 niet in zijn geheel atomair, maar opgebouwd uit een aantal afzonderlijke machine-instructies bv zo: LOADA c INCRA STOREA c Ook ingewikkelder operaties, zoals het invoegen in een zoekboom of operaties op een andere datastructuur, zijn opgebouwd uit instructies. Instructie LOADA c leest de waarde van c uit het geheugen en stopt hem in een register A in de CPU, INCRA telt daar 1 bij op, en STOREA c kopieert de waarde van het register naar het geheugen. Als er sprake is van atomiciteit op het niveau van deze instructies dan kunnen die van twee threads bijvoorbeeld als volgt worden geïnterleaved: LOADA c INCRA STOREA c LOADA c INCRA STOREA c Let op: hoewel beide threads het gebruikte register A noemen, is er sprake van verschillende registers! De registers zijn een soort locale variabelen van de threads. Wat is de waarde van c na afloop (als hij bv eerst 0 was)? Door de overlap is er een update compleet verloren gegaan. Als in programma 1.3 de gehele instructie of LOAD en STORE operaties atomair zijn, is de uitkomst van het fragment altijd 5 of 6. Dit verklaart wat Anneke, Bob, Carla en Dick zien. Bij Freek is de situatie ook simpel: het gedrag van variabele i is niet gespecificeerd als lezen en schrijven overlappen, en Freeks computer heeft nog het fatsoenlijke gedrag om in geval van overlap altijd een 0 op te leveren. Maar wat is er bij Erik aan de hand? Hij heeft een binaire machine (de anderen misschien ook wel, maar we waren dit aspect nog niet tegen gekomen) die de waarden 5 en 6 bit voor bit atomair leest cq. schrijft. Als een gedeelte van de bits geschreven zijn en dan het lezen erdoor komt, kun je op sommige bitposities de oude en op andere de nieuwe waarde tegenkomen. Erik heeft in zijn machine wel atomiciteit, maar op een lager nivo dan lees/schrijfoperaties. Over het algemeen zijn processors zo ingericht dat machine-instructies atomaire eenheden zijn. Geheugens zijn doorgaans zo ingericht (door arbitrage bij de interface) dat afzonderlijke lees- en schrijfoperaties op registers atomair zijn. Daarom zullen we voorlopig steeds de aanname maken dat lees- en schrijf-acties atomair zijn. Dit heet read-write atomicity. Read-write atomicity geldt voor variabelen in Java, mits het een enkelvoudige variabele is die in ten hoogste 32 bits is opgeslagen. Een Java implementatie van programma 1.3 geeft dus als resultaat altijd 5 of 6. Omdat het verhogen van c twee read-write operaties vraagt, prefereren we meestal de schrijfwijze c=c+1 (waarin de dubbele toegang tot c in de notatie tot uiting komt) boven c++.

22 12 1 Inleiding: Concurrency Kader 1.10: Een cluster voor parallel rekenen Bij het ontwerpen van een parallelle computer is het van belang, vast rekening te houden met het soort taken waarvoor de machine wordt gebruikt. Dit cluster bestaat uit standaard PC s (zonder beeldscherm en toetsenbord), gekoppeld via een 100Mb Ethernet; per machine is een harde schijf geïnstalleerd. In deze opzet is relatief weinig communicatiecapaciteit beschikbaar, en relatief veel geheugen per processor. Het cluster kan daarom worden gebruikt voor taken, die in grote onafhankelijke rekenklussen zijn op te splitsen. Veel wetenschappelijke rekentaken vereisen steeds na enkele kleine stapjes het uitwisselen van data met andere delen van de berekening, en voor zo n klus is deze poor man s supercomputer niet geschikt. De laatste notatie is natuurlijk wel geschikt in omgevingen waar de ophoging als geheel atomair is. 1.5 Nogmaals scheduling We kijken nog even verder naar de mogelijke volgorden van atomaire instructies. Het mechanisme dat de volgorde bepaalt noemen we wel eens de scheduler. Verschillende schedulers die je in de praktijk kunt tegenkomen zijn: Interleaving: De threads worden op dezelfde CPU gedraaid maar ze mogen beurtelings een of meer instructies uitvoeren. Op een enkele CPU (uniprocessor) is echte overlap van machine-instructies niet mogelijk. Wel kan de controle van de CPU tussen willekeurig welke twee instructies van de ene naar de andere thread overgegeven worden. De aanleiding kan van alles zijn: interrupt door een ander proces in de machine de thread moet wachten (op IO) en wordt uitgeswapt een timer loopt af. Welke thread (of proces) daarna gekozen wordt hangt af van allerlei keuzen die in het Operating System zijn gemaakt, en dit is buiten zicht van de programmeur. Parallellisme: De verschillende threads worden op meerdere CPU s gelijktijdig uitgevoerd. Nu is er wel daadwerkelijke overlap van instructies mogelijk (maar atomiciteit garandeert natuurlijk dat dit voor het resultaat niet uitmaakt).

23 1.5 Nogmaals scheduling 13 i=0 ; j=0 cobegin { while (i==0) { i = 1 j++ } } coend Programma 1.11: Een mogelijk oneindige thread? We moeten de scheduler nooit zien als een identificeerbaar mechanisme, maar eerder als een samenspel van krachten en invloeden die bij uitvoering van concurrente code werkzaam zijn. Het verschil tussen interleaving en parallellisme werd significant toen de IBM System/360 (Kader 1.6) architectuur werd uitgebreid met multi-processor mogelijkheden. Je kunt namelijk vrij gemakkelijk een instructie implementeren die meerdere geheugen-accessen doet, zoals een ADD; het resultaat is atomair op een uniprocessor, omdat de thread niet tijdens de instructie onderbroken wordt. Het is echter heel moeilijk om deze ADD zo te implementeren dat hij ook op een multi-processor atomair is Synchroniteit Het is verleidelijk te denken dat threads wel ongeveer even snel zullen gaan. Bekijk cobegin { a1 ; a2 ; a3 ; a4 ; a5 ; a6 ; a7 ; a8 ; a9 ; a10 } { b1 ; b2 ; b3 ; b4 ; b5 ; b6 ; b7 ; b8 ; b9 ; b10 } coend In het meest extreme geval worden instructies ai en bi tegelijk uitgevoerd, maar op z n minst zou je zoiets verwachten als: a1 is wel klaar voordat b10 begint. In het algemeen maken we dergelijke aannamen niet! Er zijn manieren om twee sequences van 10 te mixen, en elk van die manieren is een mogelijke executie. Zelfs als je weet dat het programma op een twee-processor computer wordt uitgevoerd en dat beide processors even snel zijn: het is mogelijk dat op de beide processors je instructies worden ge-interleaved met instructies van andere programma s. Het is natuurlijk wel mogelijk extra aannamen over de scheduling te maken als de specificatie van je executie-omgeving dit rechtvaardigt. We spreken van een synchroon parallel systeem als gelijkloop tot instructieniveau is gegarandeerd. We spreken van partiële synchronisatie als er een getal k is waarvoor geldt dat er hoogstens k instructies van een thread kunnen zijn tussen twee opeenvolgende instructies van een andere thread. Nogmaals, in het algemeen maken we dergelijke aannamen niet Fairness Van eindige threads (dwz, met elk eindig veel instructies) bestaan maar eindig veel interleavings en die staan we alle toe. Maar hoe zit het als een thread oneindig veel instructies heeft, bv een eindeloze lus, of een potentieel oneindige? De linkerthread in programma 1.11 leest i en gaat daarmee net zo lang door tot de waarde 1 gezien wordt. De rechterthread doet niets anders dan i op 1 zetten. De executieboom van

24 14 1 Inleiding: Concurrency B T I T I A T A I. A T. A I. A T. A I T. A T I. A T I. A. A. A T I T T I T. A T. A I T. T. I T T I T Figuur 1.12: Een oneindige executieboom. dit programma is (deels) getekend in figuur 1.12 (de T, I, en A staan voor de Test, Increment en Assignatie in het programma). Elk pad in de boom bevat hoogstens één assignatie, daarna is namelijk de rechterthread klaar. Paden in de boom hebben verschillende lengtes doordat het aantal slagen van de lus in de linkerthread afhangt van de interactie. Termineert het programma en wat is de eindwaarde van j? Als eerste moet nu duidelijk zijn dat het programma in ieder geval soms termineert (dwz, er bestaan eindige executies) en bovendien dat elk natuurlijk getal de eindwaarde van j kan zijn. Het is immers (voor willekeurig getal x) mogelijk dat er eerst x ronden van thread 1 worden afgewerkt en dat daarna thread 2 wordt gescheduled. Daarom is de executieboom in feite oneindig diep. Maar wat te denken van een executie waarin alleen maar instructies van thread 1 worden gedaan? Die kun je oneindig lang voortzetten zonder dat hij termineert. Ondertussen is de instructie van thread 2 dan wel al die tijd executeerbaar; het lijkt niet erg eerlijk om in een zo lange executie alleen thread 1 aan het woord te laten en thread 2 oneindig lang te negeren. Definitie 1.3 Een scheduler is fair als een instructie die continu executeerbaar is ook voorkomt. De genoemde oneindige executie is niet fair en wij nemen steeds aan dat de scheduler fair is. Programma 1.11 is dan een terminerend programma dat elk natuurlijk getal als uitvoer kan hebben. Hoewel de executieboom oneindig diep is, en dus een oneindig lang pad bevat, is dit pad niet als executie toegestaan onder een faire scheduler. Het bewijzen van terminatie met een fair scheduler is doorgaans lastiger dan wanneer de aanname van fairness niet wordt gemaakt. Een boek van Francez [Fra86] behandelt diverse vormen van fairness en de bijbehorende bewijsmethoden uitgebreid. Aanname van fairness sluit niet automatisch alle oneindige executies uit: slechts wordt uitgesloten dat gedurende een oneindig lange executie een bepaalde instructie consequent wordt

25 Opgaven bij hoofdstuk 1 15 overgeslagen bij de keuze. De aanname van fairness verandert niets aan het droeve noodlot van programma 1.9A. Immers, de oneindige executie sluit geen instructies af en is dus niet unfair. Opgaven bij hoofdstuk 1 Opgave 1.1 Hoeveel aanroepen van binco doet programma 1.1 om het ook sneller? ( n k ) te berekenen? Kan Opgave 1.2 Waarom ? Hoeveel interleavings zijn er als thread 1 k 1, en thread 2 k 2 instructies heeft? Als er drie threads zijn? Opgave 1.3 Hoe wordt concurrency uitgedrukt in C++, Java,...? (Vul op de stippels je favoriete programmeertaal in.) Hoe wordt concurrency op jouw computer uitgevoerd? Opgave 1.4 S1 en S2 zijn operaties met de eigenschap dat S1; S2 precies hetzelfde doet als S2; S1. Impliceert dit dat cobegin S1 S2 coend ook hetzelfde doet? Opgave 1.5 Operaties. (Uit het tentamen van februari 2001.) Van operaties S1 en S2 is gegeven dat S1 ; S2 hetzelfde effect heeft als S2 ; S1, dwz. bij sequentiële uitvoering is de volgorde niet van belang voor het resultaat. (a) Impliceert dit, dat ook cobegin S1, S2 coend hetzelfde effect heeft? Geef een bewijs of tegenvoorbeeld. (b) Wanneer zijn operaties atomair? (c) Neem nu als extra gegeven aan dat S1 en S2 atomair zijn en beantwoord vraag (a) opnieuw. Opgave 1.6 Definities. (Uit het tentamen van februari 2002.) (a) Wat is het verschil tussen een busy wait en een blocked wait? Van welk type zijn (1) de wait()-instructie, (2) de sleep(..)-instructie? (b) Wanneer is een executie unfair? (c) Wanneer zijn operaties atomair? Opgave 1.7 Basisbegrippen. (Uit het tentamen van mei 2002.) (a) Wat betekent fairness? (b) Leg het verschil uit tussen non-determinisme en randomness. (c) Wanneer is een collectie operaties atomair? Opgave 1.8 Basisbegrippen. (Uit het tentamen van mei 2000.) Om concurrent gedrag van programma s af te leiden uit sequentiëel gedrag van de gebruikte objecten gebruiken we het Onafhankelijkheids-axioma en Atomiciteit. Wat wordt onder deze begrippen verstaan? Opgave 1.9 Bekijk alle interleavings van cobegin { LOADA c ; INCRA ; STOREA c } { LOADA c ; ADDA 2 ; STOREA c } coend

26 16 1 Inleiding: Concurrency (ADDA 2 is een instructie die 2 bij register A optelt.) Veronderstel dat de afzonderlijke instructies atomair zijn. In hoeveel interleavings wordt c uiteindelijk met 0, met 1, met 2 en met 3 opgehoogd? Opgave 1.10 Scheduler. (Uit het tentamen van augustus 2000.) Wat is een scheduler? Kun je iets zeggen over de implementatie van schedulers? Wanneer is een scheduler fair? Opgave 1.11 Fairness. (Uit het tentamen van oktober 2006.) In dit programma kijkt thread 1 steeds naar variabele i, die door thread 2 afwisselend op 0 en 1 wordt gezet; initieel is i=0 en s=true: Thread 1: Thread 2: t = 0 while (i==0) while (s) { t=t+1 } { i=1-i } s=false print t (a) Beschrijf van dit programma een executie waarin thread 2 de waarde 2 print. (b) Beschrijf een oneindige executie die niet kan voorkomen als de scheduler fair is. (c) Beschrijf een oneindige executie die wel mogelijk is onder een fair scheduler. Opgave 1.12 Fairness. (Uit het tentamen van september 2004.) Dit programma (1.9A) heeft in een lusje twee concurrente statements, die i op 0, respectievelijk 1 zetten: i = 0 ; t = 0 ; while ( i==0 ) { cobegin { i=0 } { i=1 ; t = t+1 } coend ; print i ; } Er is read/write atomicity. (a) Laat zien dat er voor elke k N een executie is die een rij van k nullen en een één print. Wat is na afloop de waarde van t? (b) Geef de definitie van een fair scheduler. (c) Neem aan dat de scheduler fair is; is een oneindige executie van bovenstaand programma nu mogelijk? Leg uit! Opgave 1.13 Wat is na afloop van programma 1.5 de minimale, en wat de maximale waarde van c als het wordt uitgevoerd onder read-write atomicity? En als beide threads tienmaal c ophogen? Opgave 1.14 Experimenteer met programma Opgave 1.15 Geef bij elke toestand in figuur 1.8 en 1.12 de waarden van alle gebruikte variabelen aan.

27 Hoofdstuk 2 Mutual exclusion In dit en de volgende twee hoofdstukken kijken we naar een manier om niet-atomaire instructies in een concurrent programma te gebruiken. Het probleem om verschillende threads samen toegang te geven tot variabelen of andere resources in een systeem heet synchronisatie. In de volgende hoofdstukken wordt synchronisatie verkregen door binnen het tijdsinterval dat een thread met zekere actie bezig is, de daadwerkelijke toegang tot de gebruikte variabelen te concentreren in een kleiner interval. Het is hiervoor onder andere nodig, dat een thread wacht tot een andere thread met deze variabelen klaar is. Herinner, dat volgens de definitie operaties atomair zijn wanneer het resultaat van een aantal operaties, mogelijk geheel of deels overlappend, hetzelfde is als wanneer ze na elkaar worden uitgevoerd. Door te zorgen dat ze na elkaar worden uitgevoerd zal het resultaat hier inderdaad aan voldoen; we spreken dan van het sequentialiseren van operaties. Gedurende een periode van ongeveer 30 jaar (van 1960 tot 1990) was het sequentialiseren de enige manier om atomiciteit te verkrijgen, en het is nog steeds de meest gebruikte. Nog steeds wordt vaak gedacht dat atomiciteit impliceert (of per definitie equivalent is met) dat operaties ononderbroken worden uitgevoerd en dat andere threads noodzakelijk op het aflopen ervan moeten wachten. Sinds circa 1990 bestaan er echter alternatieve technieken om threads zonder wachten objecten te kunnen laten delen; deze worden vanaf hoofdstuk 10 besproken. 2.1 Probleemstelling Denk nog even terug aan de situatie waar verschillende threads updates doen op dezelfde variabele, dus zo: Thread 1 Thread 2 kritiek Figuur 2.1: Sequentialisatie van kritieke secties. 17

28 18 2 Mutual exclusion... cobegin Entry... CS... Exit Entry... CS... Exit Entry Entry CS CS Exit Exit coend Programma 2.2: Kritieke sectie, entry, exit.... c = c+1 c = c+1 c = c+1 c = c+1... Het systeem werkt correct als de operatie c = c+1 atomair is, maar dat is die niet: door interleaving van instructies is het resultaat van twee overlappende uitvoeringen ervan verschillend van twee uitvoeringen na elkaar. De oplossing die hiervoor al in de jaren vijftig werd bedacht is afgekeken van het kleinste kamertje van elk gebouw: een proces (of thread) dat aan de variable wil komen, sluit die als het ware voor anderen af. Anderen vinden de deur op slot, en hebben pas weer toegang als het bordje op VRIJ staat. Om de discussie algemener te maken volgen we de terminologie die hier gebruikelijk is en spreken van kritieke secties in het programma. Deze kritieke sectie mag willekeurige dingen doen met gedeelde variabelen, waar we verder niet op ingaan, we gaan ons concentreren op de vraag hoe je ervoor kunt zorgen dat er slechts één thread tegelijk mee bezig is. Het op slot zetten en vrijmaken van de kritieke sectie gebeurt in stukjes code die we het entry protocol en exit protocol noemen. De threads komen er dus uit te zien als programma 2.2; merk op, dat de Entry- en Exit-code expliciet in het programma wordt opgenomen op elke plaats waar het van gedeelde variabelen gebruik maakt. Een thread die ergens op de stippeltjes zit noemen we in de niet-kritieke sectie. In het programma dat de kritieke secties bevat wordt elk voorkomen ervan opgesierd met het entry- en exitprotocol. Vanuit het programma gezien gaat dit deel er eigenlijk deel van uitmaken en verschillende threads kunnen dan overlappend in het fragment Entry/Kritiek/Exit zitten. In de entry en exit-code worden de beschermde variabelen natuurlijk nog niet gebruikt, zodat het er vanuit de kritieke sectie uitziet alsof deze code een soort politieagent is die de toegang tot de kritieke sectie regelt. In figuur 2.1 zou je kunnen zeggen dat het programma de grote (mogelijk overlappende) intervallen ziet en de kritieke sectie de grijze (disjuncte) intervallen. Sequentialisatie kan alleen bereikt worden door threads op elkaar te laten wachten; immers, als een thread de varabelen wil gebruiken terwijl een andere kritiek is, kan de eerstgenoemde pas verder als de tweede het gebruik van de variabelen beëindigd heeft. Het wachten kan op

29 2.2 Vier eenvoudige algoritmen 19 verschillende manieren vorm worden gegeven, maar karakteriserend voor alle vormen is dat de voortgang van een thread afhangt van de voortgang van een andere thread. Hoe moet de synchronisatiecode eruit zien als we in ieder geval willen dat de threads elkaar wederzijds uitsluiten? Met andere woorden, de code moet resulteren in de volgende eigenschap van executies van het systeem. Mutual exclusion: Er is hoogstens één proces tegelijk in een kritieke sectie. We moeten er aan denken, dat elke thread willekeurig vaak of weinig toegang tot de kritieke sectie kan willen hebben. We maken de gebruikelijke aannamen van read-write atomicity en fairness. 2.2 Vier eenvoudige algoritmen We beginnen eenvoudig, maar komen er dan achter dat eenvoudige dingen niet altijd zijn wat we willen. Dit zal ook leiden tot een uitbreiding van de specificatie van de gevraagde synchronisatiecode, die behalve mutual exclusion ook progress en no starvation (die later gedefinieerd zullen worden) zal omvatten Eerste poging: bezetvlaggen In programma 2.3 is er voor elke thread een variablele (flag1 voor thread 1 en flag2 voor thread 2) die aangeeft of de thread kritiek is. Voor het binnengaan kijkt de thread of de ander kritiek is. Het programma bevat het statement while flag2 { } en dit is een voorbeeld van een busy wait, waarbij een thread tot nietsdoen veroordeeld is tot een bepaalde conditie geldt, maar in feite actief op die conditie wacht. De busy wait bestaat uit een normale lus-constructie met conditie, maar een lege body, zodat alleen de conditie herhaald wordt getest totdat hij een keer false is. Zo n constructie zou in een niet-concurrente omgeving meestal zinloos zijn, want als de conditie true is, zal hij dat de volgende keer weer zijn. In een concurrente omgeving is het natuurlijk mogelijk dat de variabelen in de conditie door een andere thread worden gewijzigd zodat het herhaaldelijk testen kan worden beëindigd. (In oudere programmeertalen was er soms een instructie skip of nop, die geen enkel effect had en niets deed en waarmee de busy wait beschreven werd.) Deze busy wait verschilt sterk van de constructie wait (of while (conditie) wait) die we later zullen zien: wait is namelijk een speciale instructie die wel degelijk iets doet met de aanroepende thread; zie hoofdstuk 3. Helaas komen we er al snel achter dat dit programma niet werkt, en wel doordat het entry protocol zelf niet atomair is. Door de gemaakte aanname van read-write atomicity mogen we wel over het programma redeneren alsof reads en writes na elkaar gebeuren (in een of andere volgorde), dus dat er een interleaving plaatsvindt van de instructies van de twee threads. Dit kan gebeuren: 1. Threads 1 en 2 beginnen allebei aan entry en lezen de vlag van de ander. 2. Beide lezen False en komen door de busy wait. 3. Beide zetten hun vlag op True en gaan de kritieke sectie binnen. Kortom, deze eerste poging voldoet niet aan de eis van Mutual Exclusion.

30 20 2 Mutual exclusion flag1 = False ; flag2 = False ; cobegin... while (flag2) { } while (flag1) { } // Entry flag1 = True flag2 = True CS CS // Kritiek flag1 = False flag2 = False // Exit coend Programma 2.3: Beveiliging met twee vlaggen Tweede poging: de safe sluice Laten we ervoor zorgen dat een thread eerst de weg voor de ander blokkeert en dan wacht tot de ander niet-kritiek is; programma 2.5. Stelling 2.1 Programma 2.5 voldoet aan mutual exclusion. Bewijs. Het programma bevat een interactie die ook in verdere oplossingen de eis van mutual exclusion afdwingt en daarom staan we hier uitgebreider bij stil. De interactie waar het om gaat is dat (1) de eigen vlag wordt gezet voordat de kritieke sectie wordt ingegaan en wordt gereset nadat de sectie is afgelopen, zodat de vlag in ieder geval tijdens de kritieke sectie continu op True staat; (2) er tussen het zetten van de vlag en het ingaan van de kritieke sectie een moment is waarop de vlag van de ander op False staat; zie figuur 2.4. Deze, voor de safe sluice karakteristieke, reeks gebeurtenissen komt ook voor in latere oplossingen en garandeert dat aan de mutual exclusion eis wordt voldaan. Om te bewijzen dat er hoogstens een thread kritiek is beschrijven we de interactie iets explicieter. Stel thread 1 is op zeker moment t 2 kritiek; zij t 1 het moment waarop hij flag1 op True zette. Omdat elke thread alleen aan zijn eigen vlag zit, is flag1 == True continu tussen t 1 en t 2. Voor kritiek te worden, op een moment t 3 tussen t 1 en t 2, heeft thread 1 flag2 == False gelezen en op dat moment was thread 2 niet kritiek of in het entry protocol. Tussen t 3 en t 2 was flag1 continu True dus kon thread 2 niet door het entry protocol komen. flag1 True False kritieke sectie Op t 2 is thread 1 kritiek Op t 3 was flag2 == False Op t 1 werd flag1 gezet Figuur 2.4: Interactie in de safe sluice.

31 2.2 Vier eenvoudige algoritmen 21 flag1 = False ; flag2 = False cobegin... flag1 = True flag2 = True // Entry while (flag2) { } while (flag1) { } CS CS // Kritiek flag1 = False flag2 = False // Exit coend Programma 2.5: De safe sluice. Programma 2.5 voldoet dus aan de mutual exclusion eis, maar helaas is het evenmin bruikbaar als het vorige omdat het volgende kan gebeuren: 1. Beide threads beginnen het entry protocol en zetten hun vlag op True. 2. Beide threads beginnen te wachten tot de andere vlag op False staat. 3. Beide threads blijven wachten tot de andere vlag op False staat. Deze situatie, waarin beide threads nooit verder kunnen komen noemen we deadlock. Jammer dat we dit niet hadden voorzien, we hadden onze eisen op de oplossing wat uitgebreider moeten maken. De reeds genoemde eis sluit situaties uit die niet mogen voorkomen; begrijpelijk dat deze eis kan worden overtreden in een eindig deel van een executie (namelijk, als het wel gebeurt). Een dergelijke eis noemen we een safety (veiligheids-) eigenschap. Een programma dat helemaal niets doet voldoet altijd aan elke veiligheidseis. Inderdaad, zouden we in het entry protocol simpelweg schrijven while True do { } dan blijft elke thread erin steken en zijn er nooit twee tegelijk kritiek! Het lijkt flauw om met deze oplossing voor de dag te komen, maar bedenk dat het belang van een specificatie is, een volledige karakterisering te geven van het toegestane gedrag van een programma. Dat we met programma 2.5 niet tevreden zijn komt dus omdat we de specificatie te beperkt hebben opgesteld. Naast een safety-eis hebben programma s ook altijd eisen die stellen wat er wel uiteindelijk moet gebeuren. Een dergelijke eis wordt alleen overtreden in een oneindig lange executie (want zolang het niet is gebeurd, kan het altijd nog gebeuren). Zo n eis noemen we een liveness (voortgangs-) eigenschap. Programma 2.5 voldoet wel aan de safety eigenschap en wordt de safe sluice genoemd. We stellen nu behalve mutual exclusion een voortgangseis, en wel: Progress: Als er (één of meer) threads in het entry protocol zitten terwijl de CS beschikbaar is, is er uiteindelijk een thread kritiek. Aan deze eis voldoet programma 2.5 duidelijk niet omdat het mogelijk is dat de beide threads elkaar in het entry-deel blokkeren. Ook wanneer de aanname van fairness van de scheduler wordt gebruikt blijft deze blokkering mogelijk. De aanname van fairness tussen threads is altijd nodig om progress te kunnen bewijzen. Zonder fairness is een executie mogelijk waarin thread 1 de kritieke sectie heeft verlaten (zodat

32 22 2 Mutual exclusion flag1 = False ; flag2 = False cobegin... flag1 = True flag2 = True // Entry while (flag2) { } while (flag1) { flag2 = False while (flag1) { } flag2 = True } CS CS // Kritiek flag1 = False flag2 = False // Exit coend Programma 2.6: De safe sluice met na-u. die beschikbaar is) maar nog niet aan het exit-deel is begonnen. Als de scheduler nu steeds thread 2 laat executeren (bijvoorbeeld in een busy wait om in de kritieke sectie te komen) en thread 1 niet aan bod laat komen, zal thread 2 nooit kritiek kunnen worden. Inherent aan synchronisatie door middel van wachten is, dat een thread die in de kritieke sectie blijft (bijvoorbeeld door in een oneindige lus te komen), de toegang tot de kritieke sectie voor anderen blokkeert. We zullen zeggen dat de fairness aanname impliceert dat een kritieke thread ook aan het exit protocol toekomt De safe sluice met na-u Om de mogelijkheid van blijven steken in de entry uit te sluiten wordt de safe sluice uitgebreid als in programma 2.6. Als beide threads in de entry komen, zal thread 2 zich galant terugtrekken ( na-u -gedrag) waardoor thread 1 kritiek kan worden. Als flag1 == False is probeert thread 2 het opnieuw. Omdat dit programma de safe sluice in zekere zin bevat geldt de mutual exlusion eigenschap; ga na dat de interactie die de safe sluice veilig doet zijn in dit programma aanwezig is. De progress eigenschap geldt ook. De threads kunnen niet elkaar vasthouden in de entry omdat thread 2 een stap terug doet en daarmee thread 1 doorgang verleent. We gaan hier nu niet verder op in, omdat het programma een aantal nadelige eigenschappen heeft. Het is niet symmetrisch, zodat dat een thread altijd moet weten, of hij de rol van thread 1 of van thread 2 moet spelen. Alle bewijzen moeten twee keer worden gegeven, omdat er geen symmetrie meer is. Verder heeft thread 1 heeft een structureel voordeel over thread 2. Veel ernstiger is het dat als thread 2 kritiek wil worden, er geen garantie is dat dat ooit lukt. Thread 1 kan na de kritieke sectie weer het entry protocol binnengaan, en als in de tussentijd thread 2 niet snel genoeg naar de kritieke sectie opstoomt vist hij achter het net. Dit kan zich willekeurig vaak herhalen, en we spreken dan van starvation (uithongeren). Waarom is de progress-eis niet voldoende om dit gedrag uit te sluiten? Progress vereist dat, als er een of meer threads wachten, er ooit een thread kritiek zal zijn. De garantie is dus alleen, dat de kritieke sectie weer benut zal worden, maar hiermee is nog geen individuele garantie voor een wachtende thread. De safe sluice met na u is derhalve onbevredigend, hoewel hij voldoet aan

33 2.2 Vier eenvoudige algoritmen 23 de beide geformuleerde eisen. Daarom breiden we de specificatie van de gewenste oplossing nog eenmaal uit, en wel door te eisen dat er geen starvation optreedt. No starvation: Als een thread aan het entry-deel begint, zal hij ook ooit kritiek zijn. De eis van no starvation is strikt sterker dan progress, maar toch wordt progress meestal apart bewezen. De reden is dat no starvation volgt uit progress en een grens op het aantal malen dat een andere thread aan de kritieke sectie kan beginnen terwijl een thread wacht. Stelling 2.2 Veronderstel dat een zekere synchronisatiecode voldoet aan (1) progress en (2) terwijl een thread y wacht in het entry-protocol kan slechts eindig vaak een andere thread kritiek worden. Dan voldoet de code aan no starvation. Bewijs. We leiden een tegenspraak af uit de veronderstelling dat thread y aan starvation leidt, dwz., dat y voor altijd in het entry-protocol wacht zonder ooit kritiek te kunnen worden. Stel y wacht op tijd t 1. Wegens progress komt er na t 1 een moment waarop er een thread kritiek is; wegens fairness komt er daarna weer een moment t 2 waarop de kritieke sectie vrij is. Maar op t 2 wacht y nog steeds, dus wegens progress komt er na t 2 weer een moment waarop de kritieke sectie bezet is; en wegens fairness weer een moment t 3 na t 2 waarop de kritieke sectie weer vrij is. De aannamen impliceren dat er een oneindige reeks van uitvoeringen van de kritieke sectie volgt. Er is tegenspraak met de tweede premisse. De eerste twee nadelen van programma 2.5 zijn op te lossen door het na-u-gedrag aan beide threads toe te voegen (safe sluice met na-u, na-u). De mogelijkheid tot starvation blijft echter bestaan, en een nog vreselijker mogelijkheid ontstaat: beide threads gaan de entry in, maar trekken zich steeds beide terug en gaan verder. Door dit na-u, na-u, na-u, na-u, na-u... gedrag wordt zelfs de progress eigenschap niet gerealiseerd! Een Salomonsoordeel: om de beurt De problemen met de eerdere oplossingen hadden ermee te maken dat er bij de toegangscontrole twee variabelen waren betrokken. In het entry protocol moet je twee variabelen gebruiken, en in beide gevallen ontstonden er problemen als de andere thread tussen die twee instructies actief is. Programma 2.7 regelt de toegang met slechts één variabele, turn, die 1 of 2 kan zijn en aangeeft welke thread kritiek mag zijn. Een thread geeft de beurt aan de ander na de kritieke sectie. Het is niet moeilijk te bewijzen dat dit programma aan mutual exclusion voldoet, maar hoe zit het met progress? Als beide threads in het entry protocol zitten heeft turn altijd 1 of 2 als waarde, en er kan daarom altijd beslist één van de processen door. Na afloop van de kritieke sectie geeft deze de beurt door en kan de ander. Er lijkt dus niets aan de hand. Schijn bedriegt echter. Een WC die je alleen om de beurt kunt gebruiken is knap lastig als je nodig moet en je huisgenoot is niet thuis (of drinkt minder...). Als er één thread kritiek wil worden, en turn heeft de waarde van de andere thread, kan de thread niet door. Dit is strijdig met de progress eis (onder de premisse is ook begrepen dat slechts één thread in de entry zit). Overigens is deze oplossing wel gebruikt, maar dat kan slechts in een zeer beperkte context, namelijk wanneer a priori is gegarandeerd dat beide threads precies even vaak kritiek willen worden. Dan nog kan de strakke regulering ongewenst zijn, omdat een snelle thread wordt afgeremd door de langzame.

34 24 2 Mutual exclusion... turn = 1 cobegin while (turn == 2) { } while (turn == 1) { } CS CS turn = 2 turn = coend Programma 2.7: Toegang via turn variabele. 2.3 Dekkers algoritme Door het geringe succes van de genoemde oplossingen vatte in de jaren vijftig de gedachte post, dat een oplossing voor het synchronisatieprobleem die voldoet aan mutual exclusion, progress en no starvation onmogelijk was met read-write atomicity. Er werden daarom, door aanpassing van het operating system, variabelen geïmplementeerd met sterkere atomaire operaties dan lezen en schrijven, de zogenaamde semaforen; zie hoofdstuk 3. Een beetje voorbarig, want rond 1960 vond de Nederlandse wiskundige Dekker alsnog een oplossing; zie programma 2.8. Dekkers algoritme (beschreven in [Dij68]) combineert de drie laatste algoritmen die we hebben gezien: de basis wordt weer door de safe sluice gevormd en deadlock wordt voorkomen met een na-u-mechanisme. Dit wordt feitelijk enkelzijdig toegepast, waarbij de partij die na-u-... turn = 1 cobegin flag1 = True flag2 = True while (flag2) while (flag1) if (turn==2) if (turn==1) { flag1 = False { flag2 = False while (turn==2) { } while (turn==1) { } flag1 = True flag2 = True } } CS CS turn = 2 ; flag1 = False turn = 1 ; flag2 = False coend Programma 2.8: Dekkers algoritme.

35 2.3 Dekkers algoritme 25 Kader 2.9: Edsger Wybe Dijkstra ( ) De invloed van de geniale, wereldberoemde, ja zelfs legendarische, doch enigzins eigenzinnige Nederlandse wiskundige en s werelds eerste programmeur Edsger W. Dijkstra is merkbaar in bijna alle gebieden van de informatica, maar is nergens zo groot als in het ontwerp van (gedistribueerde) algoritmen. Dijkstra begon zijn werk als programmeur in Amsterdam in de jaren vijftig. Onder andere programmeerde hij rekenwerk voor de Deltawerken en voor het nieuwe Fokker Friendship vliegtuig. Diverse algoritmen die hij later heeft ontworpen zijn zeer bekend, zoals het kortste-padalgoritme dat nog steeds het meest gebruikte is in bijvoorbeeld routeplanners. Zijn ontwerpfilosofie bestaat hieruit, dat een programma feitelijk stap voor stap wordt afgeleid uit de specificaties, waarbij steeds wordt bijgehouden welke invarianten gelden en welke doelen worden gehaald. Op deze manier wordt de uiteindelijke oplossing verkregen tesamen met een (bijna formeel) correctheidsbewijs ervoor. Dijkstra promoveerde in 1959 in Amsterdam, en werd in 1962 hoogleraar aan de Technische Hogeschool in Eindhoven. Vanaf 1972 werkte hij voor Burroughs en na 1984 was hij hoogleraar op de Schlumberger leerstoel aan de Universiteit van Texas in Austin. Dijkstra overleed op 6 augustus 2002 in Nuenen. De Principles of Distributed Computing Influential Paper Award werd daarna de Dijkstra-prijs genoemd (zie Kader 6.7). gedrag vertoont, wisselt dmv. een turn variabele. Dat het algoritme aan mutual exclusion voldoet kan bewezen worden als programma 2.5. Als een thread de entry ingaat terwijl de andere in de niet-kritieke sectie is (met vlag False dus), kan het entry protocol zonder wachten worden doorlopen; alleen als de andere partij de vlag aan heeft komt het na-u-mechanisme in werking. Als beide threads kritiek willen worden ontstaat er geen deadlock zoals bij programma 2.5 omdat één van de threads zich terugtrekt, en wel de thread die het laatst de kritieke sectie verlaten heeft. Bedenk, dat turn alleen kan veranderen in het exit protocol, dus onveranderd blijft zolang beide threads in de entry zitten. De door turn aangegeven thread doet geen stap terug en wacht slechts op de andere vlag. De andere thread komt ofwel door de entry heen, danwel zet z n vlag op False en wacht tot het zijn beurt is. Tenslotte wordt ook starvation voorkomen. Een thread die na-u-gedrag vertoont, wacht in de binnenste lus tot hij door turn wordt aangegeven; dit gebeurt zodra de andere thread de kritieke sectie verlaat. Dan wordt de eigen vlag gezet en hoeft geen na-u-gedrag meer te worden vertoond. Tot nu toe bekeken we oplossingen voor twee threads, maar een object kan ook tussen meerdere threads worden gedeeld. Dijkstra (kader 2.9) heeft Dekkers oplossing uitgebreid naar een groter aantal threads.

36 26 2 Mutual exclusion 2.4 Lamports bakery-algoritme Een ander, bekender algoritme voor mutual exclusion, dat goed te gebruiken is voor meer dan twee threads, is Lamports bakery-algoritme. [Lam74] De naam is ontleend aan gebruikelijke praktijken in de detailhandel, waar je bij binnenkomst een bonnetje krijgt met een nummer dat groter is dan iedereen die voor jou een nummertje kreeg, waarna je moet wachten tot jouw nummer kleiner is dan dat van alle anderen; zie programma Nu is bij de bakker het nummertjesapparaat van nature atomair: het deelt automatisch allemaal verschillende nummers uit en bepaalt daarmee meteen de volgorde van afhandeling. De nummerautomaat in Lamports algoritme is veel omslachtiger en garandeert niet dat alle nummers verschillend zijn. Bij binnenkomst leest een thread eerst eenmaal de nummers van de anderen, bepaalt het maximum en kiest een groter nummer (num[i] = mn+1). Omdat twee threads best hetzelfde getal kunnen trekken, is er verdere arbitrage op basis van het procesnummer; als proces i het nummer k krijgt, is zijn volgnummer feitelijk het paar (k, i). Omdat alle procesnummers verschillend zijn, zijn alle volgnummers verschillend en de lexicografische ordening zorgt ervoor dat de volgorde van aankomst toch nog enigzins in de volgnummers weerspiegeld wordt. Tijdens het kiezen van het ticket zet thread i choosing[i] op True. Na het kiezen van een ticket wacht de thread totdat elke andere thread buiten het kies-gedeelte geen of een groter ticket heeft. Lemma 2.3 Als P i kritiek is en er is een k met num[k] 0, dan is (num[k], k) (num[i], i). Bewijs. In het entry-protocol heeft P i een nummer gekozen (zie figuur 2.11) en toen gewacht tot P k (a) niet aan het kiezen was en (b) geen of een groter ticket had. Geval 1: P i zag num[k] gelijk aan 0, dus geen ticket. Als k nu een positief getal heeft, begon hij met kiezen na moment (a), dus nadat P i het kiezen had afgesloten. De waarde van num[i] verandert niet tussen het kiezen door P i en het verlaten van de kritieke sectie, en deze waarde is dus door P k gezien bij het kiezen. De keuze van k valt dan op een getal groter dan num[i]. Geval 2: P i zag (num[k], k) (num[i], i). Zelfs als P k daarna een ander nummer heeft getrokken, zal dat dan een nog groter getal zijn. Lemma 2.4 Als P i kritiek is, is num[i] > 0. Bewijs. Bij het kiezen krijgt een thread altijd een positief getal, en pas na de kritieke sectie wordt de waarde weer op 0 gezet. Stelling 2.5 Het bakery-algoritme voldoet aan mutual exclusion: als P i en P j kritiek zijn geldt P i = P j. Bewijs. Stel dat P i en P j kritiek zijn. Lemma 2.4 zegt dat num[i] > 0 en num[j] > 0, waarmee lemma 2.3 dan impliceert (num[j], j) (num[i], i) en (num[i], i) (num[j], j), met andere woorden, i = j. Er is dus altijd hoogstens één thread kritiek.

37 2.4 Lamports bakery-algoritme 27 num[i] = 0 ; choosing[i] = False cobegin P1 P2 P Pn coend Pi:... choosing[i] = True mn = 0 for (j=1 ; j <= n ; j++) mn = max (mn, num[j] ) num[i] = mn+1 choosing[i] = False // Entry: Neem ticket // hulpvariabele, niet gedeeld for (j=1 ; j <= n ; j++) // Entry: Wacht op beurt { while choosing[j] { } while ( num[j] > 0 and (num[j],j) < (num[i],i) ) { } } CS num[i] = 0... // Exit Programma 2.10: Het bakery-algoritme (1974). Vervolgens bewijzen we dat het algoritme vrij is van starvation, dus dat elke thread die het entry-protocol ingaat, uiteindelijk ook aan de beurt komt. Zoals eerder is aangegeven, is het handig om hiervoor eerst progress te bewijzen. Stelling 2.6 Het bakery-algoritme voldoet aan progress: als er threads wachten en de kritieke sectie is beschikbaar, kan uiteindelijk een thread kritiek worden. Bewijs. Veronderstel dat er een of meer threads wachten om tot de kritieke sectie toegelaten te worden. Voor het kiezen van een ticket hoeft niet te worden gewacht, dus wegens fairness krijgt elke wachtende thread een ticket. Veronderstel verder dat de kritieke sectie vrij is; er k kiest niet (a) k heeft geen of groter ticket (b) i kiest i is kritiek Figuur 2.11: Kiezen, wachten en kritieke sectie.

38 28 2 Mutual exclusion Kader 2.12: Begrensde tickets in het bakery-algoritme. Wetenschap staat nooit stil, er valt altijd iets nieuws te ontdekken. In 2001 lieten Jayanti, Tan, Friedland en Katz [JTFK01] zien dat je Lamports algoritme kunt aanpassen zodat de tickets in begrensde variabelen kunnen worden gerepresenteerd. Moeilijkheid hierbij was, dat het verschil tussen de grootste en de kleinste numwaarde willekeurig groot kan worden. Met een modificatie van het kies-gedeelte werd bereikt, dat dit verschil door n begrensd is. Het is dan voldoende, de waarde van num modulo n te representeren. Foto: King Tan vertelt over het gewijzigde algoritme op de SOFSEM-conferentie in Slowakije, november kan dan hoogstens nog een thread in het exit-protocol zijn, maar hierin hoeft niet te worden gewacht dus wordt een situatie bereikt waarin deze thread zijn num op 0 heeft gezet. Zij nu P i de thread met het laagste ticket. Als P i het kiezen heeft afgesloten komen er geen lagere tickets meer (dat kan eventueel pas weer als P i de kritieke sectie verlaten heeft), dus P i blijft de thread met laagste ticket. Thread P i moet voor elke thread wachten tot die niet kiest en een hoger ticket heeft. Het kiezen duurt altijd maar eindig lang omdat er niet in wordt gewacht, en door de keuze van P i hoeft deze niet op lagere tickets te wachten. Thread P i kan daarom uiteindelijk de kritieke sectie ingaan. Stelling 2.7 Het bakery-algoritme voldoet aan no-starvation: als P i het entry protocol begint, komt hij ook aan de beurt. Bewijs. Wegens fairness heeft P i na enige tijd het kiezen afgerond en heeft dan een ticket. Alleen de (eindig veel) threads die op dat moment een kleiner ticket hebben of aan het kiezen zijn kunnen nog voor P i kritiek worden. Threads die na dat moment aan het entry protocol beginnen krijgen een groter ticket en kunnen pas kritiek worden nadat P i kritiek is geweest. Met stelling 2.2 impliceert dit dat er geen starvation is. Lamports algoritme is populair, maar heeft een nadeel in situaties waar de threads ongelimiteerd lang actief blijven (bv. servers). De getallen in de tickets kunnen onbegrensd groot worden, en implementatie in een begrensde variabele leidt vroeg of laat tot een onverwachte variant van het millenniumprobleem. Er bestaan ook algoritmen die hetzelfde probleem oplossen met begrensde variabelen. Samenvatting en conclusies In dit hoofdstuk bekeken we een veel voorkomend synchronisatieprobleem, namelijk, hoe nietatomaire code, een kritieke sectie, kan worden gebruikt tussen threads. Meestal wordt zulke

39 Opgaven bij hoofdstuk 2 29 code dan gesequentialiseerd, wat betekent dat er voor wordt gezorgd, dat nooit meer dan een thread tegelijk met de kritieke sectie bezig is. De kritieke sectie wordt in elke thread omgeven door een entry- en exit-protocol; de specificatie hiervan bestaat uit de volgende drie eisen: Mutual exclusion: Hoogstens één proces tegelijk is in de kritieke sectie. Progress: Als er threads in het entry protocol zitten terwijl de CS beschikbaar is, is er uiteindelijk een thread kritiek. No starvation: Als een thread aan het entry-deel begint, zal hij ook ooit kritiek zijn. De progress-eis wordt geïmpliceerd door no starvation, maar wordt meestal apart opgenomen ten behoeve van het bewijs. De belangrijkste boodschap van dit hoofdstuk is, dat het mogelijk is om deze drie eisen te verenigen als (1) de scheduler fair is en (2) er read-write atomicity is. Dit werd rond 1960 ontdekt door Dekker en Dijkstra, en een populaire oplossing voor meerdere threads is Lamports bakery-algoritme. De bescherming van kritieke secties doormiddel van entry- en exitcode heeft echter een paar belangrijke nadelen. De busy wait is duur. Terwijl een thread wacht, blijft hij processortijd (en geheugencycles) gebruiken. Veel werk voor de programmeur. Overal in de threads waar gedeelde variabelen worden gebruikt moet de programmeur de entry- en exitcodes in het programma opnemen. Plenty mogelijkheden tot fouten. Een programmeur kan van een stukje code over het hoofd zien dat het kritiek is, en vergeten het te beveiligen. Een programmeur kan het entry of exit protocol per ongeluk verkeerd typen. Bij niet-symmetrische oplossingen (zoals programma 2.6) kun je ook nog de verkeerde code gebruiken. Lastig bij dynamische multithreading. Het bakery-algoritme gaat uit van n genummerde threads. Worden threads dynamisch gecreëerd en beëindigd (bv. in een recursief programma) dan moeten nieuwe threads steeds een uniek nummer toebedeeld krijgen. Met de behandelde algoritmen is het probleem van atomiciteit wel in theoretische zin opgelost, maar nog niet op een manier die vlot en veilig met software-ontwerp is te integreren. Vandaar dat men heeft gezocht naar andere methoden waarmee wordt gegarandeerd dat een thread unieke toegang tot een object heeft; men vond die in de semaforen (jaren vijftig) en de monitors (jaren zeventig), die worden behandeld in hoofdstuk 3. Opgaven bij hoofdstuk 2 Opgave 2.1 Geef een interleaving van lees- en schrijfinstructies van programma 2.6 waarbij starvation optreedt. Opgave 2.2 Geef het programma van de safe sluice met na-u, na-u. Verzin een executie waarin beide threads kritiek willen worden maar beide in de entry blijven steken omdat ze elkaar oneindig vaak voor laten gaan. Je executie moet aan fairness voldoen. Opgave 2.3 Bewijs dat programma 2.7 aan mutual exclusion voldoet.

40 30 2 Mutual exclusion Opgave 2.4 Een nadeel van de busy wait is het processorgebruik door wachtende threads. Je kunt dit nadeel beperken door af te dwingen dat er minimaal een zekere tijd verloopt tussen twee opeenvolgende tests, als in while choosing[j] { sleep(500); } Bereikt deze wijziging het beoogde resultaat? Kun je nieuwe nadelen verzinnen die nu optreden? Opgave 2.5 Bewijs dat programma 2.8 aan mutual exclusion voldoet. Opgave 2.6 Hoe vaak kan in programma 2.8 thread 2 de kritieke sectie binnengaan terwijl thread 1 continu in het entry protocol zit? En hoe vaak terwijl thread 1 aan zijn busy wait bezig is? Opgave 2.7 Dekkers algoritme. (Uit het tentamen van mei 2000.) Hier is een gedeelte van Dekkers algoritme: flag1 = True flag2 = True while flag2 do while flag1 do if turn==2 then if turn==1 then { flag1 = False { flag2 = False while (turn==2) do skip while (turn==1) do skip flag1 = True flag2 = True } } CS CS turn = 2 ; flag1 = False turn = 1 ; flag2 = False (a) Leg uit waarom het algoritme de Safe Sluice bevat. (b) Wat bedoelen we met het Na-U mechanisme? (c) Wat regelt de variabele turn precies? Opgave 2.8 Safe Sluice. (Uit het tentamen van mei 2002.) Hier zie je een fragment uit de zogenaamde Safe Sluice (Prog. 2.5): flag1 = True while flag2 do skip CS flag1 = False flag2 = True while flag1 do skip CS flag2 = Flase (a) Waarvoor wordt Mutual-Exclusioncode gebruikt? Gebruikt dit programma een busy of blocked wait en waar zit die? (b) Aan welke eigenschap dankt het programma de aanduiding safe? Bewijs deze eigenschap. (c) Waarom is deze oplossing slecht bruikbaar, en hoe kan dit probleem worden opgelost? Opgave 2.9 Een jonge programmeur moet programma 2.6 implementeren. Omdat flag1 op False staat voordat in de binnenlus flag2 wordt gezet, redeneert hij dat het dan niet nodig is, die conditie in de buitenlus weer te testen: de thread kan meteen door. Hij maakt het programma daarom zo: flag1 = True flag2 = True // Entry while (flag2) { } if (flag1) { flag2 = False

41 Opgaven bij hoofdstuk 2 31 while (flag1) { } flag2 = True } CS CS // Kritiek flag1 = False flag2 = False // Exit Waarom is de redenering fout? Voldoet het programma nu nog aan mutual exclusion, aan progress en no starvation? Opgave 2.10 Deze vraag gaat over Dekkers algoritme (programma 2.8). (a) Leg uit waarom het algoritme de safe sluice bevat. (b) Wat bedoelen we met het na-u mechanisme? (c) Wat regelt de variabele turn precies? Opgave 2.11 Toegangscontrole. (Uit het tentamen van februari 2001.) Een applicatie met vier threads heeft in elke thread een kritieke sectie, waarop deze eis geldt: op elk moment mogen er ten hoogste twee threads in de kritieke sectie zijn. (a) Laat zien hoe deze toegangscontrole met een semafoor geïmplementeerd kan worden. Een jonge programmeur lost het probleem op met behulp van mutual exclusion code voor twee threads, die voldoet aan Mutual Exclusion, Progress en Geen Starvation. Threads 1 en 2 worden hiermee zo gesynchroniseerd dat steeds hoogstens één van hen kritiek is, en threads 3 en 4 worden ook zo gesynchroniseerd dat steeds hoogstens één van hen kritiek is. (b) Laat zien dat deze oplossing aan de bovengenoemde eis voldoet. (c) Hoe zit het met de Progress en Starvation bij deze oplossing? Opgave 2.12 Laat zien dat je het mutual exclusion probleem voor meerdere threads kunt oplossen door herhaald gebruik van de 2-thread versie. Laat zien dat zowel mutual exlusion als Progress en no starvation gelden voor de uitgebreide oplossing. (HINT: Organiseer de threads in een binaire boom.) Opgave 2.13 De choosing[i] variablele in het bakery-algoritme is echt nodig. Laat zien, dat als deze wordt weggelaten (samen met de betreffende busy wait statement), de mutual exclusion eigenschap niet geldt. Opgave 2.14 Bakery algoritme. (Uit het tentamen van januari 2006.) In Lamports Bakery algoritme kiest thread i een ticket door het bekijken van de ticket-nummers van andere threads, en schrijft dan de waarde van dat ticket in variabele num[i]. Beargumenteer, dat er vanaf het moment dat thread i de waarde van num[i] schrijft, ten hoogste nog n 1 threads de kritieke sectie in gaan voor thread i dat doet. Opgave 2.15 Lamports Bakery algoritme. (Uit het tentamen van september 2004.) In een systeem met n threads wordt Lamports Bakery algoritme gebruikt. We kijken naar een situatie waar thread i toegang vraagt tot de kritieke sectie, maar daarin volledig alleen is, dwz., gedurende het gehele tijdsinterval waarin i door het entry-protocol, de kritieke sectie, en het exit-protocol gaat, is geen enkele andere thread actief in een van deze programmadelen. Geef een zo exact mogelijke schatting van de kosten (gemeten in tijd en/of het aantal read/write operaties op gedeelde variabelen) die thread i maakt in (a) het entry-protocol; (b) het exitprotocol.

42 32 2 Mutual exclusion Opgave 2.16 Lamports Bakery Algoritme. (Uit het tentamen van oktober 2007.) In deze vraag kijken we naar het kiezen van tickets (volgnummers) in Lamports Bakery algoritme voor n-thread Mutual Exclusion. (a) Hoe kiest thread i een ticket in dit algoritme? (b) Toon aan: Als i continu een ticket heeft gedurende het kiezen van een ticket door j, dan is het door j gekozen ticket groter dan i s ticket. (c) Waarom kunnen, na het kiezen van een ticket door i, nog hoogstens n 1 threads voor i de kritieke sectie ingaan? Opgave 2.17 Bestudeer de volgende twee varianten op het bakery-algoritme. Zijn de algoritmen correct? Geef een bewijs of een tegenvoorbeeld. (a) De volgorde van testen op niet-kiezen en ticket wordt omgedraaid, als in: for (j=1 ; j <= n ; j++) { while ( num[j] > 0 and (num[j],j) < (num[i],i) ) { } while choosing[j] { } } (b) Test eerst alle threads op niet-kiezen, dan alle threads op ticket, als in: for (j=1 ; j <= n ; j++) while choosing[j] { } for (j=1 ; j <= n ; j++) while ( num[j] > 0 and (num[j],j) < (num[i],i) ) { }

43 Hoofdstuk 3 Synchronisatieprimitieven Synchronisatie van threads doormiddel van entry- en exitcode in programmatuur (zoals behandeld in hoofstuk 2) mag dan algoritmisch gezien een probleem oplossen, vanuit het oogpunt van programma-ontwerp is het verre van ideaal. Daarnaast is de gebruikte busy wait natuurlijk vanuit efficiëntie al uit den boze! Veel mooier zou het zijn om een thread die op een andere moet wachten tijdelijk in het geheel niet te schedulen, en hem pas weer actief te laten worden als hij ook daadwerkelijk verder kan. Dit kan alleen worden bereikt wanneer het wachten wordt geregeld in samenhang met het operating system, dat immers verantwoordelijk is voor het al dan niet aan de beurt komen van een thread. Wanneer een thread door het operating system (tijdelijk) buiten de verdeling van processortijd wordt gehouden spreken we van een blocked wait. Deze vorm van wachten is natuurlijk veel efficiënter dan de busy wait maar vereist wel dat in de programmeertaal mechanismen bestaan waarmee aan het besturingssysteem wordt doorgegeven dat een thread moet worden geblokkeerd of gedeblokkeerd. Het deblokkeren van een thread gebeurt vrijwel altijd vanuit een andere thread (waarom ligt dit voor de hand en wanneer is het niet zo?). Het oudste primitief van deze soort is de semafoor; deze werd al toegepast voordat oplossingen voor mutual exclusion waren uitgevonden. De mogelijkheden die dit primitief biedt zijn beperkt en er is nog vrij veel verantwoordelijkheid bij de programmeur om fouten te voorkomen. Operating systemen als Unix en Windows kennen dit primitief wel. Monitors bieden de mogelijkheid om de synchronisatiebehoefte van operaties op één plaats te specificeren, namelijk bij de operatie zelf. Hun introductie liep feitelijk vooruit op object-geörienteerd programmeren, en men vindt dit primitief bijvoorbeeld terug in Java. 3.1 Semaforen voor programmeurs Dijkstra introduceerde in de jaren vijftig de semafoor als een speciaal datatype (wij zouden nu zeggen: een class). De data van een semafoor bestaat slechts uit één integer waarde, en er zijn twee krachtige operaties (wij zouden zeggen methoden) die door concurrente threads kunnen worden aangeroepen, in Dijkstras tijd als system calls. Dijkstra sprak van de pak en de vrij operatie, P en V. In Engelstalige literatuur spreekt met doorgaans van wait respectievelijk signal, maar in dit boek worden deze termen niet gebruikt om verwarring met monitorinstructies te voorkomen. 33

44 34 3 Synchronisatieprimitieven P en V verlagen respectievelijk verhogen de waarde van de semafoor, op zich niet zo wereldschokkend dus, maar de kracht zit in de garantie van atomiciteit die wordt geboden: s.p: De operatie verlaagt atomair de waarde van s op een moment dat s positief is. Als S de waarde nul heeft blijft de thread eerst wachten tot hij positief is, en voltooit pas daarna. s.v: De operatie verhoogt atomair de waarde van s. Bij initialisatie kan een beginwaarde worden opgegeven, maar initialisatie mag niet concurrent met andere operaties worden uitgevoerd. Let op: het is doorgaans niet mogelijk de waarde van s te lezen. De specificatie van de semafoor kan worden gegeven in termen van het gedrag van de operaties P en V. Het is niet nodig hierbij aan de waarde te refereren. Er zijn drie eigenschappen, die vergelijkbaar zijn met de drie eigenschappen van mutual exclusion: Semafoor: Het aantal voltooide P-operaties is altijd ten hoogste het aantal begonnen V- operaties plus de initiële waarde. Progress: Als een of meer threads aan een P zijn begonnen en het aantal voltooide P-operaties is kleiner dan het aantal voltooide V-operaties plus de initiële waarde, zal een thread zijn P-operatie kunnen voltooien. No starvation: Als een thread aan de P-operatie is begonnen kan hij die ook voltooien, mits er willekeurig vaak een V-operatie zal worden uitgevoerd. Een mooie metafoor voor de semafoor is een pot met bonen. De V-operatie doet een boon in de pot, de P-operatie pakt er een uit. De semafoor-eis zegt dat je nooit meer bonen kunt pakken dan er zijn. De progress-eis zegt dat als er bonen beschikbaar zijn, het ook zal lukken om die te pakken. Let op het onderscheid tussen begonnen V-operaties in de semafooreis en voltooide V-operaties in de progress-eis. Een boon komt blijkbaar op een bepaald moment tijdens de V-operatie beschikbaar voor andere threads; het mag vanaf het begin, maar pas vanaf de voltooiing moet de boon beschikbaar zijn. De no-starvation-eis zegt dat een thread die een boon wil pakken niet oneindig vaak kan worden overgeslagen bij de toekenning van bonen. Er kunnen meerdere threads samen wachten op het positief worden van dezelfde semafoor. Voert een andere thread een V uit, dan kan maar één van de wachtende threads de P-operatie voltooien, immers, na de atomaire verlaging van s is de waarde weer 0 zodat andere threads niet door kunnen. Twee veelgemaakte beginnersfouten met semaforen hebben te maken met een verkeerd begrip van wat de semafoor doet. Ten eerste is de betekenis van P: pak een boon en niet probeer een boon te pakken ; de operatie wordt pas voltooid als er een boon verkregen is en kan dus niet onsuccesvol eindigen. Anders gezegd, het wachten op de boon is inbegrepen in de P-operatie en men hoeft dus niet een wachtlus te maken als in herhaal P tot er een boon gepakt is. Ten tweede wordt vaak over het hoofd gezien, dat er na een V-operatie slechts één thread een P kan voltooien, ook al zijn er meerdere threads met een P-operatie bezig. Men hoeft dus in een programma als bijvoorbeeld programma 3.1, na het voltooien van de P-operatie, niet te testen of er andere threads tegelijkertijd de P hebben voltooid Binaire en algemene semaforen In het algemeen kan de waarde van een semafoor elk natuurlijk getal zijn, maar soms kan in een bepaalde toepassing alleen de waarde 0 of 1 echt voorkomen (zie programma 3.1). Omdat

45 3.2 Mutual exclusion met semaforen s = new Semafoor (1) \\ initiele waarde 1 cobegin P1 P2 P Pn coend Pi:... s.p CS s.v... \\ entry code \\ exit code Programma 3.1: Mutual exclusion met semafoor. dit bij veel toepassingen zo is, zijn er terwille van gemakkelijke implementatie ook binaire semaforen bedacht. De waarde van zo n semafoor kan slechts 0 en 1 zijn. Er zijn verschillende mogelijkheden voor de betekenis van een V op een semafoor die al 1 is: De operatie doet niets. De operatie stopt het programma met een foutmelding. De operatie blokkeert de thread tot er een P is geweest die het ophogen mogelijk maakt. 3.2 Mutual exclusion met semaforen Een semafoor maakt het erg gemakkelijk om mutual exclusion te kunnen implementeren; zie programma 3.1. Als de kritieke sectie vrij is heeft s de waarde 1, en een thread die binnen gaat verlaagt s wanneer dat mogelijk is. Om bij de bonenpot te blijven: er is in het systeem één boon, initiëel in de pot, en wie kritiek wil worden bemachtigt eerst de boon en gooit hem na gebruik terug. Merk op, hoe gemakkelijk we (in vergelijking met bv. het bakery-algoritme) een multi-party versie van mutual exclusion voor elkaar krijgen; ook met dynamisch gecreëerde threads is er geen probleem. De vervelende busy-waitconstructie komt niet meer voor: de semantiek van de semafoor zegt wel dat een thread kan wachten, maar niet hoe dat gebeurt. In het operating system kan de semafoor best met een busy wait zijn geïmplementeerd, maar deze constructie is, door hem uit het programma naar het besturingssysteem te halen, onder het tapijt geveegd. De specificatie van de semafoor impliceert de correctheid van de mutual exclusion. Stelling 3.1 Als de semafoor voldoet aan de semafoor-eigenschap, heeft programma 3.1 de mutual-exclusion-eigenschap. Bewijs. Elke thread wisselt P- en V-operaties af, maar begint altijd met een P zodat het aantal begonnen V-operaties nooit hoger is dan het aantal voltooide P-operaties (door deze thread). Een thread die kritiek is, heeft één P-operatie meer voltooid dan V-operaties begonnen. Door voltooide P- en begonnen V-operaties over de threads te sommeren, vinden we dat als er k

46 36 3 Synchronisatieprimitieven threads kritiek zijn, het aantal voltooide P-operaties ten minste k hoger is dan het aantal begonnen V-operaties. Volgens semafoor kan het aantal voltooide P-operaties ten hoogste 1 meer bedragen dan het aantal begonnen V-operaties, waarmee volgt dat er hoogstens 1 thread kritiek is. Stelling 3.2 Als de semafoor voldoet aan progress, voldoet programma 3.1 aan progress. Bewijs. Let op, dat de te bewijzen progress-eigenschap de eis is voor mutual-exclusionoplossingen zoals in hoofdstuk 2 geformuleerd, terwijl de aan te nemen eigenschap die voor semaforen is zoals boven geformuleerd. Veronderstel dat er threads zijn die kritiek willen worden; volgens programma 3.1 beginnen zij een P-operatie. Veronderstel voorts dat er geen thread kritiek is; een eventuele thread die kritiek is geweest zal binnen enige tijd het exit-protocol hebben uitgevoerd, dat wil zeggen, een V-operatie voltooien. Er zijn dan evenveel V-operaties als P-operaties voltooid, terwijl de semafoor initieel 1 was, dus wegens de progress-eigenschap voor semaforen zal een thread de P-operatie voltooien. Die thread wordt vervolgens kritiek. Starvation kan in programma 3.1 alleen ontstaan als de gebruikte semafoor toestaat dat een thread willekeurig vaak wordt gepasseerd. Anders gezegd, no starvation voor de semafoor impliceert no starvation voor programma 3.1. De eigenschappen van het programma hangen dus af van de semafoor-eigenschappen, en de programmeur moet zich dus terdege in de specificatie van het gebruikte primitief verdiepen. In programma 3.1 wordt de semafoor steeds opgehoogd door de thread die hem eerder verlaagd heeft. Dit is een veelvoorkomend patroon, maar in het algemeen is dit bij semaforen niet noodzakelijk; programma 3.6 is een voorbeeld waar het verlagen en verhogen van een semafoor juist steeds in verschillende threads gebeurt. Soms biedt een systeem een variant van de semafoor aan waarin dit juist wel altijd het geval is: een lock in een database omgeving bijvoorbeeld kan alleen worden vrijgegeven door de thread (transactie) die hem in bezit heeft. Ook het mutant primitief onder Windows kent deze koppeling. Het lijkt of het primitief daarmee minder bruikbaar wordt, en dat is ook zo: je kunt er minder ingewikkelde dingen mee uithalen. Daartegenover staan de voordelen dat je er ook minder snel fouten mee kunt maken, en dat de implementatie eenvoudiger kan zijn. 3.3 Semaforen voor systeembouwers In deze sectie wordt de werking van een semafoor bekeken. Omdat het besturingssysteem beslist over het schedulen van threads, is vanuit het besturingssysteem een veel efficiëntere manier van wachten mogelijk dan de busy wait. Het besturingssysteem blokkeert een thread door te zorgen dat die niet wordt gescheduled; dit kan eenvoudig worden verwerkt in de administratie van threads. De thread kan weer door als het besturingssysteem oordeelt dat de condities gunstig zijn; de thread wordt weer bij de actieve threads geplaatst. We noemen dit blocked wait. Een mogelijke werking van de semafoor-operaties wordt in programma 3.2 beschreven; met semafoor s is een pool s.pool van threads geassocieerd die op s wachten. De P operatie plaatst de thread in deze pool als de waarde van s gelijk aan 0 is. De V verhoogt s, en haalt vervolgens wachtende threads uit deze pool. Het verplaatsen van threads naar en van de wachtrij noemen we wait en notify (over de semantiek hiervan in Java gaat kader 3.9). In de P operatie test

47 3.3 Semaforen voor systeembouwers 37 s.p: while ( s == 0 ) { plaats deze thread in s.pool } // "wait" s = s-1 s.v: s = s+1 if ( s.pool bevat een thread ) { activeer een thread uit s.pool } // "notify" Programma 3.2: Werking van de P en V operatie. een thread die uit de wacht wordt gehaald opnieuw of s positief is. Dit is met oog op de mogelijkheid dat de notify meerdere threads heractiveert zodat er tussen de uitvoering ervan en het doorgaan van een specifieke thread andere acties kunnen plaatsvinden. Natuurlijk zal de werking ook juist moeten zijn als meerdere threads tegelijkertijd met een P- of V-operatie bezig zijn. De code voor de operaties is daarom kritiek, en het lijkt of we voor de implementatie terug zijn bij af. Echter, het maakt een belangrijk verschil of de synchronisatie binnen een applicatie, dan wel in het operating system geregeld wordt. Software implementatie. Het is mogelijk de oplossingen uit hoofdstuk 2 in te bouwen in het operating system. Het lijkt erop dat je dan niet veel opschiet met de introductie van semaforen, maar toch zijn er twee voordelen wanneer de code uit het zicht van de programmeur wordt gehaald. (1) Minder kans op fouten doordat het werk voor de programmeur overzichtelijker is geworden. (2) Programma s zijn goed te porten naar een systeem dat over een andere, wellicht betere implementatie van semaforen beschikt. Interrupt disable. De interrupt mogelijkheid van de processor wordt geblokkeerd, hiermee weten we zeker dat de thread niet op vervelende plaatsen onderbroken wordt. Zoiets doe je alleen in besturingssysteem-code, applicaties mogen dit niet doen omdat ze dan volledige macht over de processor krijgen. Als het programma dan crasht blokkeert de hele installatie omdat het besturingssysteem niet meer de mogelijkheid heeft het programma te termineren of te onderbreken. Speciale machine-instructie. In de jaren vijftig moest men het doen met de instructies die toevallig op machines zaten, maar later wilden hardwarefabrikanten wel eens een instructie speciaal voor concurrency toevoegen. Een voorbeeld is de test-and-set-instructie Var x: Test-and-Set register operatie P: while ( x.tas == 1 ) { } operatie V: x = 0 Programma 3.3: Binaire semafoor met test-and-set.

48 38 3 Synchronisatieprimitieven Kader 3.4: Synchronisatie van verkeersstromen De begrippen mutual exclusion, progress en starvation zijn ook toe te passen op het regelen van verkeersstromen op een kruispunt. De veiligheid (safety) van verkeersdeelnemers vereist dat een weggedeelte door ten hoogste één voertuig tegelijk bereden wordt, en het kruisingsvlak kan daarom als een gedeelde resource of kritieke sectie worden beschouwd. De doorstroming van het verkeer vereist dat alle voertuigen ooit door kunnen rijden. Een verkeersregelsysteem dat niet aan de eis van mutual exclusion voldoet geeft aanleiding tot direct zichtbare schade (botsing). De botsing resulteert, wanneer meerdere voertuigen het kruisingsvlak tegelijk gebruiken. Als op een gelijkwaardige kruising van vier kanten een voertuig nadert, moet elk voertuig voorrang verlenen aan het (voor hem) van rechts komende voertuig. Geen voertuig kan doorrijden, terwijl de kruising vrij (beschikbaar) is; er is sprake van deadlock, ofwel een schending van progress. In deze situatie wordt de resource gedurende een willekeurig lange periode in het geheel niet gebruikt. Als op een voorrangsweg een eindeloze stroom voertuigen rijdt wordt de kruising steeds weer gebruikt; er is dus progress, maar een voertuig uit een zijweg kan willekeurig lang blijven wachten. De situatie waarin een deelnemer het kruisingsvlak oneindig vaak aan zijn neus voorbij ziet gaan ten gunste van steeds nieuw arriverende concurrenten wordt door no starvation uitgesloten. die atomair de waarde van een binaire variabele oplevert en de waarde 1 erin schrijft. Na x.tas is de waarde van x dus altijd 1. Programma 3.3 implementeert een binaire semafoor met busy wait met behulp van een test-and-set-instructie op x. Deze test-andset maakte vanaf 1964 deel uit van IBM s System/360 architectuur; later werd de nog krachtiger compare-and-swap-instructie toegevoegd (zie Kaders 1.6 en 10.1). Na het slagen van een P operatie op een semafoor is de waarde daarvan altijd 0; daarom wordt de semafoor geïmplementeerd in een test-and-set met de waarden 0 en 1 omgewisseld: de waarde van x is 0 als de waarde van de semafoor 1 is en omgekeerd. De opgeleverde waarde van x is 0 als de waarde van de semafoor voor het schrijven 1 was, en dan eindigt de P operatie. Monitor. Java heeft geen semaforen ingebouwd, maar kent wel de monitor, waarmee code gemakkelijk te beschermen is tegen concurrente executie. Je kunt een semafoor implementeren door de code van programma 3.2 in een monitor te stoppen.

49 3.4 Toepassing in de begrensde buffer 39 int head = 0, tail = 0... cobegin // Thread 1 is producer // Thread 2 is consumer produceer B... A[tail] = B C = A[head] tail = (tail+1) mod n head = (head+1) mod n... verwerk C coend Programma 3.5: Communicatie via begrensde buffer. 3.4 Toepassing in de begrensde buffer Om te laten zien dat je met semaforen behoorlijk ingewikkelde dingen bevredigend kunt oplossen bekijken we een klassiek probleem uit de concurrente programmering, namelijk het producer-consumer, ofwel de begrensde buffer. Thread 1 in programma 3.5 maakt objecten aan die door thread 2 worden verwerkt; de overdracht gebeurt via een buffer met begrensde capaciteit. Deze buffer is in feite de abstracte datastructuur queue, maar geïmplementeerd in een array. Er blijkt tussen de twee threads synchronisatie nodig te zijn. Immers, als de producer sneller werkt dan de consumer kan ongelezen data in de buffer worden overschreven door nieuwe data. Andersom, als de consumer sneller werkt dan de producer kan oude data opnieuw worden gelezen of er wordt gelezen in een blanco stuk van de buffer. Schrijf Enq i voor de i-de schrijfactie en Deq i voor de i-de leesactie; dan bedoelen we met Enq i Deq i (ofwel: Enq i gaat-vooraf-aan Deq i ) dat de i-de schrijfactie geheel moet zijn afgerond voordat de i-de leesactie begint. Er mag dus geen sprake zijn van overlap!! Omdat de (n + i)-de data in dezelfde locatie komt als de i-de, moet gelden Deq i Enq n+i. De gaat-vooraf-aan relatie modelleert de geëiste synchronisatie, die nu met behulp van semaforen gemakkelijk kan worden geïmplementeerd. We zien nu een vrij natuurlijke situatie waar de P en V op een semafoor door verschillende threads worden uitgevoerd. Wordt een semafoor met 0 geïnitialiseerd, dan geldt steeds (semafoor): Het aantal voltooide P s is ten hoogste het aantal begonnen V s. De eerste restrictie implementeren we met een semafoor lees met initiële waarde 0; de consumer moet een pak doen voor het lezen, en na het schrijven doet de producer een vrij. Voor Deq i begint heeft de consumer i P s voltooid, er zijn dus ook i V s begonnen en dat doet de producer pas na Enq i. Wordt een semafoor met k geïnitialiseerd, dan geldt steeds:

50 40 3 Synchronisatieprimitieven int head = 0, tail = 0 Semafoor lees = new Semafoor(0) Semafoor schrijf = new Semafoor(n) cobegin // Thread 1 is producer // Thread 2 is consumer produceer B... schrijf.p lees.p A[tail] = B C = A[head] tail = (tail+1) mod n head = (head+1) mod n lees.v schrijf.v... verwerk C coend Programma 3.6: Begrensde buffer met semaforen. Het aantal voltooide P s is ten hoogste het aantal begonnen V s plus k. De tweede restrictie implementeren we met een semafoor schrijf met initiële waarde n; de producer moet een pak doen voor het schrijven, en na het lezen doet de consumer een vrij. Voor Enq n+i begint heeft de producer i + n P s voltooid, er zijn dus ten minste i V s begonnen en dat doet de consumer pas na Deq i. De besproken oplossing met semaforen is te zien in programma Monitors Alles wat voor ons vanzelfsprekend is, was ooit nieuw. Rond 1970 werd het concept van separation of concerns zichtbaar in programmeertalen en programmeeronderwijs. Een abstracte datastructuur moest worden geïmplementeerd als typedefinities en bijbehorende procedures, en o wee de student die in zijn hoofdprogramma de structuur anders gebruikte dan via deze procedures (bijvoorbeeld door de tail van een queue te lezen). Gebruik en implementatie van de structuur zijn van elkaar gescheiden door de specificatie. Rond 1970 sprak men in dit verband van structured programming, wat niet alleen het werken met abstracte datastructuren inhield maar ook het afzweren van bepaalde opdrachten (goto instructies met name) waarvoor geen bewijsregels bestonden. Toepassing van deze principes was echter verantwoordelijkheid van de programmeur. Deze manier van denken werd concreet in object-georiënteerde talen, waar data en methoden zijn samengebracht in een klasse (waarbij wordt aangegeven welke variabelen en methoden public zijn), en de goto is uitgebannen. We kunnen daarom wel zeggen dat Tony Hoare in 1974 zijn tijd vooruit was met de introductie van de monitor als basis voor concurrent programmeren [Hoa74]. De monitor specificeert (net als de moderne class) data en operaties op die data (waaronder initialisatie), en welke operaties van buiten aangeroepen kunnen worden.

51 3.5 Monitors 41 monitor counter export inc, dec int c procedure inc { c = c+1 } // Geef aan wat public is // archaisch voor methode procedure dec { c = c-1 } begin c = 0 end // initialisatie // Hier eindigt de monitorbeschrijving cobegin counter.inc counter.dec counter.inc counter.inc... counter.inc counter.dec... coend Programma 3.7: Counter bijhouden in een monitor. Het nieuwe van de monitor was de garantie dat ten hoogste één thread tegelijk code van de monitor kan uitvoeren: de monitorcode wordt dus automatisch gesynchroniseerd. Ook als er meerdere operaties (procedures) zijn gedefiniëerd, kan er slechts één thread tegelijk actief zijn in deze procedures samen. Dat wil zeggen, de toegang tot de code is exclusief, maar de toegang wordt geregeld door de compiler en het operating system, zonder dat de programmeur zich daar druk over hoeft te maken. Wanneer een thread een monitorprocedure aanroept terwijl een andere thread in de monitor bezig is, wordt eerstgenoemde in een wachtrij geplaatst tot de andere thread klaar is (danwel expliciet toegang verleent zoals te zien is in sectie 3.6). Programma 3.7 geeft een mooi voorbeeld: het bijhouden van een counter die in verschillende threads kan worden verlaagd of verhoogd. De monitor ziet er vertrouwd uit als een klasse met methoden inc en dec. Bij het gebruik van deze monitorprocedures, dus in de diverse threads die de operaties gebruiken, hoeft verder geen synchronisatiecode meer te worden opgenomen: alle synchronisatie wordt in de monitor door de programmeur gespecificeerd. Omdat elke programmeertaal weer anders is gaat het hier niet om de precieze syntax maar om de ingrediënten van de monitor en hoe die samenkomen in het programma. De monitor-code staat tussen monitor en end. Achter export staan de procedures die van buiten kunnen worden aangeroepen; daarnaast kunnen er locale procedures zijn. Na begin komt de initialisatie van de monitor. Hoares monitors maken niet het onderscheid dat we nu kennen tussen klasse en object: de monitorcode beschrijft datatype en operaties en maakt meteen één instantie van dat datatype aan. Zoals we met object-georiënteerde software gewend zijn is er een mooie scheiding tussen de implementatie van het counter object en het gebruik ervan. Een grote verbetering ten opzichte

52 42 3 Synchronisatieprimitieven van de entry/exit van hoofdstuk 2 en de semaforen van sectie 3.1 is dat de synchronisatie geregeld is bij de implementatie, en dat een programmeur die de monitor gebruikt er dus niets fout mee kan doen. (Iedere nieuwe feature van programmeertalen kwam met het argument dat de programma s voortaan automatisch goed zouden zijn. Hm, is het de lezer al opgevallen dat er vanaf 1974 geen incorrecte programmatuur meer is geschreven?) 3.6 Begrensde buffer met monitors Dat we nog iets aan de monitor-constructies moeten toevoegen merken we wanneer we het producer-consumerprobleem met monitors willen programmeren. De inc en dec operaties van een counter moeten weliswaar niet-overlappend worden uitgevoerd om te voorkomen dat updates verloren gaan, maar de operaties zijn niet voor hun voortgang van elkaar afhankelijk. Met bufferoperaties ligt dit anders omdat de uitvoering van een schrijfoperatie soms moet wachten tot een leesoperatie is voltooid (en omgekeerd). Het schrijven in en lezen uit de buffer worden natuurlijk monitorprocedures enque en deque. Maar wat gebeurt er als de consumer wil lezen en de buffer is leeg? De consumer zal moeten wachten tot de producer weer iets gemaakt en geschreven heeft, maar zolang de consumer in de monitor zit kan de producer dat helemaal niet doen. Daarom kan een thread in de monitor blijven maar toch de controle over de monitor aan een andere thread geven, door een wait instructie. Een andere thread kan dan in de monitor (bijvoorbeeld de producer om iets te schrijven) maar om ook dit andere proces exclusieve toegang te garanderen kan de eerdere thread pas weer verder als de tweede thread een notify uitvoert. Er is dus sprake van interleaving van instructies van verschillende threads, maar het wisselen tussen threads kan alleen gebeuren door wait en notify instructies. De wait en notify operaties van verschillende threads worden in het algemeen aan elkaar gekoppeld via condities, dit zijn namen voor wachtrijen. Let erop dat de hier gebruikte terminologie hier bepaald misleidend is, zowel conditie als wait. Condities in een monitor zijn geen condities in logische zin, die waar of onwaar kunnen zijn, maar namen voor een wachtrij. De wait instrueert het besturingssysteem om de aanroepende thread niet langer mee te laten doen in de verdeling van processortijd, maar hem in een wachtrij te plaatsen en een andere thread tot de monitor toe te laten. De constructie while (count==n) { wait(vol) } betekent dus niet, zoals men misschien zou verwachten: houd in de gaten of het predicaat count==n (aangevend dat de buffer vol is) waar is en ga verder zodra dit niet langer het geval is. Dat zou een busy wait zijn, adequater omschreven als while (count==n) { }. De conditie vol in het programma is helemaal geen conditie of predicaat, maar de naam van een wachtrij en wait(vol) betekent ga in de wachtrij vol staan. De thread is dan geblokkeerd, waardoor ook duidelijk wordt waarom er een actie van een andere thread nodig is om weer verder te kunnen. Doornroosje kan zichzelf niet wakker kussen omdat ze slaapt! De enque en deque procedures bevatten (op 1 regel) de algoritmen voor lezen en schrijven in de buffer. Bij het schrijven moet, als de buffer vol is (voor het gemak is de variabele count ingevoerd om dit gemakkelijk te kunnen testen) de schrijver wachten. Hij plaats zichzelf in een wachtrij met naam vol, en hoewel de schrijvende thread in de monitor zit kan er nu een andere thread in. Het wachten houdt niet vanzelf op als aan de conditie count==n niet langer is voldaan: er is een expliciete actie nodig van een andere thread, namelijk de notify.

53 3.6 Begrensde buffer met monitors 43 monitor buffer export enque, deque int A[n], tail, head, count condition leeg, vol procedure enque(b) { while (count==n) { wait(vol) } A[tail] = B ; tail = (tail+1) mod n count++ ; notify(leeg) } procedure deque(var C) { while (count==0) { wait(leeg) } C = A[head] ; head = (head+1) mod n count-- ; notify(vol) } begin tail = 0 ; head = 0 ; count = 0 end cobegin // Producer // Consumer maak B buffer.deque(c) buffer.enque(b) verwerk C coend Programma 3.8: Begrensde buffer met monitor. Deze bijpassende notify is opgenomen in de deque actie: een lezer doet notify(vol) en daarmee wordt indien mogelijk een thread uit de vol wachtrij geactiveerd. (Het wachten bij een lege buffer is identiek geregeld, je ziet dat een lezer zich deactiveert bij count==0 en dat de schrijver aan het eind van de enque ingeslapen lezers wakker kust; de prins vindt Doornroosje via de conditie leeg.) Waarom is de wait instructie bevat in een lus? Programmeertalen verschillen in de semantiek van notify. Bekijk de situatie dat er meerdere threads willen schrijven en allemaal wachten op conditie vol; een lezer verwijdert een item en doet notify(vol). Wat er precies gebeurt verschilt per programmeertaal, maar in sommige gevallen doet de notify alle threads in de betreffende wachtrij ontwaken (en die mogen dan één voor één de monitor in). De eerste kan in de buffer schrijven, maar maakt hem weer vol vòòr de tweede, die dus na het ontwaken weer een volle buffer aantreft en opnieuw gaat wachten op vol. De semantiek van notify kent ook weer verschillende varianten ingeval de notify niet, zoals in programma 3.8 (en in de meeste programma s), de laatste van een procedure is. Stel

54 44 3 Synchronisatieprimitieven thread P doet thread Q ontwaken door een notify, maar heeft zelf nog enkele instructies te gaan. Threads P en Q mogen natuurlijk niet tegelijk actief zijn, en verschillende opties zijn mogelijk: Notify-and-continue. P mag doorgaan in de monitor, en Q komt er pas in als P de monitor verlaat. Notify-and-exit. We verbieden dat er nog instructies na de notify komen, of, anders gezegd, een notify geldt meteen als exit. Notify-and-wait. Bij de notify wordt P zelf geblokkeerd en komt de monitor pas weer in als Q die verlaten heeft (danwel, een nieuwe wait tegenkomt). De urgent wait variant zorgt er voor dat P wel prioriteit krijgt boven alle andere threads die op de monitor wachten. Al met al is de semantiek van een monitor-constructie een knap ingewikkeld te doorgronden geheel. Door de wait en notify kunnen er immers toch verschillende threads tegelijk in de monitor zitten, al is er maar één tegelijk echt actief. Andere threads wachten om binnen te komen of zijn al binnen maar hebben een wait of notify gedaan. Maar: alleen door het uitvoeren van wait of notify instructies of bij het beëindigen van een procedure kan executie overgaan van één thread naar een andere. Hierdoor is het werken met monitors overzichtelijker dan met onbeschermde code die slechts read-write atomicity biedt. Een thread kan (1) geheel buiten monitor procedures zijn, (2) wachtend op toegang aan het begin van een procedure, (3) geblokkeerd door een wait(c) (of notify!), of (4) actief. Voor threads die wachten op toegang (2) is nodig dat de monitor is vrijgegeven, dus dat er geen thread actief is. Voor threads die in een wait(c) geblokkeerd zijn is een overeenkomstige notify(c) nodig. (Voor threads die wachten in een notify is nodig dat de monitor vrij is, of, ingeval een Urgent wait, in ieder geval is vrijgegeven door de thread die door deze thread is toegelaten.) Voor de monitor kunnen specificaties worden geformuleerd overeenkomstig met die voor entry- en exitcode en semaforen. Toegang: Ten hoogste één thread is in de monitor actief (4). Progress: Als er threads wachten voor de monitor en voor een of meer van hen is de voorwaarde voor toegang vervuld, dan wordt er een thread actief. No starvation: Als een thread wacht op de monitor en de voorwaarde voor zijn toelating wordt willekeurig vaak vervuld, dan wordt die thread actief. Samenvatting en conclusies Semaforen maken het de programmeur een stuk gemakkelijker zoals mag blijken uit de oplossing voor begrensde-buffercommunicatie. In plaats van moeizaam werken met entry- en exitcode kunnen de synchronisatie-eisen op abstact niveau (Enq i Deq i ) worden bekeken en omgezet naar semafoorinstructies. Er blijft echter een grote verantwoordelijkheid voor de programmeur. Die moet er zelf voor zorgen dat alle kritieke plaatsen in de code door P en V zijn beschermd, en ook dat er voldoende V-operaties staan om de applicatie op gang te houden. Berucht zijn situaties waarin een V-operatie wordt vergeten en de gehele applicatie

55 Samenvatting en conclusies 45 Kader 3.9: Monitors in Java. Een Java-object is niet automatisch een monitor. Daarvoor moeten één of meer van zijn methoden zijn gedefiniëerd met het keyword synchronized. De zo aangemerkte methoden vormen samen een monitor. Als er meerdere objecten van dezelfde (als monitor gedefiniëerde) klasse zijn, dan kunnen threads wel gelijktijdig actief zijn in verschillende objecten. Java kent geen verdeling van wachtende threads over meerdere benoemde wachtrijen (de condities). Per monitor is er één wachtrij van threads. De wait instuctie is wait() en deze moet in een try sectie staan en de exception InterruptedException moet worden afgevangen. Het opwekken van wachtende threads kan met twee instructies: de instructie notify() haalt één thread (de langst wachtende) uit de wachtrij en maakt hem weer executeerbaar. Deze thread kan pas echt actief worden als de thread die notify() heeft aangeroepen de monitor vrijgeeft (Notify-and-continue). De instructie notifyall() maakt alle threads uit de wachtrij van deze monitor weer executeerbaar. Werk je met notify, dan moet je ervoor zorgen dat er tegenover elke wait een notify staat, want elke notify activeert ten hoogste één thread. Met te weinig notify s blokkeert na enige tijd je gehele applicatie omdat alle threads in de monitors wachten. Bij de notifyall moet je er een gewoonte van maken om een test en wait (zoals in programma 3.2 of 3.8) in een lus te zetten. na enige tijd blokkeert: de applicatie lijdt aan deadlock. (Iets subtieler zijn situaties waarin de applicatie wel draait, maar concurrency niet optimaal wordt gebruikt.) Vergeet men een P, dan kunnen threads te vrijelijk door en een schending van safety eigenschappen is doorgaans het resultaat. Zeker als P- en V-operaties vanuit verschillende, en meerdere, threads worden aangeroepen is het uitbalanceren ervan een delicate kwestie. De monitors zijn een flinke stap in de goede richting. Geheel in de geest van objectoriëntatie is er een strikte scheiding tussen implementatie en gebruik van datastructuren, en synchronisatie wordt geregeld waar het thuishoort, namelijk bij de implementatie van de operaties. Het is echter met monitors heel gemakkelijk om de synchronisatie te streng te maken zodat de threads onnodig worden geblokkeerd en de applicatie wordt afgeremd; zie hoofdstuk 4. Monitors werden in verschillende programmeertalen ingebouwd, bijvoorbeeld Concurrent Pascal, Pascal Plus, Mesa, Turing Plus, en ook in Java [DD98]. Maar ook al wordt het wachten slim geregeld en wordt het bijna onzichtbaar, ook in een monitor wordt bescherming van code verkregen door threads op elkaar te laten wachten met alle bijbehorende nadelen (hier weer even opgesomd). Verlies van concurrency (dus snelheid);

56 46 3 Synchronisatieprimitieven Mogelijkheid tot deadlock; Oneindige blokkering als een thread crasht. Opgaven bij hoofdstuk 3 Opgave 3.1 Leidt programma 3.1 tot starvation als de semaforen werken volgens programma 3.2? Opgave 3.2 Bewijs dat programma 3.1 voldoet aan no starvation als deze eis geldt voor de gebruikte semafoor. Opgave 3.3 Semaforen. (Uit het tentamen van augustus 2000.) Twee threads, een producer en een consumer, delen een initiëel lege buffer waarin de producer objecten kan toevoegen en waaruit de consumer objecten kan verwijderen. De buffer heeft een begrensde capaciteit k. Er geldt de eis: zodra de buffer eenmaal objecten bevat, moet er altijd tenminste één object in blijven. (a) Geef nu eerst code die het lezen en schrijven in de buffer beschrijft en voeg daar instructies voor een semafoor S1 aan toe die voorkomen dat de buffer overloopt. (b) Voeg nu instructies voor een semafoor S2 toe die zorgen dat aan bovengenoemde eis wordt voldaan. Opgave 3.4 Onder begrensde concurrency (of, in het Engels, k-mutual Exclusion) verstaan we de eis dat er hoogstens een vast aantal (k) threads tegelijk actief mogen zijn in een bepaalde procedure. Laat zien hoe je begrensde concurrency kunt implementeren met een semafoor. Opgave 3.5 Wat is de semantiek van een V operatie in programma 3.3 als de waarde van de semafoor al 1 is? Opgave 3.6 Semafoor en monitor. (Uit het tentamen van februari 2002.) (a) Welke operaties zijn er op een semafoor en wat is hun semantiek? (b) Laat zien hoe je een semafoor kunt implementeren als een monitor. Opgave 3.7 Semaforen. (Uit het tentamen van mei 2001.) (a) Wat doen de P en V operaties van een semafoor? (b) Laat zien hoe je met een semafoor een monitor kunt implementeren als daar geen notify/wait instructies in voorkomen. (c) Hoe zou je de implementatie kunnen uitbreiden om ook notify/wait instructies te implementeren? Opgave 3.8 Veel sequentiële programma s die een queue gebruiken (bijvoorbeeld breadth-first search) gebruiken het leeg zijn van de queue als terminatieconditie. Kan de consumer in programma 3.5 termineren als de buffer leeg is? Hoe kun je een producer-consumer applicatie laten stoppen? Opgave 3.9 Semaforen. (Uit het tentamen van mei 2002.) Processen p en q voeren elk een lus uit, waarbij de eis geldt dat de rangnummers van hun iteraties niet meer dan 3 uiteen liggen. Dus, als p in iteratie i bezig is en q in j, dan moet gelden i 3 j i + 3. Implementeer de beschreven afhankelijkheid met semaforen. (Hint: Schrijf Lus(p, i) voor de uitvoering van de i-de iteratie door p, en formuleer de geëiste afhankelijkheid met.)

57 Opgaven bij hoofdstuk 3 47 Opgave 3.10 Je kunt ook een onbegrensde buffer (queue als gelinkte lijst) maken. semafoor uit programma 3.6 kan vervallen? Welke Opgave 3.11 Begrensde buffer met Semaforen. (Uit het tentamen van januari 2006.) In deze opgave delen een producer en een consumer een begrensde buffer, waar de producer items aan kan toevoegen en de consumer uit kan verwijderen. De taak van de consumer vereist echter, dat hij steeds twee items achter elkaar leest en samen verwerkt. (a) Schrijf pseudocode voor de producer en consumer, waarbij de buffer met semaforen wordt beschermd tegen over- en underflow. (b) Kan je oplossing het aan als er meerdere consumers actief zijn? Geef aan wat eventueel aangepast moet worden. Opgave 3.12 Producer-Consumer met binaire semaforen. (Uit het tentamen van september 2003.) Karin moet een begrensde buffer met n plaatsen implementeren in een systeem waar alleen binaire semaforen beschikbaar zijn. Ze heeft per bufferplaats A[i] een semafoor x[i] gedefinieerd met beginwaarde 0 en is met haar programma zo ver: Prod:... Cons:... produceer B x[h].p A[t] = B C = A[h] x[t].v h = (h+1) mod n t = (t+1) mod n verwerk C (a) Bewijs dat, als Enq i de i de schijfactie is en Deq i de i de leesactie, in bovenstaand programma geldt Enq i Deq i. (b) Maak het programma en het correctheidsargument af. (c) Wat is je mening over de efficiëntie in vergelijking met de bufferimplementatie met meerwaardige semaforen? Opgave 3.13 Wat is in programma 3.6 de maximale waarde die lees kan bereiken? schrijf? En Opgave 3.14 Op een object zijn operaties A en B gedefiniëerd. Er mogen verschillende threads tegelijk een A doen, er mogen verschillende threads tegelijk een B doen, maar een A en een B mogen nooit overlappen. Codeer deze synchronisatie-eis met semaforen of monitors, let erop dat behalve de genoemde (safety) eis ook aan no starvation is voldaan. Opgave 3.15 Op een object zijn operaties A en B gedefiniëerd. Verschillende A operaties mogen nooit overlappen, verschillende B operaties mogen nooit overlappen, maar een A en een B mogen wel overlappen. Codeer deze synchronisatie-eis met semaforen of monitors, let erop dat behalve de genoemde (safety) eis ook aan no starvation is voldaan. Opgave 3.16 Waarom is programma 3.8 minder goed dan programma 3.6 vanuit concurrency oogpunt? Opgave 3.17 De kapper. (Uit het tentamen van september 2004.) Een P-operatie op een semafoor, of een Entry-protocol, laten een thread altijd onvoorwaardelijk wachten tot de gewenste operatie (kritieke sectie) kan worden uitgevoerd. Bij de kapper kijken

58 48 3 Synchronisatieprimitieven klanten eerst naar binnen en besluiten ongeknipt door te lopen als er k of meer wachtenden zijn. Schrijf een monitor kapper met procedures entry (die een bool oplevert) en exit, kan door willekeurig veel threads zo worden gebruikt:... if kapper.entry { Knippen ; kapper.exit }... Je mag aannemen dat Knippen altijd eindigt. (a) Schrijf kapper; aan de volgende eisen moet zijn voldaan. Knip-exclusie: Op elk moment is er hoogstens één thread tegelijk in de instructie Knippen. Knip-doorgang: Een thread die aan kapper.entry begint, maakt deze aanroep ooit af. Knip-afwijzing: Als een thread False krijgt als uitkomst van kapper.entry, waren er op enig moment tijdens de uitvoering van die aanroep minstens k threads in de monitorprocedure entry actief. (b) Aan welke eigenschap moet de monitor-implementatie voldoen opdat aan de volgende eigenschap is voldaan: Begrensdewacht: Gedurende de uitvoering van kapper.entry kunnen er ten hoogste k threads beginnen met Knippen. (c) Wijzig de pseudocode (van de threads en/of de monitor) zo, dat een thread na afloop weet of hij geknipt is of niet. Opgave 3.18 Drie verschillende kritieke secties. (Uit het tentamen van oktober 2007.) Een systeem bestaat uit n threads, die in hun programma operaties A, B en C hebben. De overige code hoeft niet te worden beschermd, omdat uitvoering van die code ongelimiteerd met andere code mag overlappen. Op onvoorspelbare momenten kan een thread echter aan een uitvoering van A, B of C beginnen en hierop is wel een relevante beperking: het is verboden dat alle threads tegelijkertijd in dezelfde operatie bezig zijn. Het synchronisatieprobleem is, threads eventueel te laten wachten voor het binnengaan van de operatie. (a) Formuleer Uitsluiting, Progress en No Starvation eisen voor deze synchronisatie. (b) Implementeer deze synchronisatie; je mag kiezen of je dit met monitors of semaforen aanpakt. (c) Beargumenteer dat je oplossing correct is. Opgave 3.19 Maaltijd met toetje. (Uit het tentamen van oktober 2006.) Beschouw twee filosofen die een operatie Maaltijd hebben; omdat er in Maaltijd gedeelde resources gebruikt worden, mogen de threads niet tegelijk deze operatie uitvoeren. Er zijn twee Ober-threads die een operatie BrengToetje hebben, maar omdat de obers elkaar voor de voeten lopen mogen de obers die operatie niet gelijktijdig uitvoeren. Tenslotte geldt de eis, dat elke Maaltijd met een toetje moet eindigen; er mogen daarom nooit meer Maaltijd operaties zijn beëindigd dan er BrengToetje operaties zijn beëindigd.

59 Opgaven bij hoofdstuk 3 49 Filosoof1: Filosoof2: Ober1: Ober2: Maaltijd Maaltijd BrengToetje BrengToetje Voorzie deze threads van synchronisatiecode om aan deze eisen te voldoen. Je mag zelf weten of je het met semaforen of met monitors doet. Beredeneer of je oplossing voldoet aan de beschreven vormen van uitsluiting, en of hij voldoet aan Progress en No-Starvation. NB: dit is een buffet-stijl maaltijd waarbij de obers toetjes brengen wanneer ze zelf zin hebben, niet op bestelling!

60 Hoofdstuk 4 Dining Philosophers We zagen hoe toegang tot een object (of operatie) geregeld kan worden met mutual-exclusioncode (hoofdstuk 2), semaforen of monitors (hoofdstuk 3). In dit hoofdstuk worden voorbeelden gegeven van programma s die meerdere objecten delen, waarbij de toegang tot die objecten wordt geregeld met de toegangsmethoden die we al hebben gezien. Een eis is dat meerdere processen tegelijk actief kunnen zijn wanneer ze niet dezelfde objecten gebruiken. Het zal daarbij echter blijken, dat problemen die op een lager niveau zijn opgelost (deadlock, starvation, throughput) op een hoger niveau weer opduiken. Als prototype van een applicatie waarin meerdere objecten moeten worden beheerd geldt het probleem van de dining philosophers. De probleemstelling is echter heel algemeen zoals in sectie zal worden belicht. 4.1 Een eenvoudige doch voedzame maaltijd Het probleem van de dinerende filosofen is een abstractie van conflicten die in een gedistribueerd systeem kunnen optreden. Het werd rond 1965 geformuleerd door Dijkstra Probleemomschrijving Een gezelschap van (in voorbeelden meestal vijf, in het algemeen n) filosofen houdt een feestje, waarbij de filosofen rond een tafel zitten; zie figuur 4.1. Gesproken wordt er daarbij niet, daar filosofen dit beneden hun waardigheid achten; zij denken slechts. Filosofen krijgen soms honger; omdat het feest is staat er midden op tafel een schaal spaghetti en een filosoof die honger krijgt mag daarvan eten en daarna weer verder denken. Hierbij is echter coördinatie nodig omdat een filosoof bij het eten de twee vorken, links en rechts van hem, gebruikt. Tussen elke twee borden ligt maar één vork, zodat er nooit twee filosofen naast elkaar kunnen eten. Het kan dus voor een hongerige filosoof nodig zijn te wachten voordat hij met eten kan beginnen. Tijdens de afsluiting maakt de filosoof de vorken weer beschikbaar voor andere filosofen Eisen aan de oplossing De honger- en afsluitfase zijn te vergelijken met het entry- en exitprotocol uit hoofdstuk 2. Het correct gebruik van de vorken en het wachten op een vork leiden tot eisen die we aan de oplossing stellen, vergelijkbaar met de eisen die aan de andere synchronisatiemethoden werden gesteld. 50

61 4.2 Maaltijd met hindernissen 51 Honger Denken Eten Afsluiten Figuur 4.1: Opstelling en gedrag van filosofen. Buuruitsluiting: Nooit eten twee filosofen die naast elkaar zitten tegelijk. Progress: Als er hongerige filosofen zijn waarvan de buren niet eten, kan er minstens een gaan eten. Het mag hiervoor niet nodig zijn dat eerst een eventueel etende filosoof afsluit. No starvation: Een hongerige filosoof kan uiteindelijk eten. Aan no starvation kan niet worden voldaan als een filosoof oneindig lang eet; deze houdt dan namelijk zijn vorken oneindig lang geblokkeerd. De aanname dat eten steeds na eindige tijd is afgelopen en dat de betreffende filosoof ook de afsluitfase voltooit, moet dus steeds worden gemaakt. We kunnen ons hierbij beroepen op fairness (omdat er tijdens het eten en afsluiten nooit wordt gewacht) maar helemaal bevredigend is dit niet. In een gedistribueerde applicatie kan immers een proces crashen terwijl het resources vasthoudt. Tenslotte wordt geëist dat alleen hongerige, etende of afsluitende filosofen zich met de synchronisatie bezighouden. Er mag dus geen beroep worden gedaan op de hulp van denkende filosofen. Deze eis noemen we beperkte deelname Toepassingen Er zijn erg veel gedistribueerde programma s waarin de threads meerdere dingen met elkaar moeten delen. Het kan hierbij gaan om geheugenruimte of specifieke locaties, zoals in databasetransacties. 4.2 Maaltijd met hindernissen Om de betekenis van de diverse eisen, en de moeilijkheden die het ontwerp van een oplossing met zich meebrengt, te illustreren, worden nu enkele programma s gepresenteerd die niet aan alle eisen voldoen. De filosofen zijn steeds gemodelleeerd als threads 0 tm n 1, en de vorken zijn objecten genummerd 0 tm n 1. Hierbij heeft filosoof i de vorken i en i + 1 nodig om te eten (de mod n in de optelling laten we impliciet).

62 52 4 Dining Philosophers vork[n] : Semafoor initieel 1 Filosoof i:... // Denk vork[i].p ; vork[i+1].p // Honger Eet // Eet vork[i].v ; vork[i+1].v // Afsluiten... // Denk Programma 4.2: Vorken pakken en vrijgeven. Waar in de oplossingen gebruik wordt gemaakt van eerdere primitieven (semaforen en monitors) nemen we aan dat die aan alle daarvoor geformuleerde eisen (semafoor, progress, no starvation) voldoen Geen synchronisatie: vorkenconflict Een oplossing die aan bijna alle eisen voldoet is meestal even waardeloos als geen oplossing. Veronderstel namelijk eerst dat we elke hongerige filosoof direct toestaan te gaan eten; aan progress, no starvation en beperkte deelname is voldaan! Helaas is er geen buuruitsluiting, waardoor er twee buren tegelijk kunnen eten en met dezelfde vork geeft dat natuurlijk geknoei Synchronisatie per vork De vorken zijn objecten die maar door een filosoof tegelijk gebruikt kunnen worden en dat kan met een al bekende synchronisatiemethode geregeld worden. Er kan bijvoorbeeld per vork een semafoor (initieel 1) worden gebruikt (programma 4.2), maar het idee kan ook met entry- en exitcode of een monitor worden gerealiseerd. Het gebruik van semaforen sluit uit dat ooit twee filosofen tegelijk met dezelfde vork eten; aan buuruitsluiting is voldaan. Uiteraard kan een filosoof wachten op een vork en helaas bestaat de mogelijkheid dat na enige tijd iedereen zit te wachten op een vork, en niet van plan is een vork neer te leggen! De situatie waarin niemand verder kan omdat iedereen wacht heet een deadlock. Het feest zal minder vrolijk eindigen dan het begonnen is Globale synchronisatie In plaats van synchronisatie per vork, kan ook synchronisatie van de eet-actie als geheel worden toegepast, bijvoorbeeld met één enkele semafoor die de gehele pan spaghetti beschermt. Om te eten pakt de filosoof deze semafoor, en heeft dan toegang tot de vorken en het eten. Hetzelfde effect krijgt men door van de spaghetti een monitor te maken, zoals in programma 4.3. Dit programma illustreert weer eens het gemak van monitoren. De honger- en afsluitfasen zijn impliciet in het monitorgedrag, want de monitor dwingt hongerige filosofen te wachten tot de monitor beschikbaar is. Er eet slechts een filosoof tegelijk, waarmee meteen buuruitsluiting is gegarandeerd. Maar programma 4.3 illustreert ook hoe gemakkelijk het is om met een monitor alle concurrency te reduceren tot sequentiële activiteit. Er kan bij de monitor oplossing altijd hoogstens één filosoof tegelijk eten, en elke andere filosoof moet wachten, ook al zijn de beide benodigde

63 4.2 Maaltijd met hindernissen 53 monitor spaghetti export wileten procedure wileten(i) { Eet } begin end Filosoof i:... // Denkt spaghetti.wileten(i) // Honger, eten en afsluiting... // Denkt Programma 4.3: De spaghetti als monitor. vorken vrij. Merk op dat het voor progress is vereist dat een filosoof waarvan de vorken beschikbaar zijn, kan gaan eten terwijl een ander nog eet. Daarom voldoet het programma niet aan progress Globale synchronisatie met token In de volgende oplossing geven de filosofen elkaar een fiche (in het Engels: token) door; een hongerige filosoof mag eten als hij het stokje heeft. Het token blijft rondgaan, ook als er niemand honger heeft, voor het geval er iemand hongerig wordt; daarom heeft de hoofdlus van programma 4.4 terminatieconditie true. In programma 4.4 is het token geïmplementeerd als een integer en een filosoof wacht erop met een busy wait. Het programma illustreert zo, dat er een oplossing mogelijk is onder readwrite atomicity. Het zou natuurlijk veel efficiënter zijn, de tokendoorgifte te implementeren met semaforen. Per tweetal filosofen is er dan een semafoor, waarop de ene een P doet, en de andere geeft het token door met een V. Het programma illustreert dan, dat een oplossing aan meer eisen kan voldoen dan de onderliggende primitieven. Want de oplossing met een token voldoet aan no starvation, zelfs als de gebruikte semaforen die eigenschap niet hebben. Programma 4.4 is een erg magere oplossing. Aan de progress-eis is niet voldaan; een int token init 0 Filosoof i: while (true) { while (token!= i) { } // Busy wait for token if (filosoof heeft honger) eet token = (token + 1) mod n } Programma 4.4: Doorgeven van een token

64 54 4 Dining Philosophers monitor vorken export haal, breng int vork[n] procedure haal(i) { while (vork[i]!= -1 or vork[i+1]!= -1) { wait } vork[i] = i ; vork[i+1] = i } procedure breng(i) { vork[i] = -1 ; vork[i+1] = -1 notifyall } begin for (i=0 ; i<n ; i++) { vork[i] = -1 } end Filosoof i:... // Denken vorken.haal(i) // Honger Eet vorken.breng(i) // Afsluiten... // Denken Programma 4.5: Een monitor voor toegangscontrole. etende filosoof houdt het token vast, zodat geen enkele andere filosoof tegelijk kan eten. Ook is niet voldaan aan beperkte deelname; ook denkende filosofen moeten het token doorgeven, en daarom vereist het programma een aparte thread per filosoof Veel eten en toch hongerige gasten Een monitor kan meestal beter worden gebruikt als in programma 4.5: de monitor regelt hier alleen de toegang tot het eten door het beheer van de vorken. Elke vork wordt gerepresenteerd door een integer, met waarde 1 als hij vrij is. De monitorprocedure haal(i) kijkt of de benodigde vorken voor filosoof i beschikbaar zijn en als dit het geval is worden ze allebei als bezet aangemerkt (waarde is i). Eventueel wordt op beschikbaarheid van de vorken gewacht. Alleen voor halen en brengen van vorken is toegang tot de monitor nodig, het eten kan concurrent gebeuren. Omdat het eten nu buiten de monitor gebeurt kan er in principe door meerdere filosofen tegelijk gegeten worden. Anders dan in programma 4.3 is er bij de filosofen sprake van expliciete synchronisatiecode. Als filosoof i eet zijn beide benodigde vorken voor hem geregistreerd (vork[i]==i en vork[i+1]==i) en daarom is aan buuruitsluiting voldaan. Veronderstel nu dat een of meerdere filosofen honger hebben en dat voor minstens een

65 4.3 Deadlock 55 ervan de vorken vrij zijn. Een filosoof kan alleen geblokkeerd zijn op een wait als een van zijn vorken bezet is; bij het neerleggen van vorken worden immers alle filosofen weer een voor een in de monitor toegelaten. Een hongerige filosoof die in de monitor komt en zijn vorken vrij aantreft kan gaan eten. Daarom zal minstens een filosoof mogen gaan eten, waarmee aan progress is voldaan. Helaas voldoet deze oplossing niet aan no starvation. Zelfs als de monitor fair is en elke wachtende thread aan de beurt laat komen bij achtereenvolgende notifyall s lijdt deze constructie aan starvation. Stel dat filosoof i wil eten, maar dat i 1 en i + 1 om de beurt honger krijgen en de vorken pakken terwijl de ander nog eet. Hoe vaak thread i ook door de monitor wordt toegelaten tot procedure haal, aan de voorwaarde dat beide vorken vrij zijn is nooit voldaan. Je ziet hieruit dat de progress- en no starvation-eigenschappen van de onderliggende objecten (zoals de monitor) niet impliceren dat de toegangcontrole als geheel deze eigenschappen ook vertoont Progress en no starvation Door bovenstaand voorbeeld uit te werken is zelfs in te zien, dat er geen oplossing mogelijk is die aan progress en no starvation voldoet. Inderdaad, als alleen i 1 eet terwijl i en i + 1 hongerig zijn, dan impliceert progress dat i + 1 moet kunnen eten voordat i 1 afsluit. Als vervolgens i 1 afsluit en weer hongerig wordt, impliceert progress weer dat i 1 moet kunnen eten voordat i + 1 afsluit. Een oplossing waarin filosoof i ooit aan de beurt komt, moet dus filosoof i + 1 (of i 1) een keer laten wachten, ook al zijn beide vorken vrij, als filosoof i hongerig is. Omdat no starvation (dat iedereen die honger heeft aan de beurt komt) belangrijker is dan progress (wat meer met de totale doorstroming te maken heeft), zullen we ons in het vervolg op no starvation concentreren. 4.3 Deadlock Deadlock is een situatie waarin een deelverzameling R van de threads in de wachttoestand verkeert en daaruit slechts kan ontwaken na een actie van een thread in R. Soms worden deadlocks gezien als het tegenovergestelde van progress, maar de relatie ligt ingewikkelder. De benodigde actie kan bijvoorbeeld een V-operatie op een semafoor of het vrijgeven van een monitor zijn. De threads in een deelverzameling in deadlock blijven altijd geblokkeerd en zullen dus nooit de aangevraagde resources krijgen. Als er dan in het geheel geen resources worden toegekend, is er inderdaad sprake van schending van progress. Het is echter bij een deadlock niet noodzakelijk zo, dat alle threads van een applicatie geblokkeerd zijn! Er kunnen behalve de threads in deadlock nog andere zijn die wel resources krijgen en dan is er wel progress. In ieder geval levert deadlock een schending van no starvation Wait-for-graphs Als hulpmiddel in de discussies over deadlocks introduceert men vaak de zogenaamde waitfor-graph waarvan figuur 4.6 een voorbeeld toont. Knopen van de graaf zijn de threads, en er is een kant van u naar v als u in een wachtsituatie verkeert en voor continuering afhankelijk is van v.

66 56 4 Dining Philosophers p q r s t Figuur 4.6: Voorbeeld van een wait-for-graph. De beoordeling of er een deadlock is kan lastig zijn; de applicatie kan er zeer levendig uitzien doordat (bijvoorbeeld in een multi-threaded sorteeranimatie) er actieve threads zijn die de animatie flink in beweging houden. De sorteeranimatie zal verdenking oproepen als het programma tot stilstand komt en de array is nog niet geheel gesorteerd. Maar in het geval van oneindig lang lopende applicaties is het probleem veel moeilijker: je geeft een print-opdracht en na een kwartier heb je je output nog niet; is het systeem gewoon traag of is er iets vastgelopen? Zelfs met de wait-for-graph voor ogen is er nog van alles mogelijk. Is er in figuur 4.6 sprake van deadlock? Thread p is nog actief, en q wacht op een actie van p die hopelijk ooit nog komt (het vrijgeven van een resource). Voor de beoordeling van r, s en t moeten we weten in wat voor situatie r zit. Als r kan doorgaan na het vrijgeven van een resource door q of door s, is er geen sprake van deadlock. Immers, q kan de resource van p krijgen en verdergaan, en daarna vrijgeven wat r nodig heeft om verder te gaan. Als r echter de beide resources van q en van s nodig heeft, is de deelverzameling {r, s, t} gedeadlocked. Een cykel in de graaf is blijkbaar geen voldoende voorwaarde voor een deadlock, maar is wel een noodzakelijke voorwaarde. Lemma 4.1 In een deadlock situatie bevat de wait-for-graph een cykel. Bewijs. Zij R een verzameling threads die in deadlock verkeert. Elke thread in R wacht op een actie van een thread in R, heeft dus in de wait-for-graph tenminste één kant naar een knoop in R. Hieruit volgt dat de graaf een cykel bevat (die geheel in R is gelegen). Bij een and-request (waarbij een thread afhankelijk is van alle threads waarop hij wacht) is een cykel ook een voldoende voorwaarde. Bij een or-request (waarbij een thread afhankelijk is van een van de threads waarop hij wacht) wordt deadlock gekarakteriseerd door een zogenaamde knot, dit is een sterk samenhangende component van twee of meer knopen zonder uitgaande kanten. Wij gaan hier verder niet op in. Voor het ontstaan van deadlocks zijn vier factoren nodig [CES71]. 1. Objecten worden zo met elkaar geshared dat de ene thread soms moet wachten op een andere. 2. Een thread die een resource bemachtigd heeft houdt die vast terwijl hij op toewijzing van andere wacht. Hierdoor kan er op een wachtende thread gewacht worden zodat ketens van wachten ontstaan. 3. Het systeem kan threads niet afbreken of dwingen resources af te geven; alleen een actie van de thread zelf maakt de resource weer vrij. 4. Er bestaat een cyclische structuur in de wait-for-graph.

67 4.3 Deadlock 57 De diverse heuristieken om het deadlock-probleem op de knieën te krijgen zijn allemaal pogingen om het systeem zo te ontwerpen dat aan één van de vier voorwaarden niet is voldaan. Inderdaad, één van de voordelen van de wachtvrije technieken (hoofdstuk 10 en verder) is dat er zonder wachten geen deadlock kan bestaan (factor 1). Programma 4.5 illustreert de werking van factor 2; een thread pakt, wanneer dat mogelijk is, beide vorken, en gaat dus niet met een vork zitten wachten op de andere. (Helaas illustreert programma 4.5 ook dat vrijheid van deadlocks nog niet voldoende is om het filosofenfeest in een juichstemming te laten eindigen.) Priority scheduling We kunnen het ontstaan van cykels in de wait-for-graph bijvoorbeeld voorkomen door factor 3 aan te pakken; we gaan toestaan dat vorken weer van (nog niet etende) filosofen worden afgepakt. Om te bepalen wanneer we dit doen gebruiken we prioriteiten; neem even aan dat filosoof i prioriteit i heeft. Een deadlock-vrije oplossing kan nu als volgt worden verkregen: zodra een filosoof merkt dat hij wacht op een vork in bezit van een filosoof met een lager nummer die nog niet eet, mag hij die vork afpakken van die filosoof. De gedupeerde collega is dan een stap terug in de race, want behalve op de tweede vork, moet nu ook weer opnieuw worden gewacht op de vork die eerder al was opgepakt. Het gevolg is dat pijlen in de wait-for-graph steeds wijzen naar filosofen met hogere prioriteit, en dan kan de graaf geen cykel bevatten. Men zal zich gemakkelijk voor kunnen stellen dat er wel starvation kan optreden. Hier is dan weer iets aan te doen door de prioriteiten niet statisch maar dynamisch te maken: een filosoof die een vork van zijn buurman afpakt, kent zichzelf nadien een lager prioriteit toe dan die buur. Het compleet uitwerken van zo n oplossing en het bewijzen van alle gewenste eigenschappen laten we graag aan de lezer over als puzzel Deadlock detection and recovery Een veelbesproken techniek is deadlock detection and recovery, die zich ook richt op de derde factor en ongeveer hier op neer komt. Er wordt in de wait-for-graph gekeken of zich daar kritieke structuren hebben gevormd (cykels danwel knots). Mocht uit deze inspectie blijken dat er een deadlock bestaat, dan worden er één of meer threads in de structuur afgebroken. Deze threads worden gedwongen de bemachtigde resources vrij te geven, en vervolgens gekilled of herstart. In de filosofen-allegorie: we gaan vorken afpakken van filosofen die al eten, en een aldus gedupeerde filosoof moet dan maar zien of hij opnieuw vorken gaat halen en dezelfde maaltijd opnieuw gebruikt. Toepassing van deze methode stuit op diverse problemen. 1. De wait-for-graph moet expliciet worden gemaakt. We moeten daarom van elke wachtende thread weten, op welke andere thread(s) hij wacht. Maar vaak is de wachtconditie bevat in een semafoor of conditievariabele, en is niet meteen duidelijk van welke thread actie wordt verwacht. 2. Er moet een graafalgoritme op worden uitgevoerd. In een graaf met enkele honderden of misschien duizenden knopen kan dit enige tijd vragen. Het liefst doe je dit in de monitor die de toegang regelt, want als je toestaat dat de graaf verandert tijdens het zoek-algoritme maak je de zaak er niet eenvoudiger op. Deadlock detectie kan dus veel tijd kosten en de toegang tot resources behoorlijk vertragen.

68 58 4 Dining Philosophers 3. Er moeten threads voor afbreken worden geselecteerd. De gekozen threads moeten samen alle deadlocks breken, en je wilt de schade (in aantal threads, of verloren gegaan werk) zoveel mogelijk beperken. 4. Het verrichte werk van de afgebroken threads is (in het gunstigste geval!!) verloren. 5. Je moet threads netjes termineren. Een thread die half is geëxecuteerd kan de bewerkte data op onvoorspelbare manier hebben verstoord, en herstel van de beginsituatie is slechts in bijzondere situaties mogelijk. Een halve spaghettimaaltijd kan echter zeer zwaar op de maag liggen zoals blijkt uit het volgende voorbeeld. Men kan aanvoeren dat het voor multithreaded sorteren niet uitmaakt om de thread vanuit halfgesorteerde toestand weer op te starten omdat het sorteeralgoritme elke permutatie sorteert. Helaas. Stel je eens voor dat QuickSort de eerste helft van een array-segment heeft gesorteerd en dan wordt afgebroken, waarna QuickSort op het hele segment herstart wordt. Als je programma steeds het eerste element als pivot kiest krijgt de herstarte thread meteen het worst-case kwadratische gedrag van QuickSort. Deadlock detection en recovery is voor het beheer van een concurrent geprogrammeerde applicatie niet altijd heel geschikt, maar is populair in database managers om deadlocks bij concurrente transacties op te lossen. De eigenschappen van deze applicatie zijn er meer voor geschikt. Bij database transacties is doorgaans gemakkelijk te achterhalen welke transactie houder is van een resource waarop wordt gewacht; we zitten dus niet met probleem 1. De transacties in een database zijn meestal zo ontworpen, dat het reeds gedane werk gemakkelijk geheel terug te draaien is; dit geeft een mooie oplossing van probleem 5. De transacties in een database komen doorgaans op volstrekt onvoorspelbare manier binnen, terwijl een applicatieprogrammeur vaak een overzicht heeft over de interacties van zijn threads, waardoor andere deadlock oplossingen mogelijk worden. Bij transacties in een database systeem weet je vaak niet vantevoren welke resources precies nodig zijn; dit blijkt pas tijdens de transactie! Onze filosofen weten vantevoren precies dat ze twee vorken nodig hebben; in database transacties zien we filosofen die na een paar happen opeens bestek erbij eisen voor ze verder willen eten. 4.4 Deadlock avoidance: servetten Bij deadlock avoidance probeert men de toestroom van threads naar de gedeelde resources zo te beperken dat geen deadlocks kunnen ontstaan. We gaan dit toelichten met een aanpassing van programma 4.2. Omdat er n vorken zijn en een filosoof er twee nodig heeft om te eten, kunnen er hoogstens n/2 filosofen tegelijk eten. Omdat een filosoof alleen op zijn buren kan wachten, heeft een cykel in de wait-for graaf altijd precies n knopen, en er is geen deadlock als er minder dan n filosofen in de race zijn. In programma 4.7 wordt dat bereikt door de toegang tot de vorkenrace te regelen met servetten, waar er maar n 1 van zijn. Voordat hij vorken mag pakken moet een filosoof een servet omdoen.

69 4.5 Deadlock prevention: ordening van resources 59 vork[n]: Semafoor initieel 1 servet : Semafoor initieel n-1 Filosoof i:... // Denk servet.p // Honger vork[i].p ; vork[i+1].p // Honger Eet // Eet vork[i].v ; vork[i+1].v // Afsluiten servet.v // Afsluiten... // Denk Programma 4.7: Pak een servet, dan pas vorken. De conclusie dat de wait-for-graph geen cykels kan bevatten moet voorzichtig worden getrokken! Deadlock avoidance introduceert namelijk in het algemeen nieuwe wacht-toestanden, in ons geval het wachten op een servet. Bovenstaande afwezigheid van deadlocks moeten we nu zo lezen dat er is geen cykel van wachtenden op een vork als er hoogstens n 1 filosofen in de vorken-race zijn. De n-de filosoof, in de nieuw gecreëerde wachtsituatie, zou in het algemeen een deadlock-cykel kunnen rondmaken! In ons voorbeeld is dat niet zo. Filosofen die al een servet hebben wachten nooit op stakkers die dit fluweelzachte symbool van beschaving nog niet hebben bemachtigd, en er is dus nooit een pijl van vork-wachters naar servet-wachters in de graaf. 4.5 Deadlock prevention: ordening van resources Bij deadlock prevention wordt geprobeerd het systeem a priori zo te ontwerpen dat deadlocks onmogelijk zijn. Een populaire en succesvolle strategie is het uitsluiten van cykels in de wait-for graaf door het toekennen van een vaste volgorde aan de resources. Programma 4.8 is vrijwel gelijk aan programma 4.2. Evenals in dat programma zijn de vorken als semafoor geïmplementeerd en moet filosoof i de vorken i en i + 1 pakken alvorens te eten. Maar in programma 4.8 pakt een filosoof altijd de hoger genummerde vork eerst. Omdat de nummers van de vorken modulo n gerekend worden geeft de vreemd aandoende test (i > i+1) voor filosoof n 1 de uitkomst true en deze filosoof moet eerst vork n 1 pakken en dan vork 0. De volgorde van het neerleggen van de vorken is niet van belang in dit programma, dit komt omdat voor het neerleggen van de vork toch niet hoeft te worden gewacht. Net als in programma 4.2 zorgt het gebruik van semaforen voor buuruitsluiting. De ordening van de vorken sluit deadlocks uit. Stelling 4.2 Programma 4.8 kent geen deadlocks. Bewijs. Een thread kan alleen wachten op vorken. We bewijzen met inductie naar i de bewering IH(i): Een thread die wacht op een vork i zit niet in een cykel.

70 60 4 Dining Philosophers vork[n] : Semafoor initieel 1 Filosoof i:... // Denk if (i > i+1) // We rekenen modulo n!! { vork[i].p // Honger vork[i+1].p // Honger } else { vork[i+1].p // Honger vork[i].p // Honger } Eet // Eet vork[i].v vork[i+1].v // Afsluiten... // Denk Programma 4.8: Pak de hoogste vork eerst. i = 0: Omdat vorken in dalende volgorde worden opgepakt is vork 0 voor elke filosoof de laatste die hij pakt. Wie wacht op vork 0 wacht dus op een filosoof die zijn twee vorken heeft, en eet. i + 1: Zij p een filosoof die wacht op vork i + 1 die echter in bezit is van q. Omdat vorken in dalende volgorde worden opgepakt, is q etende of wacht op een vork met een kleiner nummer; wegens IH(i) is q niet bevat in een cykel. Er is echter niet voldaan aan progress, bijvoorbeeld in het volgende scenario. Filosoof 0 eet met vorken 0 en 1. Filosoof 1 krijgt honger, pakt vork 2 (de hoogste eerst) en gaat wachten op vork 1. Filosoof 2 krijgt honger, pakt vork 3 en gaat wachten op vork 2. De eis van buuruitsluiting staat toe dat filosoof 2 tegelijk met filosoof 0 eet, en dat filosoof 2 nu moet wachten (tot eerst filosoof 0 en dan filosoof 1 klaar is) is dus een schending van progress. De oplossing voldoet ook aan no starvation en dat is op dezelfde manier te bewijzen. Stelling 4.3 Een hongerige filosoof kan uiteindelijk eten. Bewijs. We bewijzen met inductie naar i de bewering IH(i): Vork i wordt nooit oneindig lang vastgehouden. Met andere woorden, een filosoof die vork i pakt geeft die na eindige tijd weer vrij. Wachten op een vork die steeds maar eindig lang wordt vastgehouden duurt ook maar eindig lang. Natuurlijk kunnen er, terwijl filosoof p op de vork wacht, andere filosofen die vork eerst krijgen, maar door no starvation voor semaforen voltooit p ooit zijn P-operatie als er maar genoeg V-operaties worden gedaan. i = 0: Stel filosoof p heeft vork 0. Vork 0 is altijd de laatste die een filosoof pakt. Filosoof p kan dus eten en geeft daarna de vork vrij.

71 Samenvatting en conclusies 61 i + 1: Stel filosoof p heeft vork i+1. Omdat vorken in dalende volgorde worden opgepakt, kan p eten, of heeft alleen nog maar een kleinere vork (i) nodig. Wegens de inductiehypothese wordt i nooit oneindig lang vastgehouden, en wegens no starvation krijgt p die vork. Filosoof p kan dus eten en geeft daarna de vork vrij. Een filosoof die wil eten, hoeft dus op elk van de vorken slechts eindig lang te wachten. Resource ordening is in allerlei situaties te gebruiken waarin threads meerdere resources met elkaar delen, waarbij een thread voor een bepaalde taak exclusieve toegang tot een aantal resources nodig heeft. De algemene oplossing is dan, een globale ordening op de resources aan te brengen en alle threads de resources in die volgorde te laten aanvragen. Net als in programma 4.8 is het mogelijk dat een thread p meerdere andere blokkeert in hun voortgang. Zelfs een thread r die geen resources van p nodig heeft kan worden geblokkeerd, namelijk als er een q is waarvan r een resource nodig heeft, maar die op zijn beurt wacht op een resource van p. Vrijkomende resources worden echter op een eerlijke manier weer toegewezen zodat starvation niet mogelijk is. Toepassing ervan vereist wel, dat een thread voor het aanvragen van een resource al weet welke resources nodig zijn; immers de resources moeten in volgorde verkregen worden. Een iets zwakkere, maar voldoende, voorwaarde is dat bij het aanvragen van een resource x in ieder geval zeker is dat geen resources groter dan x in de toekomst nodig zullen zijn. Samenvatting en conclusies Mutual-exclusionprotocollen, semaforen en monitors zijn hulpmiddelen waarmee je gemakkelijk één resource kunt delen tussen threads. Als er meerdere resources zijn, komen er extra complicaties want je wilt dat meerdere threads tegelijk actief kunnen zijn als ze tenminste verschillende resources gebruiken. Het is aantrekkelijk te eisen, dat een thread altijd kan beginnen als de benodigde resources vrij zijn. De progress-eis stelt, dat als er een of meer threads wachten, en voor een of meer ervan zijn geen resources in gebruik, er threads door mogen gaan. We hebben echter gezien dat deze strenge eis onvermijdelijk tot starvation leidt. Er zullen dus threads moeten wachten ook al zijn hun resources vrij. Het aanvragen en vervolgens bezethouden van resources in willekeurige volgorde (programma 4.2) kan leiden tot een deadlock. Als iedere thread wacht tot al zijn resources vrij zijn en ze dan in een keer pakt (programma 4.5), kan gemakkelijk starvation ontstaan. Deadlocks kunnen in gedistribueerde applicaties ontstaan wanneer threads op elkaar wachten, en in de wait-for-graph een cykel ontstaat. Cykels in de wait-for-graph kunnen op verschillende manieren worden voorkomen, bijvoorbeeld door het definiëren van prioriteiten tussen threads. Ook kan men proberen de toestroom van threads naar wachttoestanden te beperken, men spreekt dan van deadlock avoidance. Een handige methode is resource ordening, waarbij resources in een vaste volgorde worden aangevraagd (en vervolgens vastgehouden tot na het gebruik). Voor resource ordening is nodig, dat bekend is welke resources nodig zijn voordat het aanvragen van de eerste begint. In bijvoorbeeld databases is dit niet altijd het geval. Daarom wordt daar ook wel deadlock detection toegepast, waarbij men een deadlock wel laat ontstaan maar vervolgens oplost door een of meerdere threads te termineren.

72 62 4 Dining Philosophers Opgaven bij hoofdstuk 4 Opgave 4.1 Beschrijf hoe een deadlock in programma 4.2 ontstaat. progress eis voor de gebruikte semaforen is voldaan. Laat zien dat aan de Opgave 4.2 Beschrijf een situatie in programma 4.3 waarin een hongerige filosoof niet kan gaan eten, hoewel allebei de nodige vorken vrij zijn. Voldoet programma 4.3 aan no starvation? Opgave 4.3 Geef een variant van programma 4.4, waarbij het token wordt doorgegeven met een P/V-operatie op een semafoor. Bewijs dat de oplossing aan no starvation voldoet als de semaforen aan progress voldoen (en semafoor). Opgave 4.4 Maak een executie van programma 4.5 waarin 2 filosofen tegelijk eten. Wat is het maximale aantal tegelijk etende filosofen? Opgave 4.5 Laat zien hoe in de oplossing van sectie starvation kan optreden. Welke filosofen kunnen er het slachtoffer van worden, en kun je dit voorkomen door statische prioriteiten anders toe te kennen? Opgave 4.6 Bedenk een oplossing voor de dining philosophers die prioriteit-gestuurd afpakken van vorken gebruikt en starvation voorkomt door dynamische prioriteiten. Formuleer en bewijs de correctheidseigenschappen van je oplossing. Opgave 4.7 Waarom is programma 4.7 vrij van starvation? Opgave 4.8 Synchronisatie van meerdere resources. (Uit het tentamen van oktober 2005.) Een aantal threads sorteren samen een Array A[1..n], waarbij een thread telkens naastgelegen elementen omwisselt indien nodig: if A[i] > A[i+1] { h=a[i] ; A[i]=A[i+1] ; A[i+1]=h } Gedurende deze hele kritieke sectie is exclusieve toegang tot A[i] en A[i+1] nodig. (a) Leg uit waarom het niet mogelijk is de threads zo te synchroniseren, dat zowel aan Progress als aan No-starvation wordt voldaan. (b) Een voorgestelde oplossing gebruikt een rij semaforen s[1..n], en voorafgaand aan bovenstaande operatie doet de thread s[i].p ; s[i+1].p. Na de kritieke sectie worden de semaforen vrijgegeven. Is er in deze oplossing deadlock mogelijk? Zo ja, hoe is dit te repareren? (c) Hoeveel threads kunnen er maximaal tegelijk wachten in deze oplossing? Hoeveel threads kunnen er maximaal tegelijk in hun kritieke sectie? Opgave 4.9 Als programma 4.7 wordt gebruikt met n 3 of minder servetten is een situatie mogelijk waarin een hongerige filosoof niet kan eten hoewel beide benodigde vorken vrij zijn. Hoeveel servetten zijn minstens nodig om deze concurrency-belemmering uit te sluiten? Opgave 4.10 Hoeveel hongerige filosofen kunnen er in programma 4.2 maximaal tegelijk wachten terwijl er één filosoof eet? Dezelfde vraag voor programma 4.5, de oplossing van sectie 4.3.2, programma 4.7, en programma 4.8.

73 Opgaven bij hoofdstuk 4 63 Opgave 4.11 De wait-for graph. (Uit het tentamen van mei 2001.) (a) Leg uit wat in een gedistribueerde applicatie de wait-for graph is. (b) Geef een voorbeeld van een situatie waarin deze wait-for graph een cykel bevat, maar er geen sprake is van deadlock. (Geef expliciet aan waarop wordt gewacht.) (c) Noem enkele manieren om te voorkomen dat er cykels in de wait-for graph komen, en geef korte uitleg. Opgave 4.12 Resource ordening. (Uit het tentamen van februari 2002.) Bij resource ordening wordt een ordening op de resources in een systeem gedefinieerd; een thread moet altijd resources aanvragen in dalende volgorde. (a) Waarom kan resource ordening in sommige situaties, bijvoorbeeld voor database-transacties, niet worden toegepast? (b) Welke vier factoren zijn nodig voor het ontstaan van deadlocks? (c) Bewijs dat resource ordening deadlocks voorkomt.

74 Hoofdstuk 5 Electie In de eerdere hoofdstukken werd verondersteld dat de interactie tussen threads verloopt door het lezen en schrijven van gedeelde variabelen. Dit is van oudsher de meest gebruikelijke vorm van communicatie en dit is ook voor de hand liggend omdat het lezen en schrijven van variabelen ook in niet-concurrente programma s voorkomen. In dit hoofdstuk wordt een andere vorm van communicatie verondersteld, namelijk berichtenverkeer. Deze communicatie is te vergelijken met het versturen en ontvangen van brieven (of, iets moderner, s). Zowel het verzenden als het ontvangen zijn dan expliciete acties, en elk verzonden bericht wordt precies één keer ontvangen. Omdat het aantal uitgewisselde berichten gemeten kan worden, is in dit model communicatie een parameter die als complexiteitsmaat gebruikt kan worden net zoals we dat van de grootheden tijd en geheugen gewend zijn. In dit hoofdstuk speelt daarom complexiteit van gedistribueerde programma s een grote rol. Complexiteit heeft te maken met bovengrenzen en ondergrenzen die je kunt bewijzen op de kosten voor een bepaald probleem. Een bovengrens van de complexiteit krijg je door een oplossing te geven en de complexiteit van die oplossing te berekenen. Soms al behoorlijk ingewikkeld, maar voor de algoritmen in dit hoofdstuk (sectie 5.3) nog goed te doen. Om een ondergrens op de complexiteit te bewijzen moet je redeneren over alle mogelijke algoritmen die het probleem oplossen. Je moet bewijzen dat al deze algoritmen een eigenschap gemeen hebben, namelijk dat hun kosten (communicatie, tijd, of geheugen) tenminste een bepaalde waarde bedraagt. Bij het redeneren over gedistribueerde programma s wordt hierbij steeds van een soortgelijk argument gebruik gemaakt. Threads (cq., processen) hebben namelijk geen kennis over de globale toestand, maar kunnen alleen handelen op basis van hun lokale kennis. Als een proces in een zekere executie een reeks stappen uitvoert, dan kun je afleiden dat het proces in een andere situatie hetzelfde doet, mits die andere situatie lokaal op de eerste lijkt. In sectie 5.4 wordt dit gedaan. Het probleem dat in dit hoofdstuk bekeken wordt is het electie probleem: kies één van de processen uit om een bijzondere rol te vervullen. Dit kan toegepast worden in gedistribueerde applicaties waarin niet van te voren bekend is welke processen er precies zullen zijn. Het probleem speelt in moderne gedistribueerde systemen niet meer zo n belangrijke practische rol omdat door de client/server benadering vaak een natuurlijke coördinator voor een taak beschikbaar is. Wel leent het probleem zich uitstekend om de fundamentele mogelijkheden en onmogelijkheden van distributie te bestuderen, evenals het consensus probleem dat in hoofdstuk 6 aan de orde komt. Aan de hand van het electie-probleem kan bijvoorbeeld vastgesteld 64

75 5.1 Berichtuitwisseling 65 worden dat het gebruik van timers tot een verbetering van de complexiteit kan leiden. Aan dit laatste resultaat komt dit hoofdstuk overigens niet toe; bij alle resultaten wordt verondersteld dat het systeem geheel asynchroon is. 5.1 Berichtuitwisseling Naast het gebruik van gedeelde variabelen of objecten, zoals we dat in de vorige hoofdstukken zagen, kennen we een tweede manier van communiceren en dat is door middel van message passing. De communicatie verloopt hierbij veel gestructureerder: een thread kan informatie verzenden door te schrijven in een buffer, hier kanaal genoemd, en een andere thread ontvangt die informatie door uit die buffer te lezen. De informatie noemen we natuurlijk een bericht (of message voor de anglofielen), en het schrijven en lezen van de buffer noemen we send en receive (dan wel, zenden en ontvangen voor de taalpuristen!). De structurering bestaat hieruit, dat elk bericht precies één keer wordt gelezen; de onzekerheid over hoevaak een andere thread een object heeft gebruikt verdwijnt dus. Message passing kan in een multithreaded applicatie gemakkelijk worden geïmplementeerd met de begrensde buffers die we in hoofdstuk 3 hebben gezien. Je kunt het zelfs gemakkelijk zo maken, dat een kanaal door meerdere threads kan worden beschreven, en door meerdere threads kan worden uitgelezen. Dat kan handig zijn, maar wij bekijken alleen buffers die één schrijver (sender) en één lezer (receiver) kennen. Je kunt je dan een systeem voorstellen als een netwerk waarin de threads/processen knopen zijn en de kanalen kanten. Message passing is de aangewezen vorm van communicatie tussen threads of processen op verschillende machines; er is dan immers geen gemeenschappelijk geheugen. Het operating system stelt message passing beschikbaar door een mechanisme van sockets, die kunnen worden geopend, geschreven, etcetera. Omdat we van de akelige onvoorspelbaarheid van interleavings af zijn zal de programmeur snel de elegantie van message passing leren waarderen. Men bedenke echter, dat bij message passing alle communicatie expliciet is! In het voorbeeld van het multithreaded sorteren wordt een thread afgesplitst om een gedeelte van het array te sorteren. Bij communicatie met gedeelde objecten geeft men de grenzen mee aan de thread, en de thread gaat vervolgens in de array zelf aan het werk; deze is immers aan alle threads beschikbaar. Bij message passing echter moeten alle elementen van de array expliciet aan de nieuwe thread worden opgestuurd, en na het sorteren moeten ze in volgorde weer worden teruggestuurd; de kanalen zijn volop in bedrijf. Aan de andere kant kicken theoretici enorm op deze eigenschap omdat de hoeveelheid communicatie tussen threads nu precies meetbaar is. Dat betekent dat je bij een bepaalde probleemstelling kunt praten over een zogenaamde communicatie-complexiteit, de hoeveelheid communicatie die je voor het oplossen nodig hebt. Je kunt dat meten in aantal berichten (bericht-complexiteit) of, als je dat nog niet precies genoeg vindt, kun je het aantal bits van alle berichten bij elkaar tellen (bit-complexiteit). Omdat er geen gedeelde objecten zijn is het ontvangen van een bericht de enige manier om informatie uit de andere processen te krijgen. In hoofdstuk 7 zullen we zien hoe communicatiecomplexiteit wordt gebruikt om de kracht van randomisering uit te drukken.

76 66 5 Electie Kader 5.1: Een stukje geschiedenis: de IBM token ring Toen in de jaren zeventig verschillende fabrikanten werkten aan de ontwikkeling van lokale netwerken, kwam IBM met een ontwerp waarin de stations in een cyclische structuur waren verbonden (standaard 802.5). Informatie werd, ongeacht begin- en eindstation, altijd helemaal rondgezonden en wie mocht zenden werd bepaald door een token. Dit permissiebericht werd van de ring verwijderd om te worden vervangen door een te verzenden bericht. Bij het opstarten van de ring, of na een crash, moesten de stations samenwerken om een coördinator te kiezen die een token op de ring moest plaatsen. Token rings worden nog steeds gebruikt (kader 7.4), en de principes erachter zijn opnieuw gebruikt in FDDI (Fiber Distributed Data Interface), een snelle token ring die op glasvezels is gebaseerd [Tan96]. 5.2 Probleemstelling: electie Uitgaande van een situatie met alle processen in dezelfde toestand willen we bereiken dat één proces in leider toestand komt en de anderen in niet-leider. We beginnen met de context waarin het probleem als eerste opdook, namelijk een ring-vormige configuratie met verkeer in één richting; zie kader 5.1. Het aantal processen in de ring noemen we n. Het probleem trad op bij het ontwerp van lokale netwerken van het token-ring type; de coördinator kan uitvallen, waarna de overlevende stations moeten samenwerken om te komen tot de keuze van één nieuwe manager. We nemen aan dat elk station een naam of nummer kent en die naam gaan we gebruiken; de namen zijn uniek, dwz., er zijn geen twee processen met dezelfde naam. Om wat meer inzicht in de probleemstelling te krijgen bekijken we eerst enkele oplossingen die niet bruikbaar zijn. Een bruikbaar idee (hoewel niet de enige mogelijkheid) is: we gaan het proces met kleinste nummer leider maken Maak 0 de leider De unieke nummers zullen vast wel de getallen 0 tm n 1 zijn, dan is het kleinste nummer 0 en we lossen het probleem op zonder communicatie. if (p==0) { state = leider } else { state = niet-leider } Afgezien van de mogelijkheid dat station 0 gecrasht is, we zullen verder de aanname maken dat de identificatienummers uit een willekeurig groot domein kunnen komen: de verzameling namen is een willekeurige eindige deelverzameling van N. Het is dus niet van 0 noch van enig ander getal zeker dat het voorkomt.

77 5.3 Enkele oplossingen Wie is kleiner dan zijn voorganger Als de nummers opklimmend gerangschikt zijn is er precies één proces dat een grotere voorganger heeft en lossen we het probleem op met n berichten. send <p> ; receive <q> if (q > p) { state = leider } else { state = niet-leider } Maar helaas, we moeten de aanname maken dat elke rangschikking van de namen mogelijk is, en dan kunnen er meerdere processen in de leider toestand terecht komen. De meeste algoritmen zijn wel gebaseerd op het onderling vergelijken van de namen, je kunt dit zien in sectie 5.3. De algoritmen hebben dan de eigenschap dat de executies hetzelfde blijven als je de identiteitsnummers wijzigt met behoud van onderlinge volgorde. Voor het geven van voorbeeld-executies of het analyseren van complexiteit kun je dan steeds uitgaan van de situatie dat de procesnummers de getallen 1 tot n zijn, maar nogmaals, de werking van het programma mag niet van dat uitgangspunt afhangen! Circuleer een token Je kunt het ongeveer zo proberen zoals je in een array van getallen het minimum bepaalt. Het eerste proces zendt zijn nummer naar de volgende, en elk proces zal, na ontvangst van een nummer, het minimum van het ontvangen nummer en zijn eigen nummer doorgeven. Na enige tijd (en uitwisseling van n berichten) ontvangt de beginner een bericht met het minimum erin. Een berichtje dat steeds wordt doorgegeven, eventueel met aanpassing aan de inhoud, tot het weer terugkomt waar het begon wordt vaak een token genoemd. Bedenk dat feitelijk steeds van een nieuw berichtje sprake is: en dat de ontvangst van één bericht alleen maar de aanleiding is om een nieuw bericht aan de volgende in de ring te sturen. Er is echter niet zoiets als het eerste proces, immers, alle processen zijn in dezelfde begintoestand! Als we reeds over een leider zouden beschikken, ja, dan konden we met n berichten het minimum achterhalen Enkele oplossingen Het algoritme van LeLann In 1977 publiceerde Gérard LeLann [LeL77] de volgende oplossing: 1. Elk proces stuurt een bericht rond met z n eigen nummer. 2. Binnenkomende berichten met andere nummers worden doorgestuurd. 3. Een proces dat zijn eigen nummer ontvangt, beslist over leiderschap. Dit is nl. het teken dat het alle berichten heeft gezien. 4. Een proces dat het kleinste nummer heeft van alle die het gezien heeft, wordt leider. Het algoritme gebruikt dat kanalen de first-in, first-out (kortweg fifo) eigenschap hebben, dat wil zeggen dat berichten elkaar niet inhalen in de kanalen. Buffers zoals in hoofdstuk 3 hebben die eigenschap, maar die hoeft niet bij elk systeem te gelden.

78 68 5 Electie Wat is de complexiteit? Het ziet ernaar uit dat elk proces één berichtje maakt, dat dan helemaal rondgaat: n berichten dus. Maar omdat elk bericht n keer wordt doorgestuurd is de complexiteit n 2. We tellen immers niet het aanmaken, maar het versturen van berichten Het algoritme van Chang en Roberts Chang en Roberts bedachten in 1979 dat zo n rondgaand berichtje best geëlimineerd mag worden door een proces met kleinere identiteit: dat constateert immers dat de afzender van het bericht niet zal winnen. 1. Elk proces stuurt een bericht rond met z n nummer. 2. Elk proces stuurt berichten met kleinere namen dan zijn eigen door (en het wordt meteen niet-leider). Berichten met grotere nummers worden niet doorgestuurd. 3. Een proces dat zijn eigen nummer ontvangt wordt leider. Waarom werkt het? Zij MIN het kleinste proces. Omdat de ring rond is, komt het nummer van elk ander proces langs MIN (als het niet al eerder gestopt is), ergo, een proces anders dan MIN ziet z n bericht niet terug. Niemand houdt MIN s bericht tegen, en MIN ziet dus zijn bericht wel terug en wordt leider. Wat is de complexiteit? Het aantal berichten blijkt nu sterk af te hangen van de verdeling van stations over de ring! In het beste geval zijn de processen in dalende volgorde gerangschikt en er zijn dan 2n 1 berichten. In het slechtste geval zijn de processen oplopend gerangschikt, elk bericht wordt pas afgestopt door MIN, en er zijn 1 2n(n + 1) berichten. Het kunstige van Chang en Roberts werk [CR79] was dat zij inzagen dat het aantal berichten alleen afhangt van de rangschikking van processen op de ring, en niet van non-determinisme tijdens de executie. Zij slaagden er zelfs in, de gemiddelde complexiteit te berekenen. Propositie 5.1 Het gemiddelde aantal berichten (over de (n 1)! verschillende rangschikkingen) is n.h n. Hier is H n het harmonisch getal 1 + 1/2 + 1/ /n, ongeveer ln n ofwel lg n. De berekening is niet zo spannend, meer iets om thuis door te lezen Het algoritme van Peterson Nu was de race goed geopend en men vroeg zich af of het ook met O(n lg n) berichten in worst case kon. Inderdaad lukte dit Hirschberg en Sinclair in 1980, maar zij namen wel aan dat er communicatie in beide richtingen over de ring mogelijk was (bidirectionele communicatie). We bekijken nu een variant van Peterson uit 1982 [Pet82] die hetzelfde voor elkaar krijgt maar iets simpeler is. De electie wordt gehouden in een aantal ronden, die tot doel hebben het aantal kandidaten te reduceren. 1. De eerste ronde begint met alle n processen als kandidaat voor het leiderschap. 2. In een ronde die met k > 1 kandidaten begint, worden ten minste k/2 kandidaten en ten hoogste k 1 kandidaten geëlimineerd. 3. Als een ronde met 1 kandidaat begint, merkt deze dat hij de enige is en wordt leider. De kandidaten noemen we actief, degenen die al geelimineerd zijn passief; de eerste ronde begint dus met iedereen actief. Hier is de werking van een ronde.

79 5.4 Een bewijs van ondergrens Een actief station stuurt zijn nummer naar links en naar rechts. 2. Een passief station stuurt wat van rechts komt door naar links en wat van links komt naar rechts. 3. Een actief station ontvangt vervolgens een nummer van links en een van rechts. Als deze gelijk zijn aan het eigen nummer, wordt het leider. Als een van de nummers kleiner is dan het eigen nummer wordt het passief. Als beide nummers groter zijn dan het eigen nummer gaat het actief naar de volgende ronde. Lemma 5.2 Er zijn ten hoogste lg n + 1 ronden. Bewijs. Een kandidaat kan de ronde alleen overleven als hij kleiner is dan zijn beide buurkandidaten. Hoogstens de helft van de kandidaten kan daarom overleven, en na ten hoogste lg n ronden is er slechts één kandidaat over. Dan is er nog 1 ronde waarin die kandidaat gekozen wordt. Lemma 5.3 In elke ronde worden precies 2n berichten verstuurd. Bewijs. Elk proces stuurt in een ronde precies één bericht naar links en één bericht naar rechts. De conclusie uit deze twee lemma s is dat het algoritme electeert met circa 2n lg n berichten. Na publicatie van het artikel van Hirschberg en Sinclair werd nog enige tijd geloofd dat een O(n lg n) oplossing alleen mogelijk was met bidirectionele communicatie, maar dat er in geval van éénrichtingsverkeer geen betere oplossing bestond dan Θ(n 2 ) worst-case. Peterson liet echter ook zien dat je met een truukje zijn algoritme zo kon veranderen dat het in een unidirectionele ring werkt, zonder dat het meer berichten kost (zie kader 5.2). Zo bewees Peterson deze stelling: Stelling 5.4 Er bestaat een algoritme voor electie op een unidirectionele ring van n processen dat O(n lg n) berichten gebruikt in het slechtste geval. Peterson wist zijn idee nog zover te verbeteren dat het aantal berichten werd begrensd door 1.44n lg n worst-case, zowel bi- als unidirectioneel. De meeste berichten worden verstuurd door passieve stations, voor het doorsturen van de nummers van actieven, die immers steeds verder uit elkaar komen te liggen. Als het mogelijk is om directe kanalen te openen tussen actieve stations, kan de complexiteit tot lineair worden teruggebracht. 5.4 Een bewijs van ondergrens Natuurlijk ging men zich na de uitvinding van Hirschberg en Sinclair afvragen of nog substantiële verbeteringen mogelijk waren: bestaat er een algoritme met een asymptotisch lagere communicatiecomplexiteit, bijvoorbeeld O(n lg lg n) of O(n) berichten? Het antwoord is negatief, want Burns [Bur80] wist te bewijzen dat elk electie algoritme een executie heeft waarin tenminste 1 2n lg n berichten worden ontvangen.

80 70 5 Electie Kader 5.2: Uitwisseling met eenrichtingsverkeer Een kaboutervolk leefde langs een lange oostwestweg, bij elke bushalte één kabouter. Er was bepaald dat op de jaarlijkse kabouterverkleeddag elke kabouter een sok moest ruilen met de twee kabouters die oostwaarts en westwaarts verderop aan de weg woonden. Geen probleem: elke kabouter trok zijn sokken uit, gaf er een mee met een bus die naar het oosten ging en een met een bus die naar het westen ging. Elke kabouter haalde een sok uit een bus die van het westen kwam en een uit een bus die van het oosten kwam, en klaar was de verkleedpartij. Maar er moest bezuinigd worden en voortaan reed de bus alleen nog maar oostwaarts; hoe moest de jaarlijkse sokkenruil nu uitgevoerd worden? Elke kabouter trok een sok uit en liet die bij de halte liggen, en nam de bus een halte naar het oosten. Bij de volgende halte trok de kabouter zijn andere sok uit en liet die in de bus, maar stapte zelf uit en vond de sok die door de vertrekkende kabouter was achtergelaten. Uit een bus vanuit het westen haalde de kabouter een sok om zijn kleding te completeren. Ga na dat de gewenste sokkenruil hiermee is uitgevoerd en bedenk een implementatie van Petersons algoritme op een unidirectionele ring. Het bewijs (hier overgenomen uit [AW98]) is instructief vanwege de manier waarop wordt geredeneerd over gedistribueerde systemen. Steeds ziet men in bewijzen van ondergrenzen of onmogelijkheid het idee van replay terugkomen. Dit komt hieruit voort, dat een proces in een gedistribueerd systeem alleen kan handelen op basis van zijn programma en informatie die het eerder heeft ontvangen. Veronderstel, dat proces p een bericht X van q ontvangt en vervolgens een bericht Y verstuurt. Het versturen van Y is onafhankelijk van wat er verderop in het netwerk gebeurt; we kunnen zelfs naar een ander netwerk gaan kijken, waarin q een extra proces r als buur heeft. Tenzij er communicatie tussen r en q plaatsvindt voordat q bericht X stuurt, kan ook in de nieuwe situatie proces p het bericht Y sturen. Omdat het gespecificeerde antwoord van een proces doorgaans wel van de situatie afhangt, kan met replay worden beargumenteerd dat er communicatie moet plaatsvinden. Een replay-argument heeft dan ongeveer deze vorm. Eerst wordt gekeken naar een executie (of deel van een executie) van het algoritme, waarbij aangenomen wordt dat alles conform de specificatie verloopt en er een zeker antwoord wordt opgeleverd of een zeker gedrag wordt vertoond. Daarna wordt gekeken naar een netwerk met een andere structuur of een andere invoer, maar dat lokaal op het eerste netwerk lijkt. De stations in het nieuwe netwerk zullen in het begin van hun executie dezelfde stappen kunnen uitvoeren als in het eerste netwerk

81 5.4 Een bewijs van ondergrens 71 werd gedaan. Het feit dat niet de gehele executie kan worden geplayed (omdat het antwoord in het eerste netwerk onacceptabel is in het tweede), volgt hieruit dat op bepaalde momenten communicatie nodig is. Een aantal aannamen over het systeem en het algoritme komen expliciet in de bewijsvoering terug: Minimum: Het algoritme maakt het proces met kleinste nummer leider. Uniform: De processen kennen de netwerkgrootte niet, dwz. de waarde van n is niet in het algoritme gecodeerd. Dat wil zeggen, dat het replay-argument ook kan worden gebruikt tussen netwerken van verschillende grootte. Willekeurige id s: Elke deelverzameling van N kan de verzameling nummers op de ring zijn, en elke rangschikking is toegestaan. Asynchroon: Het algoritme is asynchroon, dwz. elke interleaving van instructies is mogelijk. Unidirectioneel: De ring kent communicatie in één richting. In het vervolg van deze sectie staat A voor enig electie-algoritme dat aan deze eigenschappen voldoet. Het redeneren over A is natuurlijk moeilijk omdat we niet weten hoe het werkt. Maar wel weten we inmiddels het één en ander over hoe gedistribueerde systemen in het algemeen werken, dus ook algoritme A. Het algoritme heeft voor elke invoer (elk netwerk) een executieboom, waarvan de knopen corresponderen met toestanden van het algoritme. De wortel van de boom is een systeemtoestand waarin alle processen in hun beginstand zijn en alle kanalen leeg. De kinderen van een knoop corresponderen met de mogelijke stappen in die toestand. Sommige processen kunnen geblokkeerd zijn (doordat ze wachten op een bericht dat nog niet ontvangen kan worden), andere kunnen een bericht verzenden en/of ontvangen. Een gedeeltelijke executie is een pad naar een punt in de boom, en een volledige executie gaat naar een punt waar alle processen geblokkeerd zijn. We gaan werken met het begrip mop, dit is een speciale klasse van gedeeltelijke executies van A waarin er nog maar één proces ongeblokkeerd is, en dit ene proces heeft nog niet ontvangen. Definitie 5.5 Een prefix van A is een beginreeks van stappen van de processen volgens A. Een prefix is open als er een kanaal is waarover nog geen ontvangst heeft plaatsgevonden. Een open prefix is maximaal (mop) als er, behalve de ontvangst van berichten over één kanaal, geen stappen mogelijk zijn. Bij het redeneren met en over mop s gebruiken we de eigenschappen van gedistribueerde systemen; ten eerste dat het bevriezen en ontdooien van kanalen of processen in een asynchroon systeem helemaal niet tot verstoring van het algoritme leidt. Lemma 5.6 Elke mop is uitbreidbaar tot een complete executie van A. Bewijs. Omdat A een asynchroon algoritme is moet het correct werken in elke interleaving, dus ook als we voor één kanaal het ontvangen uitstellen tot er geen enkele andere stap meer mogelijk is. Het voltooien van de executie zal doorgaans wel vereisen dat het laatste kanaal ook wordt ontdooid, dus: berichten gaat afgeven. Dat is de strekking van het volgende lemma.

82 72 5 Electie Lemma 5.7 In een open prefix op een ring die geen proces met nummer 0 bevat heeft nog geen enkel proces de toestand leider aangenomen. Bewijs. We gaan gebruiken dat de processen niet weten hoe groot de ring is, en evenmin welke andere identiteiten aanwezig zijn, en deze informatie alleen kunnen krijgen door communicatie, het ontvangen van berichten. Zij P een open prefix op een ring met n processen, waarin proces p nog niet heeft ontvangen en stel (in tegenspraak met het lemma) dat er een proces r is dat zich leider heeft verklaard. Volgens de aanname in het lemma is het nummer van r positief. Maar omdat p nog nooit heeft ontvangen weet p, noch een van de andere processen, wat er direct vòòr p in de ring zit! Bekijk een ring met n + 1 processen, die dezelfde nummers heeft als eerst, maar nu met proces 0 toegevoegd vòòr p. Omdat A uniform is gedragen in die ring alle processen zich precies hetzelfde als eerst, en de prefix P is een mogelijke executie in de uitgebreide ring, inclusief de electie van r. Dit is strijdig met de eigenschap van A dat het kleinste proces wordt geëlecteerd. Je ziet hier hoe we de aanname dat de ring-grootte onbekend is gebruiken: namelijk door een gegeven prefix af te spelen in een andere situatie die echter alleen in de ring-grootte verschilt en voor het overige gelijk is aan die waarin P ontstond. Dit is een zeer gebruikelijke methode in de gedistribueerde algoritmiek. De uitsluiting van een proces met nummer 0 is nodig in het bewijs: dat proces kan zich immers zonder ooit iets te ontvangen tot leider verklaren omdat er geen kleinere nummers bestaan. We bekijken daarom verder alleen executies op ringen waarin de 0 niet voorkomt, maar bedenk dat het algoritme correct moet werken op alle ringen, dus ook als 0 wel voorkomt. Daarom mag de veronderstelde afwezigheid van nummer 0 niet als gegeven in het algoritme geëxploiteerd worden! Corollarium 5.8 Het aantal receives in een open prefix is een ondergrens voor de worst case bericht-complexiteit van A. Bewijs. De prefix is onderdeel van een complete executie, en die executie heeft minstens zoveel ontvangsten als de prefix. Onze verdere redenering met mop s zal een recurrente betrekking opleveren voor een ondergrens op het aantal berichten, en we zullen die eerst maar even oplossen met de substitutiemethode. Lemma 5.9 De oplossing van de recurrente betrekking M(2) = 1 M(2n) = 2.M(n) + n voldoet aan M(n) = 1 2n lg n als n een tweemacht is. Bewijs. We bewijzen dit met inductie naar i voor n = 2 i en nemen de inductiehypothese IH(i): i = 1: De linkerkant, M(2 1 ), is 1. De rechterkant is lg 2 1 is ook 1. M(2 i ) = 1 2 2i lg 2 i.

83 5.4 Een bewijs van ondergrens 73 R 1 f 1 f 2 R 2 e 1 q 1 p1 p 2 q 2 e2 Figuur 5.3: Vorming van een Ring met Grootte 2n. i + 1: Splits links een factor 2 af om de recurrentie en dan de inductiehypothese te gebruiken: M(2 i+1 ) = M(2.2 i ) factor afsplitsen = 2.M(2 i ) + 2 i recurrente betrekking = 2.( 1 2 2i lg 2 i ) + 2 i IH(i) = 2 i (lg 2 i + 1) termen harken = 2 i lg(2 i+1 ) rekenregel lg = 1 2 2i+1. lg 2 i+1 rekenregel macht Definieer nu M(n) als volgt: elke verzameling van n identiteiten kan zo gerangschikt worden, dat er op de ontstane ring een mop bestaat met tenminste M(n) receives. In de rest van deze sectie bewijzen we dat M voldoet aan de recurrente betrekking in lemma 5.9. Voor een ring met 2 processen hoef je alleen maar te bewijzen dat communicatie noodzakelijk is. Lemma 5.10 In elke ring met twee processen heeft A een mop waarin tenminste 1 bericht is ontvangen, ofwel M(2) 1. Bewijs. In een ring met twee processen kan geen proces zich leider maken voordat hij heeft gehoord dat de ander een groter nummer heeft; er is derhalve communicatie nodig. Je kunt een willekeurige executie nemen en die afkappen na de eerste ontvangst, dit geeft je een open prefix met 1 ontvangst. Vervolgens kun je alle stappen schedulen die gedaan kunnen worden zonder een ontvangst in het andere kanaal, je krijgt dan een mop met 1 of meer ontvangsten. In het vervolg van deze sectie gaan we bewijzen dat M(2n) 2 M(n) + n. Laat een verzameling van 2n nummers gegeven zijn en splits deze in twee verzamelingen van n nummers elk. De inductiehypothese zegt dat we van die verzamelingen twee ringen kunnen maken, zeg R 1 en R 2, die elk een mop hebben met tenminste M(n) ontvangsten erin; noem de betreffende prefixen P 1 en P 2 en de bevroren kanalen f 1, resp. f 2 ; zie figuur 5.3. Nu vormen we één ring waar alle 2n namen op voorkomen en wel in de volgorde volgens R 1 en R 2, waarbij de namen van één ring in de andere zijn tussengevoegd op het kanaal dat bevroren was in de prefixen P 1 en P 2 ; zie figuur 5.3. De nieuwe verbindende kanalen noemen

84 74 5 Electie we e 1 en e 2 met eindpunt p 1 en p 2 en beginpunt q 1 en q 2. We gaan bewijzen dat de grote ring R een mop heeft met 2.M(n) + n ontvangsten. Lemma 5.11 De reeks stappen P 1 P 2 is een open prefix van A op R. Bewijs. Door de uniformiteit van het algoritme kan het gedrag van de processen niet veranderen als we het netwerk ergens wijzigen waar ze geen bericht van hebben ontvangen. Door de asynchroniteit mag je de stappen in P 2 vertragen. De open prefix P 1 P 2 is geen mop omdat hij twee kanalen bevat waarover nog niet is ontvangen, en je kunt hem dus op twee manieren uitbreiden tot een mop, namelijk door één van de twee kanalen e 1 of e 2 te ontdooien en de andere dicht te houden. Om te bewijzen dat tenminste één van deze twee mogelijkheden tot n extra receives leidt gaan we kijken wat er gebeurt als het ontdooien van e 1 tot minder dan n extra receives leidt. Lemma 5.12 Zij P 1 P 2 P 3 een mop waarin geen ontvangst over e 2 plaatsvindt en P 3 bevat minder dan n receives. Dan bevat P 3 geen stap van q 2 (in het bijzonder worden geen berichten aan e 2 toegevoegd) en geen overgangen naar de leider toestand. Bewijs. Alle processen in R 2 zitten na de prefix P 1 P 2 te wachten op berichten en kunnen pas iets doen na een ontvangst. Als P 3 dus k receives bevat, kunnen dat alleen stappen van de eerste k processen van R 2 zijn, en q 2 doet derhalve niets. Dat niemand leider kan worden komt omdat de prefix nog steeds open is (lemma 5.7). Na afloop van de reeks stappen P 3 staan alle processen in R 2 dus weer te wachten; er moet door p 1 weer iets worden ontvangen voordat er weer iets gebeurt in R 2. Het gevolg van ontdooiing van e 2 is (inclusief bewijs) hetzelfde. Lemma 5.13 Zij P 1 P 2 P 4 een mop waarin geen ontvangst over e 1 plaatsvindt en P 4 bevat minder dan n receives. Dan bevat P 4 geen stap van q 1 en geen overgangen naar de leider toestand. Voor de voortgang van het algoritme is het foute boel als van zowel lemma 5.12 als Lemma 5.13 de premisse waar is. Het is essentiëel dat het ontdooien van tenminste één kanaal leidt tot het sturen van een bericht over het andere kanaal. Lemma 5.14 Zij P 1 P 2 P 3 een mop waarin geen ontvangst over e 2 plaatsvindt, en P 1 P 2 P 4 een mop waarin geen ontvangst over e 1 plaatsvindt waarbij P 3 en P 4 elk minder dan n receives bevatten. Dan is P 1 P 2 P 3 P 4 een prefix die alle processen in een wachttoestand ongelijk leider brengt. Bewijs. De reeks stappen P 4 is toepasbaar na de reeks P 1 P 2, maar de reeks P 3 verandert van geen enkel proces uit P 4 de toestand. Omdat de acties van een proces alleen afhangen van de eigen toestand en niet van de toestand in een ander deel van de ring is reeks P 4 na de reeks P 1 P 2 P 3 nog steeds toepasbaar. In de betreffende reeksen is geen enkel proces leider geworden (lemmas 5.12 en 5.13) en aan het eind ervan staan alle processen in een wachttoestand. Met andere woorden, A is gedeadlocked zonder dat er een leider is gekozen. Als algoritme A niet deadlockt, zal derhalve het ontdooien van tenminste één van de twee kanalen e 1 of e 2 tot n of meer receives leiden. De prefix P 1 P 2 is daarom tot een mop uit te breiden met tenminste 2.M(n) + n receives, hetgeen bewijst: Lemma 5.15 Elke verzameling van 2n identiteiten (waar de 0 niet in zit) kunnen we zo in een ring rangschikken dat die ring een mop heeft met tenminste 2.M(n) + n ontvangsten.

85 Samenvatting en conclusies 75 Stelling 5.16 Elk asynchroon uniform minimum-electie-algoritme heeft een worst-case berichtcomplexiteit van 1 2n lg n of hoger. Bewijs. Voor het aantal berichten op een ring van n stations hebben we een ondergrens, M(n), bewezen, die voldoet aan de recursie van lemma 5.9. Lemma 5.10 geeft de inductiebasis en lemma s 5.11 tot en met 5.15 geven de inductiestap. Lemma 5.9 lost de recurrentie voor M op, en lemma 5.8 tenslotte claimt dat M inderdaad een ondergrens is op de complexiteit van A. Tenslotte kijken we nog even naar de aannamen die we maakten voor het bewijs. Minimum: De ondergrens geldt ook voor algoritmen die juist het grootste proces kiezen. Met een algoritmische reductie kan het resultaat ook gemakkelijk voor willekeurige electie-algoritmen worden bewezen (opgave 5.20). Uniform: Er is geen betere complexiteit mogelijk als de processen de ring-grootte wel kennen. Het bewijs gebruikt dezelfde methoden (knippen en plakken van deel-executies) maar is een stuk lastiger omdat je alleen maar deelexecuties kunt overzetten naar ringen van gelijke grootte. Willekeurige id s: Als de verzameling namen erg klein is (bv.: de n processen hebben namen uit een verzameling van n + 10 getallen) is een efficiëntere oplossing mogelijk. Tenminste een van de elf kleinste getallen komt voor, en er kan een variant van het Chang- Roberts algoritme worden gebruikt waarin een proces alleen een bericht rondstuurt als zijn nummer tot de elf kleinsten behoort. De complexiteit is dan ongeveer 11n berichten. In een realistische setting ( het netwerk heeft ongeveer een miljoen Pentium processoren elk met een 64-bits nummer ) heb je hier niets aan. Asynchroon: In een synchroon systeem (waar alle processen en kanalen gegarandeerd ongeveer even snel gaan) is een algoritme met O(n) berichten mogelijk. Unidirectioneel: De Ω(n lg n) ondergrens geldt ook voor bidirectionele ringen, maar weer is het bewijs wat lastiger. Samenvatting en conclusies Wanneer er niet via gedeelde objecten, maar door het uitwisselen van berichten wordt gecommuniceerd, is alle communicatie expliciet. De hoeveelheid communicatie is dan exact meetbaar. Dit hoofdstuk behandelt een klassiek probleem op het gebied van procescoördinatie, namelijk het kiezen van een leider uit een collectie a priori gelijke processen. Het electieprobleem is bestudeerd in deze context: de stations hebben unieke identificatienummers, het systeem is asynchroon, de communicatie is beperkt tot een ringvormige communicatiestructuur. Voor het electieprobleem werden de algoritmen van LeLann, Chang-Roberts en Peterson bekeken. Het laatste heeft een lagere gegarandeerde complexiteit, maar in de praktijk is Chang-Roberts ook een goede keuze omdat de gemiddelde complexiteit erg laag is, namelijk O(n lg n) berichten op een ring met n stations. Er werd ook bewezen dat er geen oplossing bestaat die o(n lg n) berichten gebruikt. Hiervoor werd een belangrijke redeneermethode gebruikt, namelijk replay.

86 76 5 Electie Opgaven bij hoofdstuk 5 Opgave 5.1 Geef een voorbeeld waarbij het algoritme van sectie eindigt met meerdere leiders. Wat is het maximale aantal (in een systeem met n processen)? Wat is het minimale aantal? Dezelfde vragen voor het algoritme in Sectie Opgave 5.2 Schrijf een programma voor LeLann s algoritme (sectie 5.3.1). Opgave 5.3 We schreven dat LeLann s algoritme (sectie 5.3.1) essentiëel gebruikt dat kanalen de fifo-eigenschap hebben. Kijk wat er gebeurt als de eigenschap niet geldt, en geef een voorbeeld waar meerdere processen leider worden doordat berichten elkaar inhalen. Kan het algoritme ook zonder leider eindigen of blijven hangen? Opgave 5.4 In het algoritme van Chang en Roberts worden verslagen processen wel in de toestand niet-leider gebracht, maar kunnen het algoritme niet termineren omdat ze niet weten wanneer het laatste bericht langs is geweest. Breid het algoritme uit met een fase waarin de leider het wachten in de andere stations beëindigt door een communicatieronde (die n berichten kost). Opgave 5.5 Is de fifo-eigenschap van kanalen essentieel voor het algoritme van Chang en Roberts (sectie 5.3.2)? Opgave 5.6 Geef voorbeelden van het Chang-Roberts algoritme met 6 processen, waar het aantal berichten (i) 11, (ii) 21, (iii) 17 is. Is elk getal tussen 11 en 21 mogelijk? Opgave 5.7 Hoe weten de niet-leiders in Petersons algoritme (sectie 5.3.3) dat de electie is afgelopen? Opgave 5.8 Electie met Petersons algoritme. (Uit het tentamen van september 2004.) Bewijs, dat in het electie-algoritme van Peterson het aantal berichten dat verstuurd wordt door actieve processen, begrensd is door 4n. Opgave 5.9 Electie: Peterson. (Uit het tentamen van augustus 2000.) In het electie-algoritme van Peterson wordt in elke ronde minstens de helft van de kandidaten uitgeschakeld, tot er nog maar één over is. (a) Hoe wordt dit gedaan, en wat gebeurt er als er nog maar één kandidaat is? (b) Wat is het maximale aantal ronden bij een ring met 16 stations, en wat is het minimale aantal? Geef een beginsituatie waarvoor dit maximale cq. minimale aantal wordt gehaald. (c) Is elk aantal tussen het minimum en het maximum ook mogelijk? Opgave 5.10 Electie, Petersons algoritme. (Uit het tentamen van mei 2002.) Het electiealgoritme van Peterson begint met elk station als kandidaat, en reduceert in achtereenvolgende ronden het aantal kandidaten door elke kandidaat zijn identiteit te laten uitwisselen met de twee buurkandidaten. (a) Hoe komt het dat nooit alle kandidaten worden uitgeschakeld? (b) Hoe komt het dat (als een ronde met meer dan 1 kandidaat begint) tenminste de helft van de kandidaten wordt uitgeschakeld? (c) Hoeveel berichten worden hoogstens verstuurd? Motiveer het antwoord.

87 Opgaven bij hoofdstuk 5 77 Opgave 5.11 Petersons electie. (Uit het tentamen van oktober 2006.) We kijken naar het electie-algoritme van Peterson op een ring met 16 stations, waarvan de identiteiten de getallen 10, 20, 30, , 160 zijn. (a) Geef een rangschikking van de identiteiten waarvoor het aantal ronden minimaal is. Hoeveel ronden neemt het algoritme, en hoeveel berichten worden uitgewisseld? (b) Geef een rangschikking van de identiteiten waarvoor het aantal ronden maximaal is. Hoeveel ronden neemt het algoritme, en hoeveel berichten worden uitgewisseld? (c) Bestaat er een rangschikking waarbij er precies 3 ronden nodig zijn? Motiveer! Opgave 5.12 Berekening op Ring. (Uit het tentamen van februari 2001.) In een ringvormig message-passing netwerk heeft elk station een uniek identificatienummer (id) en een integer invoer x. Geef een (zo efficiënt mogelijk) algoritme dat bepaalt of de som van de invoeren even of oneven is. Beargumenteer waarom je oplossing correct is. Geef de berichtcomplexiteit. Opgave 5.13 Electie: Open Prefix. (Uit het tentamen van mei 2000.) Wat was, in het ondergrensbewijs voor electie algoritmen, (a) een prefix; (b) een open prefix; (c) een maximale open prefix? (d) Bestaan er maximale open prefixen waarin al een proces tot leider is gekozen? Leg uit. (e) Wat verstaan we onder het gebruik van replay in bewijzen over gedistribueerde algoritmen, en waarom is het hiervoor belangrijk dat een prefix open is? Opgave 5.14 Moet de lg n in lemma 5.2 naar boven of naar beneden worden afgerond? Opgave 5.15 Waarom worden in Petersons algoritme nooit alle kandidaten geëlimineerd in een ronde? Opgave 5.16 Schrijf een programma voor Petersons algoritme (Sectie 5.3.3). Opgave 5.17 Geef enkele voorbeelden van mop s in de algoritmen van LeLann en Chang en Roberts. Opgave 5.18 Replay bij Electie. (Uit het tentamen van januari 2006.) Deze opgave bekijkt het probleem van electie onder n stations op een ring, met elk een unieke eigen identiteit. (a) Vertel hoe het electie-algoritme van Chang en Roberts werkt en waarom het correct is. (b) In het bewijs van de n lg n ondergrens voor electie werd de techniek van replay gebruikt. Leg uit wat dit is. (c) Voor het bewijs werd de aanname gemaakt dat de stations de netwerkgrootte (het aantal stations) niet kennen. Waar loopt het bewijs spaak als deze grootte wel in het algoritme bekend is? Opgave 5.19 Bewijs dat de ondergrens op electie ook geldt voor electie-algoritmen die juist het grootste proces tot leider maken. Opgave 5.20 Bewijs dat een Ω(n lg n) ondergrens geldt voor elk electie-algoritme (zelfs waarbij geen restrictie geldt op het proces dat gekozen wordt).

88 78 5 Electie Opgave 5.21 Electie van willekeurig proces. (Uit het tentamen van oktober 2007.) In het dictaat is bewezen dat er een Ω(n lg n) ondergrens is voor het aantal berichten in een electie-algoritme dat het kleinste proces leider maakt (eigenschap Minimum). In deze opgave onderzoeken we, of een efficienter algoritme mogelijk is wanneer we toestaan dat een ander proces leider wordt; dwz., we vervangen Minimum door: Uniciteit: Het algoritme maakt exact één proces leider. (a) Hoe werd in het bewijs de Minimum eigenschap gebruikt? Is dit deelresultaat ook met de Uniciteit eigenschap te bewijzen? (b) Stel dat E een electie-algoritme is (dat niet Minimum maar wel Uniciteit garandeert), en in het slechtste geval f(n) berichten gebruikt. Toon aan, dat er een electie-algoritme E is dat aan Minimum voldoet, en ten hoogste f(n) + 2n berichten gebruikt. (c) Bewijs een Ω(n lg n) ondergrens voor electie-algoritmen. Opgave 5.22 Electie: Ondergrens. (Uit het tentamen van september 2003.) Wat is in het ondergrensbewijs voor electie algoritmen (a) een prefix, (b) een open prefix, (c) een maximale open prefix? (d) In het diktaat werd aangenomen dat A een electie-algoritme is dat het kleinste proces tot leider verkiest. Bewijs, dat de ondergrens van Ω(n log n) berichten ook geldt voor electiealgoritmen waarbij een willekeurig proces tot leider mag worden gekozen.

89 Hoofdstuk 6 Fouttolerantie en consensus Het thema complexiteit, behandeld in hoofdstuk 5, is centraal in de studie van algoritmen: bij alle berekeningsmodellen is de vraag naar het beroep op resource van belang. Grotere problemen bij het ontwerp van gedistribueerde applicaties liggen echter niet bij de complexiteit van oplossingen, maar bij het fout-bestendig krijgen. Een realiteit in computersystemen is dat er componenten kunnen uitvallen en dat dit op de meest ongelukkige momenten kan gebeuren. Dat maakt van programmatuur een heel andere eigenschap van belang, namelijk de robuustheid of tolerantie tegen storingen in componenten. De uitgebreide algoritmische studies en onderzoeken naar robuustheid zijn vrijwel allemaal op gedistribueerde systemen gericht en vormen binnen het gedistribueerde onderzoek de belangrijkste lijn. In dit hoofdstuk kijken we naar het soort moeilijkheden dat robuustheid oplevert en we bewijzen de onmogelijkheid van het consensus probleem. Dit hoofdstuk en hoofdstuk 8 bekijken robuustheid in systemen waar met berichten wordt gecommuniceerd; ook in hoofdstuk 7 komt dit aan de orde. In de hoofdstukken over wachtvrij rekenen (10 tot 12) wordt robuustheid behandeld voor systemen die met gedeelde objecten communiceren. 6.1 Fouttolerant programmeren Computers in een netwerk kunnen down gaan en threads van een multithreaded applicatie kunnen door het operating system worden gekilled. Zelfs als componenten op zich heel betrouwbaar zijn wordt de kans op zo n storing groter naarmate het systeem meer componenten bevat. Wat we willen is dat in zo n geval de overgebleven processen of threads kunnen overleven en de applicatie afmaken. Nu is het niet moeilijk om fouten af te vangen op zo n manier dat het bijna altijd werkt. Als voorbeeld hiervan kijken we naar een constructie in de programmeertaal Modula-3. Het probleem is dat een thread bijvoorbeeld een semafoor kan pakken en crashen voor het vrijgeven ervan. Deze constructie is vrij gebruikelijk en een crash in de kritieke sectie kan de gehele applicatie blokkeren: s.p Kritieke Sectie s.v 79

90 80 6 Fouttolerantie en consensus Kader 6.1: Besturing van de Space Shuttle. Het primaire computersysteem van de Space Shuttle bestaat uit vier identieke computers, die allemaal exact dezelfde berekening uitvoeren. In geval van een storing in een van deze computers wordt de besturing automatisch overgenomen door de andere computers [SG84]. Automatische omschakeling is nodig omdat tijdens de start en landing het vliegtuig zal vergaan als er langer dan 400ms niet goed gestuurd wordt. Hoewel replicatie een voor de hand liggende oplossing lijkt om de betrouwbaarheid te vergroten, is het ontwerp van algoritmen om een cluster van (mogelijkerwijs gebrekige) computers te laten samenwerken verre van eenvoudig. Dit wordt geïllustreerd door het uitstel van de eerste lancering van de Shuttle (van 8 naar 10 april 1981), dat te wijten was aan een fout in de software die juist deze taak vervulde. De shuttle heeft een secundair systeem dat bestaat uit een vijfde computer als hete reserve ; deze computer voert ook dezelfde berekeningen uit en door een handmatige druk op een rode knop wordt de besturing op deze computer overgeschakeld. Tenslotte is er een koude reserve, een computer die niet is ingeschakeld. Mochten alle werkende computers het begeven, dan kunnen de astronauten op een rustig moment de zesde computer uitpakken, opstarten en de besturing erop overschakelen. (Hierin is s een semafoor en P en V zijn de pak- en vrijoperatie.) Daarom vond men voor de programmeertaal Modula-3 iets uit waarmee een thread aan het besturingssysteem een soort testament kon nalaten waarin stond wat er moest gebeuren als de thread in een bepaald fragment zou crashen. Omgezet naar de modernere syntax van Java zou de kritieke sectie er dan zo uit komen te zien: s.p try { Kritieke Sectie } exception { s.v } s.v Een heel aardig idee, want bij een crash in de kritieke sectie opent het besturingssysteem het testament en voert de vergeten s.v operatie uit. Maar waterdicht is het niet: een thread kan immers direct na de s.p operatie, dus voor de try sectie, crashen en dan val je naast het vangnet. Dan maar iets eerder je testament maken: try { s.p ; Kritieke Sectie } exception { s.v } s.v Het is nu onmogelijk dat een crashende thread de applicatie doet hangen. Maar nu is er een ander risiko: er kan een s.v gegeven worden zonder dat een s.p is voltooid (ga na!), waardoor de safety-eigenschappen van de applicatie gevaar lopen.

91 6.1 Fouttolerant programmeren 81 Kader 6.2: Commit/abort voor transacties In een gedistribueerde database moet een transactie worden doorgevoerd in alle sites, of in geen. Nadat een transactie bij alle sites bekend is gemaakt, bepaalt elke site of die transactie in die site uitgevoerd kan en mag worden. Vervolgens moeten alle sites dezelfde beslissing nemen, of de transactie wordt uitgevoerd (commit) of niet (abort). De beslissing voor een commit mag alleen worden genomen als de transactie in alle sites doorgevoerd kan worden. Populaire protocollen zoals two-phase commit of three-phase commit (plaatje) gebruiken time-outs om te bepalen dat een site is gecrasht (zijn dus niet asynchroon) of kunnen blijven hangen als de coördinator crasht. Men zocht naar een asynchroon algoritme dat het crashen van een willekeurig proces kon overleven en dan in de andere processen een abort zou genereren en de data-locks vrijgeven. Maar vond het niet; de publicatie van Fischer, Lynch en Paterson maakte aan de zoektocht een eind Foutmodellen Behalve dat threads kunnen crashen zijn andere soorten van storingen mogelijk. Bijvoorbeeld dat de thread één of meer instructies overslaat (omissie) en dan weer als normaal handelt. Of fouten waarbij een proces onjuiste waarden schrijft of berichten stuurt met foute inhoud; we spreken van Byzantijns falen. Dit soort storingen kunnen optreden bij ernstige hardwareof softwarefouten, maar ook wanneer in het systeem één of meerdere stations gehackt zijn en door aanvallers ongemerkt van andere software voorzien (attacks). In het laatste geval hebben we te maken met gehaaide tegenstanders met snode bedoelingen, en het behoud van integriteit van de applicatie is een zeer delicate kwestie. Bescherming tegen Byzantijns falen en aanvallen behoort tot het terrein van de gedistribueerde algoritmen [Tel00] en cryptografie [Tel02]. Hier beperken we ons tot crash fouten, die echter wel op elk willekeurig moment kunnen toeslaan. In dit hoofdstuk nemen we aan dat er geen gebruik wordt gemaakt van operating system primitieven die aan threads/processen informatie kunnen geven over gecrashte collega s; dat komt in hoofdstuk 8. Er wordt dus ook geen gebruik gemaakt van time-outs; de processen moeten asynchroon zijn en dan is het onmogelijk om een gecrasht proces te onderscheiden van een dat gewoon erg traag is Beslisproblemen Een vrij algemene benadering van de problematiek krijgen we met het model van beslisproblemen. In deze klasse van problemen moet elk correct (dwz., niet-crashend) proces een uitvoer genereren, en er gelden restricties op de waarden van die uitvoer. De eisen die aan de output worden gesteld zijn steeds een voortgangs-, en een consistentie-eis. Een voortgangs-eis vraagt dat alle correcte processen uiteindelijk iets berekenen; drie vormen worden onderscheiden:

92 82 6 Fouttolerantie en consensus (Deterministische) Terminatie. In elke berekening van het algoritme neemt elk correct proces een beslissing. Deze beslissing moet onherroepbaar zijn, dwz., elk proces schrijft (slechts eenmaal) een waarde naar de speciale uitvoervariabele. Probabilistische terminatie, of convergentie. De kans dat elk correct proces een beslissing neemt, dwz., een waarde naar de uitvoervariabele schrijft, is 1. Het verschil tussen deterministische en probabilistische terminatie is verduidelijkt aan de hand van programma Impliciete terminatie, of stabilisatie. Elke berekening van het algoritme is eindig en na afloop hebben alle processen een waarde in hun uitvoervariabele staan. Het verschil met terminatie is dat de processen niet weten of het huidige resultaat het uiteindelijke is, of in de toekomst nog veranderd gaat worden. Beslissingen van een proces zijn dus herroepbaar; dit wordt verder duidelijk in sectie De consistentie-eis stelt vast welke waarden naar de uitvoer geschreven mogen worden. Bij commit/abort zal deze eis luiden: alle beslissingen zijn gelijk, en bij electie: één proces beslist op leider en de anderen op niet-leider. Je kunt vrij ingewikkelde probleemstellingen als kloksynchronisatie en renaming als beslisprobleem modelleren. Bij het synchroniseren van hardware-klokken van verschillende computers is de invoer de huidige klokwaarde, en de uitvoerwaarden van de diverse stations is een getal dat in de buurt van het gemiddelde over de invoeren moet liggen, en de uitvoerwaarden moeten dicht bij elkaar liggen. Bij renaming wil je de computers in je netwerk kleine identificatienummers toekennen. Naast de besproken eisen op het gedrag van een programma kunnen er eisen op de fouttolerantie (type en aantal fouten) worden gesteld Voorbeeld: flexibele electie Een voorbeeld waarin een aantal typische, en fundamentele, aspecten van fouttolerante applicaties naar voren komen is programma Doorgaans is er een (kleine) verzameling van n processen die volledig verbonden zijn, dwz., elk proces kan aan elk van de andere berichten sturen. Het getal n staat voor het totale aantal opgestarte processen, en verandert dus bij een crash niet: correcte en gecrashte processen tellen beide mee. 2. De t in het programma is een veiligheidsparameter, de robuustheid, en geeft het maximale aantal crashes waartegen het programma gewapend is. 3. Het zogenaamde shouten (stuur een bericht aan alle stations) is acceptabel (qua kosten) omdat het aantal stations meestal niet groot is. 4. Na de shout wacht een proces op de berichten van de anderen, waarbij er echter op niet meer dan n t processen gewacht wordt om blokkering te voorkomen. Tijdens het wachten weet een proces namelijk nooit of de zender van het bericht gecrasht of gewoon traag is. 5. En vervolgens hebben we een probleem met de interpretatie van de berichten, omdat (als er minder dan t processen gecrasht zijn) de diverse processen een verschillende deelverzameling van n t berichten kunnen hebben gehoord!

93 6.1 Fouttolerant programmeren 83 int n =... t =... set S // Totaal aantal processen, constant // Robuustheid, constant // Processen die p kent // Stuur identiteit aan alle processen forall proces q!= p { send <p> to q } // Verzamel n-t identiteiten S = {p} // Proces p kent zichzelf while ( S < n-t ) { receive <q> ; S = S + {q} } // Neem een beslissing if ( rang(p,s) <= t+1 ) { decide(leider) } else { decide(niet-leider) } Programma 6.3: Flexibele electie (voor proces p). Programma 6.3 is een voorbeeld waarin één zo n ronde wordt gebruikt. De rang van een element p in een verzameling S geeft aan welke positie p heeft in de ordening van S. Dus rang(p,s) <= t+1 betekent dat p tot de laagste t + 1 getallen in de verzameling S van proces p zit. Het standaard electie-probleem (waar exact één proces in leider-toestand komt) is niet op te lossen als crashes mogelijk zijn; we formuleren de volgende zwakkere eisen: Terminatie: In elke executie waar hoogstens t processen crashen beslist elk correct proces op leider of op niet-leider. Flexibele Electie: Het aantal leider beslissingen is tenminste 1 en ten hoogste 1 + 2t. Stelling 6.1 Programma 6.3 is een asynchrone oplossing voor flexibele electie. Bewijs. Daar er hoogstens t processen crashen zijn er minstens n t die niet crashen, en die maken hun zend-stappen af. Dit impliceert dat een correct proces tenminste n t 1 berichten kan ontvangen, en door de while-lus heen komt. Daarom voldoet programma 6.3 aan terminatie. De flexibele-electie-eis is probleemspecifiek en we laten het bewijs als opgave 6.2 omdat het hier de bedoeling was een algemeen mechanisme te behandelen. In het bewijs moet men er rekening mee houden, dat de diverse correcte processen hun beslissing op verschillende verzamelingen S kunnen baseren. In dit voorbeeld hebben we aan één ronde van communicatie genoeg, maar de wat ingewikkelder voorbeelden hebben er meerdere achtereen. Er blijven na afloop van het algoritme nog berichten over in de kanalen; dit is bij crash-robuuste algoritmen onvermijdelijk en wordt als zodanig geaccepteerd. Het betekent wel dat er ontvang-deamons actief moeten blijven die berichten van afgesloten programma s in onvangst nemen en weggooien. Ook, dat als zo n algoritme meerdere keren na elkaar wordt uitgevoerd, de berichten informatie moeten bevatten om ze van verschillende executies uit elkaar te houden.

94 84 6 Fouttolerantie en consensus Let er op dat t niet staat voor het aantal fouten dat tijdens een executie optreedt, en zelfs niet voor het aantal fouten dat tijdens een executie kan optreden. Deze parameter is de robuustheid van het programma, en geeft aan hoeveel crashes het programma kan overleven. Het werkelijke aantal crashes kan hoger zijn, en dan blokkeert het programma misschien maar dit valt buiten de specificatie. Het werkelijke aantal crashes zal er in verreweg de meeste gevallen onder liggen. Bij het bewijzen van de correctheid van het programma mogen we altijd uitgaan van een aantal crashes begrensd door t. 6.2 Consensus-algoritmen In het vervolg van dit hoofdstuk gaat het feitelijk niet om één probleem, maar we hebben het over een klasse van problemen die gemeenschappelijk hebben, dat alle stations hetzelfde antwoord moeten berekenen. Zulke problemen zijn er natuurlijk te over: commit/abort (kader 6.2), een chatbox, waar elke deelnemer dezelfde berichten moet zien, of een algoritme dat een conditie in het netwerk test en het resultaat in alle stations oplevert. Een algoritme waarin gelijkheid van beslissingen wordt geëist noemen we een consensus-algoritme. De consistentie die ervoor geldt omvat deze eis: Overeenstemming: De genomen beslissingen zijn gelijk. Bij (flexibele) electie mogen leider en niet-leider beslissingen in één executie voorkomen; dit is daarom geen consensus-probleem. Bekijk nu het kloksynchronisatieprobleem. Synchronisatie wordt altijd uitgevoerd voordat de klokken elkaar te veel ontlopen, en laten we aannemen dat bij het begin van de synchronisatie het verschil tussen de snelste en de langzaamste klok ten hoogste een minuut bedraagt. Men kan eisen dat de stations beslissen op een gemeenschappelijke waarde, die tussen de hoogste en de laagste invoer in moet zitten. In dat geval is er sprake van een consensus-probleem en in sectie 6.3 wordt bewezen dat geen synchronisatieprogramma aan deze eis kan voldoen. Daarom wordt er een zwakkere eis gesteld, bijvoorbeeld dat de processen moeten beslissen op waarden die elkaar ten hoogste een seconde mogen ontlopen. Deze eis impliceert geen overeenstemming en is met een vrij eenvoudig programma te realiseren (kader 6.4). De volgende subsecties bespreken allemaal een eenvoudig programma dat aan overeenstemming voldoet Een triviale oplossing Dit programma (uitgevoerd in elk proces) voldoet aan overeenstemming en terminatie: decide(0) Hier is decide de statement waarmee een proces een waarde naar zijn uitvoer schrijft; je mag die maar één keer uitvoeren. Alle beslissingen zijn gelijk (namelijk 0 in alle executies) en alle processen beslissen tenzij ze voor hun eerste instructie al crashen. Dit programma voldoet aan de tot nu toe geformuleerde eisen, maar erg nuttig is het waarschijnlijk niet (dit hangt natuurlijk van de verdere eisen op de uitvoer af). Een programma dat niet echt iets uitrekent maar altijd hetzelfde antwoord geeft noemen we triviaal.

95 6.2 Consensus-algoritmen 85 Kader 6.4: Kloksynchronisatie door herhaalde convergentie // Er geldt x p x q < D Stuur x aan alle stations Ontvang n t klokwaarden in verzameling S x = gemiddelde(s) // Er geldt x p x q < D.t/(n t) Waarden die alleen p heeft Als synchronisatie wordt gestart ontlopen de klokken elkaar maximaal δ: x p x q < δ en het doel is een precisie van ϵ te bereiken. In een t n t ver- ronde wordt de precisie met een factor beterd. Elk station stuurt zijn klokwaarde aan alle anderen, wacht tot n t waarden ontvangen zijn, en neemt van de n t ontvangen waarden het gemiddelde. S p S q n 2t waarden gemeenschappelijk Waarden die alleen q heeft Omdat de stations p en q elk ten hoogste t waarden niet ontvangen, hebben ze tenminste n 2t waarden gemeenschappelijk. De t waarden die p alleen heeft verschillen elk minder dan D van de t waarden die q alleen heeft. Het gemiddelde over S p en S q verschilt daarom minder dan (D t) n t. Hoe kun je nu bij een gegeven begin- en eindprecisie het benodigde aantal ronden uitrekenen? De replicated server Voor de volgende oplossing nemen we aan dat er twee servers zijn, p en q, die beschikken over dezelfde waarde, dwz., de invoerbits x p en x q zijn gelijk; programma 6.5 zorgt dat alle processen die waarde horen. De servers beslissen elk op hun eigen bit, de aanname stelt dat die gelijk zijn. Elke client krijgt bits toegestuurd en de eerste die aankomt wordt genomen, maar voor de waarde maakt dat natuurlijk niet uit. Ook als een server crasht krijgt elke client tenminste één bit, en zelfs als de crashende server al bits had uitgestuurd, die bij sommige clients als eerste arriveren geldt overeenstemming. Voor server p : forall client c { send <xp> to c } decide (xp) Voor server q : forall client c { send <xq> to c } decide (xq) Voor client i : receive y // Mag van p of van q zijn decide (y) Programma 6.5: Broadcast door replica servers.

96 86 6 Fouttolerantie en consensus Voor de server: uitvoer = x if (x==1) { forall client c { send 1 to c }} Voor elke client: uitvoer = 0 receive 1 // t doet er niet toe van wie uitvoer = 1 forall client c { send 1 to c } Programma 6.6: Een zwakke broadcast door een server. Aan terminatie is voldaan zolang er hoogstens één server crasht. Terminatie eist slechts dat elk proces beslist, over de berichten die na dit algoritme nog on-ontvangen in de kanalen rondzwerven wordt in de eis niets gezegd! Dit programma voldoet ook weer aan de geformuleerde eisen, maar voor de goede werking moet er wel voor gezorgd worden dat de servers a priori overeenstemmen. Niet alle mogelijke combinaties van toegestane invoeren zijn als invoercombinatie toegestaan De zwakke broadcast Met een broadcast bedoelen we een consensus-probleem waarbij het de bedoeling is dat elk proces de invoer van een bepaald proces (meestal de generaal maar hier de server genaamd) te weten komt. In aanvulling op overeenstemming eisen we namelijk: Broadcast: Als de server niet crasht, is elke uitvoer gelijk aan de invoer x van de server. Stelling 6.2 Programma 6.6 is een asynchroon stabiliserend broadcast algoritme. Bewijs. Merk als eerste op dat elke executie van het algoritme eindig veel stappen heeft: er zijn hoogstens n zendacties voor elk proces mogelijk. Als de server invoer 0 heeft heeft elk proces na afloop uitvoer gelijk 0; immers, de server zendt niets, en alle clients blijven na uitvoer = 0 wachten met uitvoer 0. Als de server invoer 1 heeft en correct is heeft elk correct proces na afloop uitvoer 1. Immers, de server zendt een 1 naar elke client dus elke client kan iets ontvangen, en zet de uitvoer op 1. Alleen door vroegtijdig te crashen kan een proces aan het eind nog 0 hebben. Het lastigste geval is wanneer de server 1 heeft en crasht. Misschien heeft hij al wat berichten verstuurd waardoor sommige clients hun uitvoer op 1 zetten, maar daarvan kunnen er natuurlijk ook weer crashen voordat ze aan iedereen berichten hebben gestuurd! Maar we kunnen inzien: als er een correct proces aan het eind uitvoer gelijk 1 heeft, dan hebben na afloop alle correcte processen uitvoer 1. Immers, zij p een correct proces met eindwaarde 1 en q ook een correct proces; na de uitvoer op 1 te zetten heeft p nog een bericht aan q gestuurd, en q moet dus een keer hebben ontvangen. We zien dat elk correct proces aan het eind dezelfde waarde heeft, en dat deze waarde gelijk is aan de invoer van de server mits deze niet crasht. Het programma illustreert het verschil tussen terminatie en stabilisatie (impliciete terminatie). We weten dat aan het eind alle uitvoeren gelijk zijn, maar een proces weet niet wanneer zijn uitvoer definitief is geworden! Immers, een proces met uitvoer 0 wacht op ontvangst van een bericht (dat hem naar 1 doet switchen) maar zolang het wacht weet het niet

97 6.3 Onmogelijkheid van consensus 87 Kader 6.7: Dijkstra- en Knuth-prijs voor Nancy Lynch. De Amerikaanse ACM (Association for Computing Machinery) heeft in april 2007 de Knuth-prijs toegekend aan Professor Nancy Lynch vanwege haar vele en belangrijke bijdragen aan de theorie van gedistribueerd rekenen. Het was de eerste keer dat deze prijs, die sinds 1996 bestaat, aan een vrouw werd toegekend. Haar werk (veel met co-auteurs uitgevoerd) omvat een precieze modellering van gedistribueerde systemen en het analyseren ervan, en deze inzichten hebben ook andere deelgebieden van de informatica beïnvloed, zoals database ontwerp en security. De inzichten die ze ontwikkelde over de mogelijkheden en onmogelijkheden van consensus zijn altijd haar bekendste wetenschappelijke bijdrage gebleven, en hiervoor werd ze in 2001 al onderscheiden met de Dijkstra-prijs. De prijzenkast in huize Lynch is inmiddels goed gevuld, want in 2006 mocht ze in het CWI in Amsterdam een gedeelde Van Wijngaarden-prijs in ontvangst nemen. In juli 2007 werd de Dijkstra-prijs toegekend aan het paper Consensus in the presence of partial synchrony van Cynthia Dwork, Nancy Lynch en Larry Stockmeyer [DLS88]. of zo n bericht nog komt, dan wel 0 de eindwaarde is. Er is dus geen sprake van een beslissing die op een bepaald moment wordt genomen, dus aan terminatie is niet voldaan De sterke broadcast Je zou dit algoritme natuurlijk expliciet kunnen laten termineren met timers: je moet dan eerst bepalen hoe lang het voor een client maximaal kan duren voor hij een bericht ontvangt. Als de client weet op welke tijd de broadcast in de server begint, kan met een time-out het wachten op een bericht in programma 6.6 beëindigd worden. Het resulterende programma is een sterke broadcast (voldoet aan terminatie ipv. stabilisatie), maar is niet langer asynchroon omdat je eisen nodig hebt op de tijdsduur van instructies in een machine. De berekening van de te gebruiken tijdconstanten is nog niet zo gemakkelijk (zie opgave 6.8). 6.3 Onmogelijkheid van consensus Deze sectie bespreekt het baanbrekende werk van Michael Fischer, Nancy Lynch en Michael Paterson [FLP85], die in 1982 bewezen dat er geen asynchrone consensus-algoritmen bestaan. In de jaren daarna werd dit negatief geformuleerde resultaat op een constructieve manier gebruikt: de formulering van wat onmogelijk is, werd tegelijk een randvoorwaarde die systemen omschreef waarin consensus wel opgelost kon worden. We besteden in sectie aandacht aan de formulering van het consensus-probleem, resulterend in vijf eigenschappen waaraan een gewenste oplossing voldoet. In sectie worden deze eisen gebruikt om eigenschappen van de berekeningsboom van zo n oplossing af

98 88 6 Fouttolerantie en consensus te leiden, waaruit in sectie een tegenspraak tussen de vijf eisen kan worden bewezen. Een discussie volgt in sectie Eisen op een consensus-oplossing De verdere discussie veronderstelt een programma A, waarin processen eenmaal kunnen beslissen op waarden 0 en 1; we eisen overeenstemming. Overeenstemming: Binnen een executie zijn alle genomen beslissingen gelijk. Het programma van sectie (en de variant die altijd 1 geeft) is waarschijnlijk nergens echt bruikbaar voor; een bruikbaar programma rekent iets uit, maar we willen ons niet vastleggen op wat dat kan zijn. De aanname dat een programma niet-triviaal is, sluit alle oplossingen uit die altijd op 0, of juist die altijd op 1 beslissen, maar doet dit niet op grond van de programmatekst maar op grond van het uitvoergedrag. Niet-trivialiteit: Er is een executie waarin op 0 wordt beslist en een executie waarin op 1 wordt beslist. Doorgaans wordt niet-trivialiteit geïmpliceerd door de aanvullende eis op de genomen beslissingen. Let op, dat 0 en 1 voorkomen in verschillende executies, want binnen één executie zijn de genomen beslissingen steeds gelijk (wegens Overeenstemming). In programma 6.5 is er de noodzaak van a priori overeenstemming tussen de beide servers; hoewel invoer 0 op zich voor p toegestaan is, en invoer 1 voor q, is de combinatie van deze twee invoeren niet toegestaan. De volgende eis sluit zulke programma s, waarin de overeenstemming als het ware al in de startconfiguratie vastligt, uit. Invoercombinaties: Elke combinatie van per proces toegestane initiële toestanden, is een toegestane initiële configuratie. De formulering van de terminatie concentreert zich op het nemen van beslissingen (in programma 6.6 gebeurt dat niet, al heeft elke executie een eindig aantal stappen) en niet op eindigheid van de executie. Om de formulering te vergemakkelijken nemen we juist aan, dat elk correct proces een oneindig aantal stappen kan doen; elk algoritme A is zo te beschrijven dat dit kan, bijvoorbeeld door te veronderstellen dat een proces na het beslissen altijd nog loze stappen kan doen. Definitie 6.3 Een run is een 1-crash run als tenminste n 1 processen oneindig veel stappen doen in die run. Een run is fair als elk bericht dat aan een correct proces (dwz., een proces dat oneindig veel stappen doet) is verstuurd ook wordt ontvangen. Terminatie: In elke 1-crash fair run beslissen alle correcte processen. Merk op dat het de Terminatie-eis is die de robuustheid tegen crashes impliceert. Het programma uit sectie tenslotte is niet asynchroon. Asynchroniteit: Het handelen van een proces is onafhankelijk van het tijdstip. Fischer, Lynch en Paterson [FLP85] hebben bewezen dat geen algoritme bestaat dat de vijf genoemde wenselijke eigenschappen combineert. Stelling 6.4 (Fischer, Lynch, Paterson, 1985.) Er bestaat geen gedistribueerd algoritme dat de vijf eigenschappen Overeenstemming, Niet-trivialiteit, Invoercombinaties, Terminatie en Asynchroniteit heeft.

99 6.3 Onmogelijkheid van consensus 89 Voor elk van deze vijf eisen bestaat er een heel eenvoudig algoritme dat niet aan die eis, maar wel aan de andere vier voldoet. Dit impliceert, dat in het bewijs alle vijf de eigenschappen moeten worden gebruikt! In de onderstaande tekst wordt zoveel mogelijk aangegeven waar de getrokken conclusies van deze vijf eisen afhangen Modelvorming Deze sectie relateert de executieboom van A aan de bijbehorende configuraties, de toestanden die het algoritme bereikt na een reeks van stappen. Beschrijving van A. Algoritme A bestaat uit n (n 2) processen, waarbij proces p (o.a.) een input register x p en een output register y p heeft. De waarde van y p is 0, 1 of b (voor blanco) en dit register mag slechts éénmaal geschreven worden (wanneer p beslist). Een configuratie van het protocol bestaat uit de lokale toestand van elk proces en de berichtverzameling van alle berichten die in die configuratie onderweg zijn. Voor elk bericht is er slechts één proces dat dit bericht kan ontvangen. In een initiële configuratie zijn alle y p registers gelijk aan b, en er zijn geen berichten onderweg. We veronderstellen dat alle invoer voor p is gerepresenteerd door x p, dus de initiële configuratie is volledig vastgelegd door de waarden van de x p registers (voor alle p). Als steeds redeneren we met de executieboom van een veronderstelde oplossing voor het probleem. De knopen van deze graaf zijn de configuraties van het algoritme en ze zijn gerangschikt aan de hand van de stappen die het algoritme kan doen. Enkele technische randopmerkingen zijn hier op z n plaats. Ten eerste, omdat er verschillende beginconfiguraties kunnen zijn (afhankelijk van de invoer) is de graaf niet echt een boom maar een woud; toch blijven we de term executieboom voor deze graaf gebruiken. Ten tweede, omdat verschillende reeksen stappen tot dezelfde configuratie kunnen leiden kunnen twee knopen in de graaf dezelfde configuratie beschrijven. De Terminatie-eis op de oplossing dwingt een eigenschap van de executieboom af. Dat op een willekeurig moment (configuratie C) een willekeurig proces (p) kan crashen vertaalt zich in deze eigenschap: Propositie 6.5 Voor elke configuratie C in de executieboom en elk proces p, bestaat er een reeks stappen vanuit C waarin p geen stappen doet, en waarin elk proces behalve p dat in C nog niet beslist heeft, een beslissing neemt. Dit lijkt erg logisch tot men zich af begint te vragen: Hoe zit het met een configuratie C waarin al een proces, zeg q, gecrasht is? Omdat het protocol slechts één crash hoeft te overleven zou je nu voor de werking van het algoritme mogen aannemen dat andere stations niet meer crashen. Echter, in ons model is elke knoop in de executieboom te bereiken in een eindige reeks van stappen vanaf een wortel. Vanwege Asynchroniteit kun je niet spreken van een gecrasht proces binnen een eindige reeks! Proces q kan best een hele tijd niet aan de executiereeks hebben deelgenomen, maar de asynchroniteit staat toe dat tussen twee stappen van q een willekeurig groot aantal stappen van andere processen zit. Dus vanuit elk punt kan elk proces zijn executie hervatten en weer stappen gaan doen. Berichten, stappen, commuteren. Nu beschrijven we de stappen en configuraties in meer detail voor processen met communicatie door message passing. Een stap in de berekening wordt genomen door één proces en bestaat uit (1) het al dan niet ontvangen van één bericht

100 90 6 Fouttolerantie en consensus (er wordt geen bericht ontvangen bij een interne stap); (2) een verandering van de toestand van het proces; (3) het verzenden van nul of meer berichten. De stap wordt genoteerd als e = (p, m), waar p het proces is dat de stap neemt, en m het bericht dat ontvangen wordt (m = als er geen bericht wordt ontvangen in de stap). De stap e = (p, m) is toepasbaar in configuratie C als m = of m is onderweg (naar p) in configuratie C. (Wanneer FIFO kanalen worden gemodelleerd, moet m ook nog voorin het kanaal staan; dit heeft op het verdere betoog geen invloed.) De configuratie die ontstaat na de toepassing van e wordt genoteerd als e(c) en wordt als volgt uit C gevonden: 1. Als m dan is m uit de berichtverzameling verdwenen. 2. De toestand van p is veranderd. 3. Er kunnen berichten toegevoegd zijn aan de berichtverzameling. De verandering in p s toestand, en de verzonden berichten, zijn bepaald door het algoritme A. Ze hangen niet af van de tijd wegens Asynchroniteit en niet van de toestand van andere processen of de inhoud van de kanalen wegens de localiteit van p s kennis. Uit de werking van een stap en de karakterisering van toepasbaarheid volgt, dat een nietgekozen stappen toepasbaar blijven. Propositie 6.6 Zij C een configuratie met daarin toepasbare stappen e en f. Dan is e toepasbaar in f(c). Het effect van stap e = (p, m) kan in f(c) anders zijn dan in C, maar alleen als stap f de toestand van p heeft gewijzigd. Uit de werking van de stap volgt dat toepasbare stappen in verschillende processen commuteren [Tel00, Thm. 2.19]. Lemma 6.7 Zij C een configuratie met daarin toepasbare stappen e en f voor verschillende processen p en q. Dan is f toepasbaar in e(c) en e toepasbaar in f(c), en e(fc)) = f(e(c)). Het bewijs is hoofdzakelijk een kwestie van alle toestandsovergangen heel precies uit te schrijven. Reeksen van stappen. Een reeks σ = (e 1, e 2,..., e k ) van stappen is toepasbaar in een configuratie C 0 als e 1 toepasbaar is in C 0, e 2 is toepasbaar in C 1 = e 1 (C 0 ), etc., en e k is toepasbaar in C k 1. Als σ toepasbaar is in C 0 is σ(c 0 ) = e k (e k 1 (..(e 1 (C 0 ))..)). Configuratie D is bereikbaar uit C als er een reeks σ is zodanig dat σ(c) = D. Herhaalde toepassing van Propositie 6.6 geeft: Propositie 6.8 Zij C een configuratie met een toepasbare stap e en een toepasbare reeks van stappen σ die e niet bevat. Dan is e toepasbaar in σ(c). Herhaalde toepassing van Lemma 6.7 geeft: Lemma 6.9 Laat de reeksen σ 1 en σ 2 van stappen toepasbaar zijn in configuratie C, waarbij er geen proces is dat zowel een stap doet in σ 1 als in σ 2. Dan is σ 2 toepasbaar in σ 1 (C), en σ 1 toepasbaar in σ 2 (C), en σ 2 (σ 1 (C)) = σ 1 (σ 2 (C)).

101 6.3 Onmogelijkheid van consensus 91 D 0 D 1 Initiële conf.. 0-bes. 1-bes... 0-bes C... 1-bes Besliste conf. Niettrivialiteit + 1-crash tolerantie Bivalente init. conf. Figuur 6.8: De bivalente initiële configuratie. Klassificatie van configuraties: Valentie. In elke complete berekening komen beslissingen voor (Terminatie), en in de executieboom komen zowel 0- als 1-beslissingen voor (Niet-trivialiteit), maar in een berekening slechts één soort (Overeenstemming)! Door non-determinisme zijn vanuit een configuratie in het algemeen meerdere manieren om de executie voort te zetten. Op zijn minst door de mogelijkheid dat verschillende processen die een stap kunnen doen in verschillende volgorden aan de beurt kunnen komen, maar ook kunnen er voor één proces meerdere toepasbare stappen zijn. Vanuit een configuratie kunnen zo meerdere ontwikkelingen mogelijk zijn, en de verzameling mogelijkheden kan door het toepassen van een stap worden beperkt maar niet uitgebreid. Immers, de mogelijkheden in een punt zijn de vereniging van de mogelijkheden in de verschillende deelbomen. Voor het consensus-probleem is een belangrijk aspect van de mogelijke ontwikkelingen samen te vatten in: welke beslissingen zijn er vanuit deze configuratie nog mogelijk. Dit aspect van de configuratie wordt samengevat in de valentie. Een configuratie heet 0-beslist als er in deze configuratie een p is met y p = 0, en 1-beslist als er een p is met y p = 1. Wegens Overeenstemming zijn er geen configuraties die zowel 0- als 1-beslist zijn. Uit Terminatie volgt, dat er vanuit elke bereikbare configuratie een (0- of 1-) besliste configuratie bereikbaar is. Een configuratie C heet 0-valent als alle besliste configuraties die uit C bereikbaar zijn, 0-beslist zijn, en 1-valent als alle besliste configuraties die uit C bereikbaar zijn 1-beslist zijn. Een configuratie C heet bivalent als zowel 0- als 1-besliste configuraties uit C bereikbaar zijn en univalent als hij 0- of 1-valent is. Opgave 6.11 werkt dit begrip verder uit. In een univalente configuratie hangt de beslissing zo te zeggen in de lucht : misschien heeft nog geen enkel proces de beslissing genomen (door het schrijven van de uitvoer) maar globaal is de keuze voor een bepaalde waarde al onontkoombaar. In een bivalente configuratie zijn beide mogelijkheden nog open Bivalentie in runs Uit de eigenschappen van A zal nu worden afgeleid, dat A een oneindig lange fair run heeft waarin geen beslissing wordt genomen; hetgeen strijdig is met Terminatie. Hiertoe bewijzen we dat er een bivalente startconfiguratie is (Lemma 6.10) en daarna, dat bivalente configuraties willekeurig lang door bivalente kunnen worden gevolgd (Lemma 6.11).

102 92 6 Fouttolerantie en consensus De bivalente startconfiguratie. De Niet-trivialiteit zegt dat de executiegraaf zowel 0- als 1-besliste configuraties bevat, maar die zitten mogelijk in verschillende deelbomen, dwz., zijn mogelijk bij verschillende startconfiguraties. De fouttolerantie en Invoercombinaties worden gebruikt om te bewijzen dat deze twee soorten ook onder één startconfiguratie voorkomen; zie figuur 6.8. Lemma 6.10 Algoritme A heeft een bivalente initiële configuratie. Bewijs. (1) Uit Niet-trivialiteit volgt dat er initiële configuraties D 0 en D 1 zijn, zodanig dat vanuit D 0 een 0-besliste configuratie, en vanuit D 1 een 1-besliste configuratie bereikbaar is. (2) Als D 0 = D 1, dan is deze configuratie een bivalente initiële configuratie. Laat verder D 0 D 1. (3) Configuraties D 0 en D 1 verschillen mogelijk in meerdere processen, maar er zijn initiële configuraties C 0 en C 1, zodanig dat vanuit C 0 een 0-besliste configuratie, en vanuit C 1 een 1-besliste configuratie bereikbaar is, en C 0 en C 1 verschillen slechts in de input van één proces. (4) Om (3) te bewijzen, beschouw een serie initiële configuraties, beginnend met D 0, en eindigend met D 1. Elke volgende configuratie wordt verkregen door van één proces, waarvan de input ongelijk is aan de input die dat proces in D 1 heeft, de input te veranderen in de input van dat proces in D 1. Wegens Invoercombinaties is elke configuratie in deze reeks een toegestane startconfiguratie van A. Omdat er vanuit elke initiële configuratie een besliste configuratie bereikbaar is, vanuit D 0 een 0-besliste, en vanuit D 1 een 1-besliste, kunnen C 0 en C 1 als in (3) gekozen worden als twee opeenvolgende configuraties in deze serie. (5) Noem het proces waarin C 0 en C 1 verschillen p, en zij r een 1-crash run vanuit configuratie C 0 waarin p geen enkele stap neemt, en B de besliste configuratie die in die run wordt bereikt. (6) Als B 1-beslist is, is C 0 bivalent omdat we al weten dat vanuit C 0 een 0-besliste configuratie bereikbaar is. (7) Als B 0-beslist is, is C 1 bivalent. Omdat C 0 en C 1 slechts in p verschillen en p geen stap doet in r, is r ook toepasbaar in C 1 en leidt naar dezelfde 0-besliste configuratie B. We weten al dat vanuit C 1 een 1-besliste configuratie bereikbaar is. Bivalente opvolgers. Vervolgens gaan we aantonen dat de overgang van een bivalente naar een univalente configuratie willekeurig lang kan worden uitgesteld. Het is hiervoor niet voldoende, te laten zien dat elke bivalente configuratie een bivalent kind heeft. Daaruit volgt wel, dat de berekeningsboom een oneindig lang pad bevat, maar net als in figuur 1.12 zou dit oneindige pad kunnen corresponderen met een unfair run. Wat we moeten aantonen is, dat er een oneindige niet-beslissende run is die ook nog fair is. Daarom nemen we in het volgende lemma niet alleen een bivalente configuratie C aan, maar ook een willekeurige in C toepasbare stap e. Er wordt bewezen dat C een bivalente afstammeling heeft, waarin e is toegepast. Achter het bewijs zit dit idee: stel dat de uitvoering van stap e in alle gevallen het systeem forceert univalent te worden, dan is er ergens één proces p dat de gehele uitkomst beslist, namelijk door e al dan niet voor een andere stap e uit te voeren. Als op dat punt echter p niets meer doet zit het systeem vast: Terminatie dwingt het systeem te beslissen, maar door Asynchroniteit is crashen in een eindige prefix niet te onderscheiden van een zeer traag proces. Ergo, als p lang genoeg stil houdt om het systeem tot een beslissing op i te forceren, kan p vervolgens inconsistentie afdwingen door de stap te doen die naar een ī beslissing leidt.

103 6.3 Onmogelijkheid van consensus 93 Lemma 6.11 Zij C een bivalente configuratie en e = (p, m) toepasbaar in C. Zij C de verzameling configuraties die uit C bereikbaar zijn zonder e toe te passen, en D de verzameling configuraties die verkregen worden door e toe te passen op een configuratie in C: Dan is er een bivalente configuratie in D. C = {σ(c) e komt niet voor in σ}, D = {e(e) E C en e is toepasbaar in E}. Bewijs. (1) Stap e is toepasbaar in alle configuraties in C wegens propositie 6.8. (2) Er zijn configuraties G 0 en G 1 in C, zodanig dat vanuit e(g 0 ) een 0-besliste, en vanuit e(g 1 ) een 1-besliste configuratie bereikbaar is. (3) Om G 0 en G 1 te vinden, merk op dat C bivalent is, dus er zijn een 0-besliste configuratie B 0 en een 1-besliste configuratie B 1 bereikbaar uit C. Als B i C, kies G i = B i (want e(b i ) is nog steeds i-beslist). Als B i C, kies voor G i de configuratie in de berekening die van C naar B i leidt, van waaruit e is toegepast. (4) Als G 0 = G 1, dan is e(g 0 ) een bivalente configuratie in D en zijn we klaar. Veronderstel G 0 G 1. Beschouw de opeenvolgende configuraties in berekeningen die van C naar G 0 en G 1 leiden, en noem twee van deze configuraties buren als de ene configuratie volgt op de andere. Omdat er vanuit e(g 0 ) een 0-besliste configuratie bereikbaar is en vanuit e(g 1 ) een 1-besliste, volgt dat er 1. onder de beschouwde configuraties een C is waarvoor e(c ) bivalent is; of 2. twee buren C 0 en C 1 zijn waarvoor e(c 0 ) 0-valent en e(c 1 ) 1-valent is. In het eerste geval is e(c ) een bivalente configuratie in D. (5) Voor het tweede geval zullen we een tegenspraak afleiden. Er geldt C 1 = e (C 0 ) of C 0 = e (C 1 ), voor een stap e = (p, m ). Veronderstel C 1 = e (C 0 ) (het andere geval gaat analoog). Laat D 0 = e(c 0 ) en D 1 = e(c 1 ). Daar e(e (C 0 )) = D 1 een 1-valente configuratie is, en e (e(c 0 )) = e (D 0 ) een 0-valente configuratie, geldt e(e (C 0 )) e (e(c 0 )). Met lemma 6.7 volgt nu dat p = p. (6) In configuratie C 0 is de beslissing over de uiteindelijke output geheel in handen van proces p, immers p kan het systeem naar de 0-valente configuratie D 0 of naar de 1-valente configuratie D 1 voeren. (7) Hieruit volgt dat er niet beslist kan worden als p in deze configuratie stopt. Zij namelijk σ een reeks van stappen, die toepasbaar is in C 0, en waarin p geen stap doet, en laat A = σ(c 0 ). Door toepassing van lemma 6.9 volgt dat e(a) = e(σ(c 0 )) = σ(e(c 0 )) = σ(d 0 ) is 0-valent, en e(e (A)) = e(e (σ(c 0 )) = σ(e(e (C 0 ))) = σ(d 1 ) is 1-valent, en dus is A bivalent. (8) Er is dus een 1-crash run (namelijk elke run die het systeem in configuratie C 0 voert en daarna geen stap meer doet voor p) waarin niet wordt beslist. Dit is een tegenspraak met Terminatie Bewijs en discussie Het bewijs van stelling 6.4 kan nu worden voltooid. Bewijs. Zij A een protocol; er kan een fair run r worden geconstrueerd waarin geen proces beslist. Zij A 0 een bivalente initiële configuratie van A (lemma 6.10). In A i, kies voor e i de stap die het langst niet is toegepast in het pad dat naar A i leidt. Laat A i+1 een bivalente

104 94 6 Fouttolerantie en consensus configuratie zijn waarin e i is toegepast (die bestaat volgens lemma 6.11). Verleng de run met de stappen die van A i naar A i+1 leiden. De geconstrueerde oneindige run blijft bivalent en er wordt dus geen beslissing in genomen. De run is tevens fair omdat geen enkele stap oneindig lang wordt uitgesteld. Bij elke stap zijn er namelijk maar eindig veel oudere stappen en dat impliceert dat elke stap die ooit toepasbaar wordt, een keer aan de beurt komt om als e i gekozen te worden (als hij niet al eerder wordt uitgevoerd in de constructie). De run is fair, maar geen proces beslist; dit is in tegenspraak met terminatie. Hoewel geformuleerd als een negatief resultaat, wordt de stelling van Fischer, Lynch en Paterson meestal geïnterpreteerd als een overzicht van de manieren waarop consunsus-problemen juist wel kunnen worden aangepakt. Wil men het probleem kunnen oplossen, dan zal het systeem aan een van de geformuleerde eisen niet voldoen en hier zijn enkele voorbeelden van succesvolle aanpakken. 1. Randomisering (hoofdstuk 7): De Terminatie-eis kan worden verzwakt door het bestaan van oneindige executies toe te staan. Populair gezegd worden deze oneindige executies onschadelijk gemaakt door aannamen over de waarschijnlijkheid van executies, en een bewijs dat de oneindige executies met kans 0 voorkomen. 2. Synchroniteit: Onder een aanname van synchroniteit verstaan we eigenlijk elke beperking van de mogelijke volgorde van stappen. Zo kan bijvoorbeeld de relatieve snelheid van processen als volgt begrensd zijn: tussen twee opeenvolgende stappen van proces p zitten ten hoogste 3 stappen van proces q. Deze aanname heeft invloed op de toepasbaarheid van stappen en maakt propositie 6.6 ongeldig. Kijk hiervoor naar een situatie waarin q net twee stappen heeft gedaan en kan kiezen tussen toepasbare stappen e en f. In f(c) is stap e niet toepasbaar omdat eerst p een stap moet doen voordat q weer in actie mag komen; ga na hoe het bewijs van lemma 6.11 als een kaartenhuis in elkaar stort. De FIFO-aanname voor kanalen is ook een vorm van synchroniteit, maar onvoldoende om consensus mogelijk te maken. 3. Fout-detectors (hoofdstuk 8): Ook het gebruik van fout-detectors maakt propositie 6.6 ongeldig. Veronderstel dat e een stap van p is en f een stap waarin q het crashen van p detecteert. In een configuratie C kunnen beide stappen toepasbaar zijn, maar het is een eigenschap van een accurate detector dat p geen stappen meer doet nadat q zijn falen geeft geconstateerd. Samenvatting en conclusies Bij het ontwerp van gedistribueerde applicaties is robuustheid tegen het optreden van storingen in één of meerdere stations uiterst belangrijk. In een robuust algoritme mag een station nooit wachten op ontvangst van een bericht van een bepaald ander station; immers een crash van het tweede zou ook het eerste blokkeren. Daarom wordt informatie meestal uitgewisseld in ronden, waarin een station eerst informatie zendt aan alle stations, en vervolgens de informatie van n t stations ontvangt. Men dient er nu rekening mee te houden dat de diverse stations verschillend verzamelingen berichten hebben ontvangen.

105 Opgaven bij hoofdstuk 6 95 Veel probleemstellingen zijn te modelleren als een beslisprobleem: de stations moeten aan het eind van het protocol een onherroepelijke beslissing nemen over hun uitkomst. Binnen de beslisproblemen onderscheiden we een belangrijke klasse, namelijk de problemen waarbij de beslissingen van de stations aan elkaar gelijk moeten zijn. Die klasse van problemen wordt consensus-problemen genoemd. Uit de literatuur is bekend dat, wanneer in een systeem een consensus-probleem kan worden opgelost, oplossingen voor veel andere problemen hieruit kunnen worden afgeleid. Fischer, Lynch en Paterson bewezen in 1985 dat er geen asynchroon en deterministisch consensusalgoritme bestaat dat bestand is tegen een enkele crash. Opgaven bij hoofdstuk 6 Opgave 6.1 Verzin voor programma 6.3 een executie (met n = 8 en t = 2) waarin slechts 1, en een andere waarin 5 processen leider worden. Opgave 6.2 Bewijs dat programma 6.3 aan Flexibele Electie voldoet. Bedenk dat de processen na afloop verschillende waarden van de verzameling S kunnen hebben! Opgave 6.3 Flexibele Electie. (Uit het tentamen van mei 2003.) Hier is Prog 6.3 voor flexibele electie in een systeem met n stations waarvan er hoogstens t kunnen crashen: forall proces q!= p do send <p> to q S = {p} while S < n-t do { receive <q> ; S = S+{q} } if ( rang (p,s) <= t+1 ) then { decide(leider) } else { decide(niet-leider) } (a) Waarom probeert een station slechts n t, en niet alle identiteiten te ontvangen? (b) Laat een executie zien voor de waarden n = 5 en t = 2, waarin geen crashes voorkomen en er stations p 1 en p 2 zijn, die na afloop verschillende verzamelingen S hebben. (c) Wat is, voor n = 15 en t = 3, bij afwezigheid van crashes, het maximale en minimale aantal stations dat leider kan beslissen? Opgave 6.4 Consensus-eisen. (Uit het tentamen van februari 2001.) (a) Hoe luiden (bij consensus) de eisen Overeenstemming en Niet-trivialiteit? (b) De eigenschap van Geldigheid luidt: de uitvoer is gelijk aan de invoer van tenminste één proces. Bewijs dat Geldigheid impliceert Niet-trivialiteit. Opgave 6.5 Onmogelijkheid van Consensus. (Uit het tentamen van november 2003.) Fischer, Lynch, en Paterson bewezen in 1985 dat er geen asynchroon, deterministisch 1-crashrobuust consensusprotocol bestaat. (a) Welke specificaties van zo n protocol zijn het uitgangspunt in het bewijs van FLP? Beschouw het volgende beslisprobleem, Ondergrens. Invoer van elk proces is een getal tussen 1 en 6. Uitvoer van proces i is een getal y i met de eigenschap dat tenminste de helft van de invoeren grotergelijk y i is;

106 96 6 Fouttolerantie en consensus er is een proces j waarvan de invoerwaarde y i is. (b) Is Ondergrens een consensusprobleem en waarom (of waarom niet)? (c) Geef een algoritme voor Ondergrens. Bewijs dat het correct is. Waarom sluit de Stelling van FLP het bestaan van jouw algoritme niet uit? Opgave 6.6 Geen-extreem. (Uit het tentamen van november 2005.) In 1985 bewezen Fischer, Lynch, en Paterson (FLP85) de onmogelijkheid van algoritmen voor consensus. (a) Wat zijn de eigenschappen van zo n algoritme die tezamen tot een tegenspraak leiden? (b) Geef van het volgende probleem aan, of er sprake is van een Consensus probleem (in de zin van FLP85); t is de robuustheid: Geen-extreem: Elk station p heeft een integer invoer x p en genereert (als het niet crasht) een integer uitvoer y p. Deze y p mag niet tot de t kleinste of de t grootste invoerwaarden behoren. Formeel: bij elke p zijn er tenminste t stations q waarvoor x q y p, en tenminste t stations r waarvoor geldt x r y p. (c) Als je antwoord op (b) JA luidt, bewijs dan dat de specificatie van Geen-extreem Niettrivialiteit en Overeenstemming impliceert. Als je antwoord op (b) NEE luidt, probeer dan een asynchroon t-robuust algoritme voor Geen-extreem te geven. Welke waarde kan t maximaal hebben? Opgave 6.7 Fouttolerante algoritmen. (Uit het tentamen van februari 2002.) We kijken naar een asynchroon consensus algoritme voor n processen, met fouttolerantie t. In elke ronde doet elk proces (1) een shout van zijn waarde (0 of 1); (2) wacht dan tot n t stemmen zijn ontvangen; (3) neemt als nieuwe waarde de meerderheid van de ontvangen stemmen. (a) Waarom kunnen in het algoritme maar n t stemmen in aanmerking worden genomen, ook als er geen processen zijn gecrasht? (b) Waarom kunnen de processen in stap 3 verschillende waarden als meerderheid vinden? (c) Hoeveel nullen of enen moeten er aan het begin van de ronde zijn opdat is gegarandeerd dat elk proces dezelfde waarde berekent in stap 3? Opgave 6.8 Na hoeveel tijd kan het wachten in programma 6.6 veilig beëindigd worden? Welke aannamen moet je allemaal maken? Opgave 6.9 Bewijs Lemma 6.7. Opgave 6.10 Consensusprobleem. (Uit het tentamen van mei 2002.) Hoe luiden de Terminatie, Overeenstemming, en Niet-Trivialiteit eisen waaraan een oplossing voor het Consensus probleem moet voldoen? Hoe luidt de stelling van Fischer, Lynch en Paterson? Opgave 6.11 Bewijs: als een bivalente configuratie alleen univalente kinderen heeft, zijn daar zowel 0-valente als 1-valente bij. Opgave 6.12 Zoek voor elk van de vier oplossingen uit sectie 6.2 uit, waarom het bewijs van Fischer, Lynch en Paterson niet van toepassing is.

107 Opgaven bij hoofdstuk 6 97 Opgave 6.13 Robuuste convergentie. (Uit het tentamen van november 2004.) Beschouw een systeem met n processen, waarin elk proces p een vijf-bits integer invoergetal x p [ ] heeft. We willen dat elk proces zijn invoer vervangt door een getal d p, dat ligt binnen het bereik van de oorspronkelijke invoeren, maar zo dat de spreiding van de waarden kleiner is; hierbij verlangen we een robuustheid van t = n/4. In dit algoritme is floor de functie die een getal naar beneden afrondt op een integerwaarde. shout xp collect (n-t) waarden z = gemiddelde van de n-t ontvangen waarden decide floor(z) (a) Bewijs dat voor de besliswaarde d p van p geldt: er zijn q, r met x q d p x r. (b) Bewijs dat voor elke correcte p, q geldt: d p d q 12. (c) Is het mogelijk de waarden d p weer als invoer van hetzelfde algoritme te gebruiken om zo de spreiding verder te verkleinen? Is een uiteindelijke spreiding van 0 (dus gelijkheid van de uitvoerwaarden) te garanderen? Opgave 6.14 Ongewenste kleine kleur. (Uit het tentamen van november 2006.) In deze opgave ontwerp je een algoritme voor een systeem van n processen, dat bestand is tegen het crashen van t ervan. De invoer x p van proces p is driewaardig: een kleur uit de verzameling {W, G, Z} (wit, grijs, zwart). Een kleur die t keer of minder voorkomt noemen we een kleine kleur. (Motivatie hiervoor, die verder niets met de opgave te maken heeft: zo n kleur kan door crashes geheel uitgewist worden.) (a) Geef een algoritme dat voldoet aan Terminatie en voor elk correct proces p een niet-kleine kleur als uitvoer bepaalt: Niet-kleine kleur: De uitvoerwaarde d p van p komt voor als invoerwaarde x q van tenminste t + 1 processen q. (b) Hoe groot mag t zijn in je oplossing? Je collega poneert deze stelling: Onder de aanname dat er in de invoer geen kleine kleuren voorkomen (dwz., waarden die t keer of minder voorkomen) is het mogelijk, Consensus te bereiken met een deterministisch algoritme. (c) Bespreek de mogelijkheid om je algoritme uit (a) te gebruiken als een eerste fase in een deterministisch Consensus-algoritme. Opgave 6.15 Robuuste Kloksynchronisatie. (Uit het tentamen van juli 2011.) In een gedistribueerd systeem met 4 processoren willen we de klokken gesynchroniseerd houden, zelfs als een proces uitvalt. Door de fysieke onnauwkeurigheid van de klokken, kan na ongeveer een etmaal het verschil oplopen tot 100ms, wat het maximaal toelaatbare is. Er geldt dus c p c q 100 en we willen de afwijking terugbrengen tot 5ms: c p c q 5. (a) We beginnen met een shout/collect ronde, waarna elk proces het gemiddelde van de 3 ontvangen waarden neemt. Wat is het maximale verschil tussen twee van zulke gemiddelden? (b) Hoeveel ronden zijn nodig om te garanderen dat het verschil hoogstens 5 is? (c) Wat is nodig om te zorgen dat de processors gelijke waarden berekenen voor hun klokken? Opgave 6.16 De betrouwbare thermometer. (Uit het tentamen van november 2007.) Om warmte te meten, zelfs als er (tot t) thermometers kapot gaan, voeren we een temperatuursensor n-voudig uit. De sensors meten elk hun temperatuur, en overleggen dan om ieder een

108 98 6 Fouttolerantie en consensus temperatuur te kiezen waarmee ze verder rekenen. Defecten in een sensor kunnen leiden tot een crash tijdens de overleg-fase, maar ook tot het aangeven van een volstrekt verkeerde temperatuur. De goede sensors geven ongeveer gelijke waarden aan, maar vanwege tolerantie, niet exact dezelfde waarde. Doel is, dat de sensors uiteindelijk waarden aangeven die niet meer dan twee graden van elkaar verschillen, en bovendien liggen binnen het interval van waarden dat door correcte sensoren is aangegeven. Dit leidt tot het volgende beslisprobleem, Middeling: Input: Proces p heeft invoer x p, integer tussen 0 en 100; Terminatie: Als t of minder stations crashen, beslist elk correct station p op een integer u p ; Output-verschil: Voor beslissende p en q geldt: u p u q 2; Output-legaal: Voor output u p zijn er tenminste t + 1 stations q met x q u p en tenminste t + 1 met x q u p. (a) Bespreek of Middeling een Consensus-probleem is in de zin van Fischer, Lynch en Paterson. Ga met name in op de Overeenstemming, Invoercombinaties en Niet-trivialiteit. We laten de sensors met hun invoer een shout en collect (van n t waarden) doen. Van de ontvangen waarden gooit proces p de hoogste t weg, en de laagste t, waarna y p gekozen wordt als het (afgeronde) gemiddelde van de overige waarden. (b) Waarom moet gelden n > 3t? (c) Voldoen de berekende waarden y p aan de eis van Output-verschil? Voldoen ze aan Output-legaal? In ronde twee doen de stations een shout/collect met y, en vervangen hun y door het gemiddelde van de n t ontvangen waarden. (d) Bewijs, dat na ronde 2, voor (niet-crashende) stations p en q geldt: y p y q 50. (e) Is het mogelijk, Middeling te doen met een deterministisch, asynchroon algoritme? Opgave 6.17 Atomic Broadcast. (Uit het tentamen van november 2006.) Schiper [Sch06] zegt dat de moeilijkheid van het implementeren van actieve replicatie vrijwel geheel verborgen zit in de implementatie van de Atomic Broadcast. (a) Wat is het verschil tussen passieve en actieve replicatie? (b) Waar past Atomic Broadcast in een gelaagde netwerkarchitectuur? (c) Geef de specificatie van Atomic Broadcast. (d) Schets een algoritme dat Atomic Broadcast implementeert, gebruikmakend van Consensus. (Je mag dus veronderstellen dat een werkend Consensus primitief aanwezig is.)

109 Hoofdstuk 7 Randomisering Dit hoofdstuk bekijkt het belang van gerandomiseerde algoritmen in gedistribueerde systemen. De eerste sectie (7.1) dient om het een en ander aan terminologie te introduceren, bijvoorbeeld de verschillende soorten gerandomiseerde algoritmen die we onderscheiden. We denken aan gerandomiseerde algoritmen als programma s waar rand()-statements inzitten, maar de formele karakterisering is hier anders. Net zoals we in sectie 6.3 de triviale programma s niet werden uitgesloten op grond van hun programmatekst maar op grond van hun uitvoergedrag, onderscheiden gerandomiseerde programma s zich door hun specificaties. Verder laat sectie 7.1 een eerste voorbeeld zien dat aantoont dat randomisering ook daadwerkelijk de mogelijkheden van gedistribueerde systemen uitbreidt. Nog wel op een wat kunstmatig probleem misschien, maar juist door dit soort simpele voorbeelden kun je bij ingewikkelder problemen de werking van randomisering los gaan zien van de vele probleemdetails. In sectie 7.2 wordt aangetoond dat je met gerandomiseerde algoritmen een lagere complexiteit kunt halen. Dit wordt ook echt aangetoond, doordat een probleem wordt behandeld inclusief een ondergrens voor deterministische algoritmen. Deze werkwijze (ondergrens in één model plus een lagere bovengrens in een ander model) wordt vaak toegepast om de kracht van verschillende modellen met elkaar te vergelijken. Het behandelde probleem is een geïdealiseerde variant van de kanaaltoegang in netwerken als ethernet (waar ook randomisering in wordt gebruikt). Sectie 7.3 tenslotte zet randomisering vanuit de eerdere theoretische behandeling rechtstreeks op het menu van het gedistribueerde systeemontwerp. Met randomisering kun je namelijk oplossingen geven voor het consensus probleem, waarvan in sectie 6.3 werd bewezen dat een deterministische oplossing onmogelijk is. 7.1 Anonieme electie Om een inzicht te krijgen in de diverse soorten gerandomiseerde algoritmen die er zijn (Las Vegas, Monte Carlo, Sherwood) beginnen we onze studie met een heel elementair probleem: electie in een netwerk van 2 identieke stations. Een dergelijk netwerk, waar de stations noch door verschillende namen, noch door verschillende programma s van elkaar worden onderscheiden noemen we anoniem. 99

110 100 7 Randomisering Het probleem en deterministische onmogelijkheid Twee stations p en q communiceren met elkaar door bericht-uitwisseling via kanalen M pq en M qp. We wensen beide stations met hetzelfde programma uit te rusten, dat toestanden leider en niet-leider kent, zò dat aan de volgende twee eisen is voldaan: 1. Terminatie. Elke berekening van het gecombineerde systeem is eindig. 2. Verkiezing. In elke eindtoestand van het systeem is 1 station in toestand leider en 1 station in toestand niet-leider. De eis verkiezing is een voorbeeld van een partiële correctheidseis: dit betekent dat er in een eindtoestand een bepaalde conditie moet gelden, maar de eis dwingt op zich niet af dat die ook wordt bereikt (dat staat in de andere eis, terminatie). Een algoritme dat aan terminatie (en één of andere eis van partiële correctheid) voldoet noemen we deterministisch. De anonimiteit wordt impliciet vermeld, namelijk door het weglaten van een aanname die we in hoofdstuk 5 maakten: dat elk station een unieke naam heeft (die door het programma gelezen kan worden). We kunnen dus niet p en q met elkaar vergelijken en sinds een jaar of twintig bewijzen we de onoplosbaarheid van dit soort problemen met symmetrie-argumenten. Stelling 7.1 Er bestaat geen deterministisch electie-algoritme voor een anoniem netwerk van 2 processen. Bewijs. Een configuratie of globale toestand van het systeem is de combinatie van de toestanden van p en q en de kanalen: (s p, s q, Q pq, Q qp ). We noemen de configuratie symmetrisch als s p = s q en Q pq = Q qp ; het blijkt dat we een executie kunnen maken die telkens na 2 stappen weer in een symmetrische configuratie zit. Initieel zijn beide kanalen leeg en beide stations in de starttoestand dus de configuratie als geheel is symmetrisch. Zij nu (s, s, Q 0, Q 0 ) een symmetrische configuratie en stel dat er voor station p een stap mogelijk is. De toestand van p verandert naar t, en eventueel wordt er een bericht gelezen uit M qp en eventueel één toegevoegd aan M pq ; noem de nieuwe configuratie (t, s, Q 1, Q 2 ). Maar, omdat (s, s, Q 0, Q 0 ) symmetrisch is, en p en q hetzelfde programma draaien, is dezelfde stap ook voor q mogelijk: ook q kan de toestand naar t veranderen. Als p iets ontving in de stap (Q 2 = Q 0 m), dan zal q in deze stap ook bericht m uit M pq verwijderen. Als p iets zond in de stap (Q 1 = Q 0 + n), dan zal q in deze stap ook bericht n aan M qp toevoegen. Er ontstaat na de stap van q dus een symmetrische configuratie (t, t, Q 3, Q 3 ). Deze constructie geeft ons een executie die na elk even aantal stappen een symmetrische configuratie heeft. Als de executie oneindig lang is, voldoet het algoritme niet aan terminatie. Als de executie eindig lang is voldoet het niet aan verkiezing: in een symmetrische eindconfiguratie zijn de stations in dezelfde toestand. Symmetrie-argumenten kun je ook op allerlei andere netwerken toepassen. Je kunt bijvoorbeeld gemakkelijk bewijzen dat je niet kunt verkiezen op een ringnetwerk met meer dan 2 stations, of op een ander netwerk waar één of andere vorm van symmetrie in zit Een terminerende oplossing: Monte Carlo Programma 7.2 gebruikt het vijfmaal trekken van een random bit en voldoet aan de volgende, zwakkere eisen: 1. Terminatie. Elke berekening van het gecombineerde systeem is eindig.

111 7.1 Anonieme electie 101 Kader 7.1: Coördinatie van Lego-robots. Machines in netwerken en processen in een systeem hebben altijd een uniek identificatienummer, waardoor de interesse voor anonieme electie altijd theoretisch is gebleven. Echter, met het goedkoper worden van hardware is er ook een tendens om allerlei producten van processors te voorzien, die simpel moeten zijn maar wel met elkaar communiceren. Van Lego Mindstorm kun je allerlei programmeerbare robots bouwen. Meestal is na één robot (met controller) je zakgeld op, maar toch komen er soms meerdere robots bijeen, bijvoorbeeld om samen blikjes op te ruimen (foto). Dan moet er eentje tot coördinator van het karwei worden benoemd, en omdat de controllers geen serienummer hebben is er sprake van anonieme electie. Het programmeertutorial stelt de volgende oplossing voor. Elke robot begint een random tijd (gekozen uit ) te wachten. Wordt binnen die tijd een bericht ontvangen, dan wordt de robot slaaf, anders zendt hij zelf een bericht en wordt master. De mogelijkheid dat meerdere robots tegelijk zenden wordt afgedaan als rather unlikely. Zie 2. Verkiezing met kans 0, De kans dat in een bereikte eindtoestand van het systeem 1 station in toestand leider is en 1 station in toestand niet-leider, is tenminste 0, Het algoritme gebruikt randomisering (het trekken van een getal uit het bereik ) en we hebben de specificatie van partiële correctheid verzwakt door toe te staan dat met een positieve kans (de faalkans, 1/32 in dit geval) een onbruikbare eindtoestand wordt bereikt. Je kunt de faalkans wel willekeurig klein maken door mijngetal uit een groter bereik te keizen, mijngetal = random(0..31) send mijngetal receive haargetal if (mijngetal < haargetal) { toestand = leider } else if (mijngetal > haargetal) { toestand = niet-leider } Programma 7.2: Monte-Carlo-electie.

112 102 7 Randomisering toestand = onbeslist while (toestand == onbeslist) { mijngetal = random(0..31) send mijngetal receive haargetal if (mijngetal < haargetal) { toestand = leider } else if (mijngetal > haargetal) { toestand = niet-leider } } Programma 7.3: Las-Vegas-electie. maar hoe groot het bereik ook is, de faalkans blijft altijd positief. Definitie 7.2 Een Monte-Carlo-algoritme is een algoritme dat in elke executie termineert en met positieve kans een gewenste eindtoestand bereikt Een probabilistisch terminerende oplossing: Las Vegas Programma 7.3 pakt de zaak anders aan: het trekken van getallen wordt net zo lang herhaald tot de stations een keer verschillende getallen hebben getrokken. Een station springt uit de lus omdat de getallen verschillend zijn, en als dat aan één kant gebeurt, gebeurt het aan de andere kant ook en er is dan precies 1 leider en 1 niet-leider. Programma 7.3 voldoet derhalve aan verkiezing. Maar hoe zit het met terminatie? Slecht, want men zal snel het bestaan van oneindige executies kunnen inzien: een oneindige reeks ronden, waarbij de twee stations telkens gelijke nummers trekken. Men zal denken dat dit in de praktijk nooit voorkomt, dit is misschien zo, maar bestaan doet zo n executie toch wel. Wel kunnen we zeggen dat in de kansverdelingsruimte dit soort akelige verschijnselen een dichtheid van 0 hebben (al ga ik op de precieze wiskundige fundering van deze uitspraak liever niet in). Al met al voldoet programma 7.3 aan de volgende specificatie: 1. Terminatie met kans 1. De kans dat een berekening van het systeem eindigt is Verkiezing. In een eindtoestand van het systeem is 1 station in toestand leider en 1 station in toestand niet-leider. Mocht iemand nog denken dat terminatie met kans 1 hetzelfde is als terminatie: de combinatie van stelling 7.1 en programma 7.3 toont aan dat terminatie met kans 1 echt een zwakkere eigenschap is. Definitie 7.3 Een Las-Vegas-algoritme is een algoritme dat termineert met kans 1 en partieel correct is. In de praktijk heb je van oneindige executies van Las Vegas algoritmen nooit last. Oneindige executies zijn nog nooit voorgekomen! Al was het maar omdat er pas eindig lang computers bestaan. Omdat er op elk moment in de toekomst ook pas eindig lang computers

113 7.1 Anonieme electie 103 Kader 7.4: Het orgel van de Notre Dame Las-Vegas-algoritmen voldoen in de praktijk meestal prima omdat de executies die voorkomen altijd eindig zijn. Er is echter een fundamenteel nadeel van al deze algoritmen: er is geen garantie op de tijd die het algoritme neemt. Immers, voor elk getal k is er een positieve kans dat het algoritme meer dan k stappen nodig heeft (ga na aan de hand van programma 7.3). In een groot kerkorgel is de verbinding tussen het klavier en de orgelpijpen meestal elektrisch en als het orgel erg groot en erg modern is kan dit een digitaal lokaal netwerk zijn. Een onzekere vertraging tussen het indrukken van een toets en het klinken van de toon is natuurlijk onacceptabel. Daarom kon bij de revisie van de tractie van het kerkorgel in de Notre Dame de Paris in 1992 niet gekozen worden voor ethernetverbindingen (sectie 7.2). Het orgel werd door Societé Synaptel voorzien van een IBM token ring. Links en rechts van de speeltafel werd een computermonitor ingebouwd. bestaan zullen hebben, zullen oneindige executies in de toekomst ook nooit voorgekomen zijn. Oneindige executies bestaan dus nooit in het verleden, misschien in de toekomst, maar met kans 0 zit het er niet erg in Terminerend met faalkans 0 Het zal de lezer zijn opgevallen dat in ons voorbeeld het Monte-Carlo-algoritme een positieve faalkans had, terwijl bij het Les-Vegas-algoritme de kans op een oneindige executie slechts 0 was. Dingen die mogelijk zijn maar kans 0 hebben, komen alleen voor in de wereld van oneindige rijen; daarom is een Monte-Carlo-algoritme met faalkans 0 gewoon deterministisch correct. Stelling 7.4 Als A een Monte-Carlo-algoritme is dat met kans 1 een zekere postconditie bereikt, dan wordt die postconditie in elke executie bereikt. Bewijs. Alle executies van een Monte-Carlo-algoritme zijn eindig en in een eindige executie worden ook maar eindig veel random bits getrokken. Als er een executie is die in een nietgewenste toestand eindigt, dan is er dus een eindige reeks random keuzes die tot deze toestand leidt, en de kans dat deze keuzes worden gemaakt is positief, zodat A een positieve faalkans heeft. Faalkans 0 sluit dus het bestaan van (eindige) incorrecte executies uit. We zien nu ook in hoe programma 7.3 een kans 0 op een oneindige executie kan hebben: het is dan nodig dat er oneindig vaak gelijke getallen worden getrokken.

114 104 7 Randomisering Definitie 7.5 Een Sherwood-algoritme is een algoritme dat randomisering gebruikt, maar aan terminatie en partiële correctheid voldoet. Waar kun je deze algoritmen voor gebruiken? Stelling 7.4 laat zien dat Sherwood-algoritmen niet, zoals Monte-Carlo- of Las-Vegas-algoritmen, gebruikt kunnen worden om problemen op te lossen die geen deterministische oplossing hebben. Je kunt een Sherwood-algoritme ook altijd de-randomiseren : Stelling 7.6 Zij A een Sherwood algoritme voor een zeker probleem; dan bestaat er ook een niet-randomiserend algoritme B voor hetzelfde probleem. Bewijs. Beschouw de executie van A waarin alle getrokken random bits 0 zijn. Omdat A aan terminatie voldoet is deze executie eindig (en heeft daardoor zelfs positieve kans van voorkomen). Omdat A partieel correct is voldoet de eindtoestand aan de gestelde eis. Het algoritme B is bijna gelijk aan A, maar alle random trekkingen zijn vervangen door een deterministische procedure die altijd 0 oplevert. De executies van B zijn eindig en de eindtoestand is correct, waarmee B een niet-randomiserend, terminerend en partieel correct algoritme is. Als voorbeeld van een Sherwood-algoritme kun je denken aan quicksort: de gerandomiseerde versie kiest een random pivot, maar je kunt ook een gederandomiseerde versie nemen die bv. altijd het eerste element van een segment als pivot neemt. Dit voorbeeld maakt ook duidelijk waarom je een Sherwood algoritme zou willen gebruiken: de verwachte complexiteit is vaak lager dan die van een niet-randomiserend algoritme. 7.2 Coordinatie over een broadcast-kanaal De Medium Access Control protocollen van Ethernet zijn randomiserend. In een Ethernetsysteem zitten meerdere computers op één kabel aangesloten. Een station dat een bericht wil versturen plaatst het, zonder overleg of zonder met een schema rekening te houden, op de kabel. Hierbij bestaat het risico dat verschillende uitzendingen overlappen en beide boodschappen gaan dan verloren. De stations merken dit (nog tijdens de uitzending!), breken de uitzending af en proberen het na een random gekozen tijdsinterval opnieuw. Vanwege de mogelijkheid dat er heel vaak achter elkaar botsingen plaatsvinden lijkt dit idee niet zo goed, toch is het de meest efficiënte manier om van zo n kanaal gebruik te maken. In deze sectie leer je waarom: met randomiserende protocollen kunnen we een complexiteit bereiken die niet-randomiserend onmogelijk is. Om deze claim hard te maken moet er een ondergrens zijn voor deterministische oplossingen en een bovengrens die lager is voor een randomiserende oplossing. Nu is rekenen aan Ethernet nogal lastig en daarom vertalen we de onderliggende conflictsituatie naar een model met discrete tijd, dwz. de tijd is niet als R maar als N gemodelleerd. Net als bij Ethernet zoeken we naar het eerste moment waarop een station alleen zendt Probleemstelling Een satelliet (figuur 7.5) kan vanuit verschillende grondstations worden bestuurd. Deze stations berekenen, wanneer zij actief zijn, allemaal hetzelfde stuurcommando op tijdstip 0 en kunnen dit radiografisch overbrengen. Radiocommunicatie is georganiseerd in zg. slots, dwz., de tijd is in kleine stukjes verdeeld die elk net lang genoeg zijn om de stuurcommando s over

115 7.2 Coordinatie over een broadcast-kanaal n Figuur 7.5: Een satelliet met grondstations. te zenden. De satelliet kan de commando s ontvangen als in een bepaald slot exact één grondstation zendt; als meerdere stations tegelijk zenden worden de uitzendingen verstoord. De grondstations weten niet, welke andere stations actief zijn of de satelliet in zicht hebben en kunnen hierover ook niet communiceren (anders hadden ze die prachtige satelliet natuurlijk helemaal niet nodig). Het zendgedrag van een station kan daarom niet afhangen van welke stations er actief zijn, of van welke stations zenden in eerdere ronden. We zoeken een strategie die voor elk station bepaalt wanneer (in welke slots) er gezonden wordt, en wel zo, dat als er tenminste één grondstation actief is, de satelliet het commando krijgt. De communicatie moet zo snel mogelijk afgerond zijn: we kijken naar het aantal slots dat het algoritme nodig heeft Niet-randomiserende oplossingen Omdat de stations geen informatie van elkaar ontvangen kunnen we ons een deterministische strategie voor K ronden voorstellen als de toekenning van een verzameling S i {1,..., K} aan station i. Deze verzameling S i is het zendschema voor station i, dwz. station i zendt precies in de slots in S i. Let op dat i in het geheel niet zendt als hij inactief is, en dat S i niet van het al dan niet actief zijn van de anderen mag afhangen. Een andere representatie van de strategie is, per tijdstip te vermelden welke stations willen zenden: R t is de verzameling stations die kunnen zenden in slot t, dus: R t = {i : t S i }. Als A de deelverzameling van de actieve stations is, zijn de daadwerkelijk zendende stations in slot t gegeven door R t A. De werking van het schema vereist dat er voor elke niet-lege verzameling A tenminste één slot is waarin precies één actief station zendt: A {1,..., n}, A : ( t : R t A = 1) Een voor de hand liggende oplossing is de volgende: grondstation i zendt (indien actief) in slot i. Er vinden nimmer overlappende uitzendingen plaats, en als er tenminste één station operationeel is zendt dat station de boodschap op. In het bovenstaande model uitgedrukt: S i = {i}, en R t = {t}. Als nu u A, dan is R u A = {u}, dus deze oplossing voldoet aan de eis. Het aantal gebruikte ronden (het aantal verzamelingen R dat je nodig hebt) is hier n, het aantal stations. Zie opgaven 7.9 en 7.10.

116 106 7 Randomisering De geschetste oplossing (en die in de opgaven) is meteen de best mogelijke, met andere woorden: er bestaat geen deterministische strategie die minder dan n slots gebruikt. Het bewijs is voor de lijn van ons betoog van ondergeschikt belang, maar wordt volledigheidshalve hier gegeven. Stelling 7.7 Elke deterministische strategie gebruikt tenminste n ronden. Bewijs. We gaan het bewijs doen met inductie naar n, maar merk eerst op dat een oplossing correct blijft onder het permuteren van stations, en onder het permuteren van slots. De inductiehypothese is (voor n): Een rij R 1,..., R K {1,..., n} die voldoet aan A {1,..., n}, A : t : R t A = 1 bevat tenminste n verzamelingen (ie. K n). n = 1: Het element 1 zal tenminste eenmaal moeten voorkomen in een R t, er is dus tenminste 1 verzameling. n + 1: Zij nu R 1,..., R K een oplossing voor n + 1. Vul nu A = {1,.., n + 1} in in de eis, dan is steeds R t A = R t en hieruit volgt: t : R t = 1. Omdat we de slots mogen permuteren kunnen we stellen dat R K = 1, en omdat we de stations mogen permuteren kunnen we stellen dat R K = {n + 1}. Nu voldoet t = K aan de correctheids-eis voor alle verzamelingen A die n + 1 bevatten, maar voor geen enkele A die n + 1 niet bevat; voor die verzamelingen A moet er dus een andere t zijn: A {1,..., n}, A : t < K : R t A = 1. De R t zijn deelverzamelingen van {1,..., n + 1}, maar omdat de bekeken A s niet n + 1 bevatten, kunnen we die uit de verzamelingen verwijderen. Definieer R 1 tm R K 1 door R t = R t {n + 1} en nu voldoen deze K 1 deelverzamelingen van {1,..., n} aan A {1,..., n}, A : t : R t A = 1. De inductiehypothese zegt nu dat K 1 n, waarmee K n + 1 is bewezen Een randomiserende oplossing: de vervalprocedure Programma 7.6 toont een randomiserende procedure: elk station dat de boodschap heeft, begint te zenden en gaat hiermee net zolang door tot een random-bittrekking voor de eerste keer 0 oplevert of slot 2 lg(n) is afgelopen. Stelling 7.8 Programma 7.6 is een Monte-Carlo-algoritme met faalkans hoogstens 1/2. Bewijs. Omdat het zenden na 2 lg(n) slots wordt afgebroken is elke executie eindig. Het aantal zendende stations wordt van slot tot slot uitgedund, en we hebben succes als er in het laatste slot waarin gezonden werd precies één zendend station was. De kans dat het zenden van een station wordt afgebroken doordat de grens op het aantal ronden werd bereikt is zo klein (hoe klein precies?), dat we dit in de berekening gaan verwaarlozen. Zij S(n) de kans op succes ingeval er n stations aan het zenden beginnen. Er geldt S(0) = 0 omdat zonder

117 7.2 Coordinatie over een broadcast-kanaal 107 m = "station heeft boodschap" for (i=1 ; i <= 2*lg(n) ; i++) if (m) { send message in slot i m = random(0,1) } Programma 7.6: De vervalprocedure. zenders geen bericht wordt overgebracht en S(1) = 1 omdat een enkele zender meteen in de eerste ronde succes heeft. De precieze berekening van de slaagkansen is voor de lijn van ons betoog weer van ondergeschikt belang maar volledigheidshalve geven we hier de hoofdlijn. Voor het berekenen van S(2) bedenken we, dat in het eerste slot de zenders tegelijk zenden en er geen boodschap wordt overgebracht: het zal er dus van afhangen hoeveel stations er in het volgende slot doorgaan. Met kans 1 4 trekken beide stations 0 en stoppen ze allebei, en met kans 1 2 trekt er één een 0 en heeft de volgende ronde succes, Met kans 1 4 trekken ze allebei een 1 en in dit geval gaan beide stations door en is het in de volgende ronde net zo alsof ze dan beginnen 1. We vinden S(2) = 1 4 S(0) S(1) S(2), met oplossing S(2) = 2 3. Bij drie stations hebben we kans 1 8 dat geen station een 1 trekt, kans 3 8 dat 1 station een 1 trekt, kans 3 8 dat 2 stations een 1 trekken, en kans 1 8 dat 3 stations een 1 trekken. Daarom is S(3) = 1 8 S(0) S(1) S(2) S(3), met oplossing S(3) = 5 7. Voor n stations vinden we S(n) = ( ) n n 1 k=0 k 2 S(k) en de oplossing hiervan is S(n) = n ( ) 1 n 1 n 2 n 1 k=0 S(k). We zien dat S(n) een gewogen gemiddelde van de slaagkansen voor k kleinere waarden is. Met inductie kun je nu bewijzen dat S(n) > 1 2 voor alle n. In 1994 heeft O.P. Lossers [RGL94] de slaagkans direct, zonder gebruik van recurrentie, uitgedrukt. Als p de kans op 0 is en q de kans op 1 (dus q = 1 p), dan is, voor station a: de kans om ronde k + 1 te halen, gelijk aan q k, omdat dit precies gebeurt als a in elk van de eerste k ronden een 1 trekt. de kans om ronde k + 1 niet te halen, gelijk aan 1 q k, omdat dit het complement is van de vorige gebeurtenis. de kans om ronde k + 1 als enige te halen, gelijk aan q k (1 q k ) n 1, omdat hiervoor nodig is dat a die ronde haalt, en elk van de andere n 1 stations die niet haalt. de kans om in ronde k + 1 als enige en laatste af te vallen, gelijk aan p q k (1 q k ) n 1, omdat dit precies gebeurt als a als enige deze ronde haalt, en dan een 0 trekt. Omdat er n mogelijke winnaars zijn, is de kans op een unieke winnaar in ronde k gelijk aan n p q k (1 q k ) n 1. Door te sommeren over de mogelijke ronden volgt dat S(n) = n p q k (1 q k ) n 1. k=0 1 Feitelijk is de slaagkans iets kleiner geworden, doordat de twee stations een ronde dichter bij een mogelijke geforceerde terminatie zijn gekomen. Maar het afbreken zouden we in de berekening verwaarlozen.

118 108 7 Randomisering Kader 7.7: Oh Pee Who? Als je Lossers heet (een naam die volgens het Meertens Instituut niet eens bestaat, zie geef je je kind niet de initialen O.P. mee, en zeker niet de voornamen Old Pal. Dat zou namelijk kindermishandeling zijn. De Oplossers was de naam van een wisselende groep wiskundigen van de Technische Universiteit Eindhoven, die zich ter ontspanning richtte op het oplossen van problemen uit wiskundige periodieken. Het verborgen wiskundegenootschap De Oplossers werd in 1964 opgericht door Jacob H. van Lint, bestond meestal uit 8 á 10 personen en kwam tweewekelijks bijeen. O.P. Lossers publiceerde honderden oplossingen en heeft Erdös-getal 2. Lossers liet, verrassend, zien dat deze waarde niet convergeert voor n. Voor p = q = 1 2, blijft de kans oscilleren tussen en , dwz., met een amplitude van rond de waarde 1/ ln 4 = Met een module als programma 7.6 kun je verschillende dingen doen. Bedenk zelf of/hoe je programma 7.2 op dezelfde manieren kunt gebruiken. 1. Eénmalig toepassen als snel maar onzeker algoritme. Zoals we zagen, kost dit slechts 2 lg(n) ronden en de faalkans van dit Monte-Carlo-algoritme is beter dan 1/2. 2. Meervoudig toepassen voor kleinere faalkans. Net als bij programma 7.2 kunnen we met een kleine ingreep de faalkans willekeurig klein (maar positief!) maken. Als we de vervalprocedure r maal herhalen hebben we nog steeds een Monte-Carlo-algoritme, dat 2r lg(n) slots gebruikt, maar de faalkans wordt 1 2 r. 3. Met een test combineren tot een Las-Vegas-algoritme. Als de satelliet na afloop van slot 2 lg(n) kan terugzenden of de boodschap is overgekomen, kunnen we de vervalprocedure simpelweg herhalen tot het optreden van succes. Dan is het resultaat partieel correct, maar terminatie geldt niet meer, alleen terminatie met kans 1: we hebben dan een Las-Vegas-algoritme. Dit is ruwweg wat er in ethernet gebeurt, en daarmee is het bij ethernet wel zeker dat elk aangeboden bericht ook wordt verzonden, maar er is geen harde limiet op de tijd waarbinnen dit gebeurt; zie kader Met een deterministisch algoritme combineren tot een Sherwood-algoritme. Verdeel de slots, draai in de even slots het Las-Vegas-algoritme en in de oneven slots de nietrandomiserende oplossing uit sectie 7.2.2, met dien verstande dat het wordt afgebroken als er in de even slots succes wordt gerapporteerd. Dit algoritme voldoet aan terminatie (op zijn laatst in de n e oneven slot slaagt het niet-randomiserende algoritme) en is partieel correct (we stoppen pas als de satelliet het bericht heeft). De verwachte complexiteit is slechts 8 lg(n) ronden, hetgeen behoorlijk beter is dan het beste niet-randomiserende algoritme.

119 7.3 Ben-Ors consensus-algoritme 109 De uitbreiding tot een Las-Vegas-algoritme is mogelijk met elk Monte-Carlo-algoritme waarvan het succes achteraf te testen is. 7.3 Ben-Ors consensus-algoritme Als klap op de gerandomiseerde vuurpijl gaan we laten zien hoe je randomisering (het verzwakken van terminatie tot terminatie met kans 1) kunt toepassen om een oplossing te vinden voor een consensus-probleem. In sectie 6.3 was meer sprake van een klasse van problemen, want we bewezen de onmogelijkheid van elk probleem waarbij overeenstemming van uitvoeren gevraagd is. Wat men doorgaans verstaat onder het consensus-probleem is een soort stemming over de invoer, waarbij elke uitslag acceptabel is mits deze door minstens één kiezer wordt ondersteund (geldigheid). Het algoritme dat we gaan zien heeft in elk proces een invoer (0 of 1) en processen kunnen beslissen op 0 of 1, waarbij aan deze eisen wordt voldaan: Terminatie met kans 1: De kans dat elk correct proces beslist is 1. Overeenstemming: Alle genomen beslissingen zijn gelijk. Geldigheid: De genomen beslissing komt voor als invoerwaarde. Geldigheid stelt in feite dat als alle processen dezelfde waarde v als invoer hebben, deze v ook de uitvoer is. Als zowel 0 als 1 als invoer vookomen dan zijn beide uitvoerwaarden toegestaan Uitleg van het algoritme Het algoritme werd bedacht door Ben-Or [BO83], we volgen de presentatie uit Aguilera en Toueg [AT98]. Het algoritme van Ben-Or verloopt in een aantal ronden, die elk uit drie fasen bestaan: een rapportagefase, een voorstellenfase (proposal-fase) en een verwerkingsfase. Een parameter t is de tolerantie: er mogen maximaal t processen crashen voor de goede werking (maar in de meeste executies crashen natuurlijk veel minder processen). We nemen voor het algoritme aan dat t < n 2 ; je kunt trouwens ook bewijzen dat als t groter is, randomisering niet voldoende is om een oplossing te kunnen vinden. Een proces begint elke ronde met een waarde x die 0 of 1 kan zijn, en voor de eerste ronde is dit de invoer van het proces. In de eerste fase worden de waarden uitgewisseld: een proces zendt zijn waarde aan alle processen en wacht de ontvangst van n t berichten af. Zou er op meer berichten worden gewacht, dan wordt het crashen van t stations door de overgeblevenen niet overleefd. We moeten er nu wel op bedacht zijn dat de verzameling van ontvangen rapporten daardoor, juist bij afwezigheid van crashes van proces tot proces kan verschillen! In de tweede fase kan een voorstel (proposal) tot beslissen worden ingediend. Een proces dient een voorstel voor v in als er in de eerste fase meer dan n/2 rapporten met die waarde werden ontvangen. Merk op dat, omdat er slechts n t rapporten werden ontvangen, het mogelijk is dat dit aantal voor geen van de twee mogelijke waarden wordt gehaald; in dat geval stelt het proces de voor. De proposals worden weer aan alle processen verstuurd, en er wordt gewacht tot er n t zijn ontvangen.

120 110 7 Randomisering uitvoer x = invoer k = 0 klaar = False while (not Klaar) { k++ ; if { Klaar = True } } // Fase 1: Rapporteer forall j { send <report, k, x> to j } receive n-t berichten <report, k, *> // Fase 2: Proposals if (er waren meer dan n/2 berichten met waarde v) { p = v } else { p } forall j { send <propose, k, p> to j } receive n-t berichten <propose, k, *> // Fase 3: Kies en Beslis if (er was een bericht <propose, k, v> met then { x = v} else { x = random(0,1) } if ( (er waren meer dan t berichten <propose, k, x>) AND (uitvoer ) { decide(x) } // Zet uitvoer op x Programma 7.8: Ben-Ors consensus met randomisering. In de derde fase worden de ontvangen proposals geanalyseerd. De waarde voor de volgende ronde is bij voorkeur een waarde waarvoor proposals werden ontvangen, maar als geen proposals (anders dan werden ontvangen gaat het proces de volgende ronde in met een random bit. Er wordt beslist op een waarde waarvoor meer dan t proposals zijn ontvangen. Als er een beslissing is genomen doet het proces nog één ronde mee en daarna loopt het programma af Bewijs van overeenstemming en geldigheid In deze sectie bewijzen we dat beslissingen van verschillende processen gelijk zijn, hoogstens één ronde na elkaar komen, en aan geldigheid voldoen. Lemma 7.9 Als elk proces dezelfde invoer v (in {0, 1}) heeft, neemt elk correct proces (in de eerste ronde) een beslissing voor v.

121 7.3 Ben-Ors consensus-algoritme 111 Bewijs. Elk rapport in fase 1 is in dit geval voor v, zodat de drempel voor het voorstellen van v in fase 2 door alle processen wordt gehaald (immers, n t > n/2). Elk voorstel is dus voor v, waarmee de drempel voor beslissen wordt gehaald (immers, n t > t). 1 2 Een beslissing voor v is zelfs al gegarandeerd als er een voldoende grote meerderheid (van n + t processen) voor v is; zie opgave Lemma 7.10 Voor elke k geldt: als er niet vòòr ronde k 1 een beslissing is genomen, maakt elk correct proces ronde k af. Bewijs. Er wordt in de ronde gewacht op rapporten en proposals, maar er zijn genoeg correcte processen om het benodigde aantal toe te sturen. Pas als er in ronde k 1 is beslist gooien correcte processen na ronde k de handdoek in de ring waardoor andere correcte processen niet aan het vereiste aantal komen. Lemma 7.11 In elke ronde wordt voor hoogstens één waarde een proposal gestuurd. Bewijs. Stel proces i doet een proposal voor v en proces j één voor w, in dezelfde ronde. Dan ontving i meer dan n/2 rapporten voor v, en w ontving meer dan n/2 rapporten voor w. Er is nu tenminste één proces dat zowel een v-rapport aan i als een w-rapport aan j zond; en omdat een proces voor hoogstens één waarde rapporten zendt in een ronde, impliceert dit v = w. Lemma 7.12 Als proces i in ronde k voor v beslist, dan beslissen alle correcte processen voor v, en wel in dezelfde ronde of ten hoogste één ronde later. Bewijs. Neem aan dat k de eerste ronde is waarin een beslissing wordt genomen. Proces i ontving meer dan t proposals voor v, en nu sluit lemma 7.11 meteen uit dat er proposals voor de andere waarde zijn: er wordt dus in ronde k geen tegenstrijdige beslissing genomen. Je weet niet, hoeveel proposals voor v door de andere processen ontvangen werden: sommige ontvingen er misschien ook meer dan t en beslisten ook in ronde k, anderen ontvingen er misschien minder. Maar omdat er minstens t + 1 processen een v-proposal stuurden, ontving elk correct proces tenminste één v-proposal! Dus gaan alle correcte processen ronde k + 1 in met waarde v, waarmee (net zoals in lemma 7.9 werd beredeneerd) alle processen in die ronde voor v gaan beslissen. Je ziet nu waarom een proces kan stoppen na de ronde volgend op de eigen beslissing: alle processen hebben dan gegarandeerd ook beslist. De processen die pas in ronde k + 1 beslisten willen ronde k +2 ook nog uitvoeren, en in die ronde blijven ze misschien hangen omdat er niet genoeg berichten worden verstuurd. Dit hangen is niet in strijd met de gewenste voortgang, omdat terminatie met kans 1 alleen over het nemen van beslissingen (extern gedrag) gaat en niet over het hangen van programma s (interne toestand) Bewijs van convergentie In het voorgaande is al gebleken dat het algoritme snel klaar is zodra alle (levende) processen een ronde ingaan met dezelfde waarde van x. Het aardige van het algoritme is, dat dit door de random keuzes die worden gemaakt, in elke ronde met positieve kans gaat gebeuren.

122 112 7 Randomisering Lemma 7.13 Elke ronde heeft een kans van tenminste 1 2 n 1 om te bereiken dat na afloop elk proces dezelfde waarde heeft. Bewijs. We onderscheiden twee gevallen. Stel dat er voor een v {0, 1} een proposal wordt ontvangen door tenminste één proces. We weten uit lemma 7.11 dat er geen proposals voor de andere waarde worden verstuurd. Minstens één proces ontvangt een proposal voor v, er zijn dus hoogstens n 1 processen die 1 hun nieuwe waarde door een random-keuze bepalen; met kans tenminste trekken zij alle 2 n 1 de waarde v. Stel nu dat er in de ronde geen proposals (anders worden ontvangen. De n processen nemen allemaal een random waarde: met kans 1 2 nemen ze allemaal 0, met kans 1 n 2 nemen ze 1 n allemaal 1, dus met kans zijn de getrokken waarden gelijk. 2 n 1 (Een technische opmerking: we nemen aan dat de volgorde van ontvangst van berichten niet kan afhangen van de uitkomst van de random trekkingen die het algoritme doet.) Stelling 7.14 Ben-Ors algoritme (programma 7.8) is een asynchroon gerandomiseerd consensus algoritme. Bewijs. We hebben overeenstemming bewezen in lemma 7.12, en geldigheid in lemma 7.9. Voor een oneindige executie is nodig dat de processen oneindig vaak met verschillende waarden een nieuwe ronde ingaan, en wegens lemma 7.13 is de kans hierop 0. Dit bewijst terminatie met kans 1. Samenvatting en conclusies In dit hoofdstuk hebben we gezien dat randomisering, nog in meerdere mate dan in het geval van sequentiële programmering, een belangrijk hulpmiddel is bij het ontwerpen van gedistribueerde applicaties. Net als bij sequentiële berekeningen is het mogelijk randomiserende algoritmen te ontwerpen met een lagere (verwachte) complexiteit. De algoritmen kunnen dan zo gemaakt worden, dat elke berekening eindig is en bovendien de uitkomst gegarandeerd juist is; we spreken dan van een Sherwood-algoritme. In het geval van gedistribueerd rekenen kennen we ook problemen, waarvoor geen oplossing bestaat wanneer er terminatie èn partiële correctheid worden verlangd. Zulke problemen zijn soms wel oplosbaar met randomiserende algoritmen, waarbij een kleine kans op een onjuist antwoord, of het bestaan van oneindige berekeningen moet worden geaccepteerd. Zo zijn er Monte-Carlo-algoritmen, waarvan elke berekening eindig is, maar een onjuist eindantwoord mogelijk is. Aan de andere kant kennen we Las-Vegas-algoritmen, waarbij het eindantwoord gegarandeerd juist is maar er geen harde grens is op de lengte van een berekening. Van de diverse soorten algoritmen hebben we in dit hoofdstuk voorbeelden gezien, en ook, dat hiermee problemen oplosbaar zijn die dat met deterministische algoritmen niet zijn: Sectie 7.1 gaf een Monte-Carlo- en een Les-Vegas-oplossing voor electie zonder identiteiten. Deze electie is deterministisch onmogelijk. Sectie 7.2 gaf diverse algoritmen voor het versturen van een bericht over een gedeeld kanaal. Het basisprincipe (de vervalprocedure) kan op een Sherwood-, Monte-Carlo- of Las-Vegas-manier worden gebruikt. De complexiteit ervan is lager dan met een deterministische oplossing kan worden gehaald.

123 Opgaven bij hoofdstuk Sectie 7.3 behandelde een randomiserend consensus-algoritme van het Las-Vegas-type. In hoofdstuk 6 werd bewezen dat hiervoor geen deterministisch alternatief bestaat. Opgaven bij hoofdstuk 7 Opgave 7.1 In computersystemen worden random bits helemaal niet met één of ander fysisch randomiserend mechanisme bepaald, maar deterministisch bepaald door een pseudo-random number generator. Onderzoek wat dat precies is. Wat is het effect van het gebruik van pseudo-randomisering op de werking van programmas 7.2 en 7.3? Wat is het effect op de werking van programma 7.8? Opgave 7.2 Is het electie-algoritme uit de Lego-tutorial (kader 7.1) van het Monte-Carlo- of van het Las-Vegas-type? Wat is de faalkans, resp., de verwachte complexiteit? Kun je het algoritme verbeteren? Opgave 7.3 Randomisering. (Uit het tentamen van mei 2000.) Hier zie je een Monte Carlo algoritme voor electie tussen twee processen: mijngetal = random(0..31) send mijngetal ; receive haargetal if (mijngetal < haargetal) then { toestand = leider } else if (mijngetal > haargetal) then { toestand = niet-leider } (a) Wat is het verschil tussen terminatie en terminatie met kans 1? (b) Aan welke van deze voldoet dit algoritme? (c) Wat is een Las Vegas algoritme? (d) Verander het algoritme zo dat het een Las Vegas algoritme wordt. Opgave 7.4 Laat zien dat je, gegeven een Las-Vegas-algoritme voor een zeker probleem, altijd een Monte-Carlo-algoritme (met elke gewenste faalkans) kunt maken. Opgave 7.5 Gerandomiseerde electie. (Uit het tentamen van november 2005.) Itai en Rodeh bedachten in 1981 een originele oplossing voor het probleem van electie op ringen. Stations kiezen een random getal (uit het bereik 1... n 2, waar n het aantal stations is) als identiteit, en gebruiken die identiteit vervolgens om het (deterministische) algoritme van Chang en Roberts uit te voeren. (a) Bespreek wat er kan gebeuren in het (onwaarschijnlijke) geval dat enkele stations gelijke getallen als identiteit kiezen. (b) Wat kun je zeggen over de bericht-complexiteit van deze aanpak? (c) Is er sprake van een Monte Carlo, Las Vegas, of Sherwood algoritme? Opgave 7.6 Randomisering in electie. (Uit het tentamen van juli 2011.) Dit is een Monte Carlo algoritme voor electie tussen twee processen: X = random(0..15) send X ; receive Y if (X < Y) { toestand = leider } else if (X > Y) { toestand = niet-leider }

124 114 7 Randomisering (a) Wat is de definitie van een Monte Carlo en van een Las Vegas algoritme? (b) Kun je het algoritme omvormen tot een Las Vegas algoritme? (c) Kun je het algoritme ook omvormen tot een Sherwood algoritme? Opgave 7.7 Nederlandse boommarters krijgen zender. Om het uitsterven van de boommarter beter in kaart te brengen, krijgen 101 boommarters een zendertje. Elke zender heeft een unieke MarterId. De zendertjes kunnen de marters naar één van de vier voederplaatsen A, B, C of D sturen, en omdat we vier voersoorten willen testen, moet elke voederplaats door tenminste één marter worden bezocht, wat leidt tot het probleem Etenstijd: Input: Elke zender p kent zijn unieke MarterId m p. Output: Elke marter kiest een voederplaats v p {A, B, C, D}, waarbij elk van de letters tenminste eenmaal moet voorkomen. In deze opgave onderzoek je drie oplossingen voor dit probleem. (a) Deterministische toekenning: Marter p berekent v p als functie van de MarterId: v p = F (m p ). Bewijs dat deze oplossing niet mogelijk is als de verzameling MarterIds een willekeurige deelverzameling van N is. (b) Crash-tolerante coordinatie: De marters doen een shout/collect met hun MarterId, rekening houdend met de mogelijkheid dat ze t collega s niet horen. Hoe kan een marter zijn voederplaats berekenen uit de 101 t ontvangen Id s? Hoe groot mag t zijn? (c) Random keuze: Elke marter kiest een random voederplaats, elk met kans 25%. Wat is een Monte Carlo, en wat is een Las Vegas algoritme? Van welk type is hier sprake? Is er sprake van terminatie, en wat is de slaagkans? Opgave 7.8 Randomiserende algoritmen. (Uit het tentamen van mei 2001.) Piet heeft een nieuw randomiserend algoritme uitgevonden. (Het berekent een Rangordening van Invoer, dwz. elk station heeft een invoergetal waarvan de rang in de verzameling invoeren wordt bepaald, maar dat is voor deze opgave eigenlijk niet van belang.) Helaas is het aantal ronden dat het algoritme gebruikt niet begrensd, maar bij terminatie geeft het wel altijd een correct resultaat. (a) Wat is het verschil tussen een Las Vegas en een Monte Carlo algoritme? Van welk type is Piet s algoritme? (b) Piet s algoritme heeft een verwacht aantal ronden van R. Bewijs dat voor elke s geldt: de kans dat het algoritme s of meer ronden nodig heeft is begrensd door R s. (c) Piet wil voor ongeduldige gebruikers een variant maken die wel een begrensd aantal ronden gebruikt, maar dan kan het resultaat incorrect zijn. De faalkans en/of aantal ronden moet instelbaar zijn en wel zo dat de faalkans zo klein kan worden als de gebruiker wil. Leg uit hoe dat kan en hoe verhouden zich dan de tijdgrens en de faalkans? Opgave 7.9 Dit is een alternatieve deterministische strategie voor het broadcast probleem: Station i zendt in alle ronden tot en met i. Geef de S- en R-verzamelingen van deze strategie, analyseer de tijd, en bewijs de correctheid. Dezelfde vragen voor deze strategie: Station i zendt in ronde i en, als i > 1, ook in i 1. Opgave 7.10 Een strategie voor het satellietprobleem heeft deze eigenschap: elk station i zendt voor het eerst in slot i (en wat hij daarna nog doet interesseert ons niet). Druk deze eigenschap uit als eisen op de R- en S-verzamelingen en bewijs dat elke strategie met deze eigenschap correct is. Hoeveel slots zijn nodig?

125 Opgaven bij hoofdstuk Opgave 7.11 Bereken S(n) (zie stelling 7.8) voor alle n 10. Wat is de limiet? Opgave 7.12 Zij T (z) de exponentieel voortbrengende functie van S(n): T (z) = zn n 0 S(n) n!. (a) Bewijs dat T (z) = z/2 + e z/2 T (z/2). (b) Kun je een uitdrukking opstellen voor de k-de afgeleide T (k) (z)? (c) Kun je S(n) vinden als T (n) (0)? Opgave 7.13 In een variant op programma 7.6 zendt een station niet elke ronde tot de eerste 0, maar juist alleen in de eerste ronde met een 0. Bewijs dat de slaagkans hierdoor stijgt. Bereken de slaagkans. Opgave 7.14 Vervalprocedure. (Uit het tentamen van november 2003.) In de vervalprocedure (Prog. 7.6) zendt een station in elk slot, totdat een keer de waarde 0 de uitkomst van een random-instructie ( is. ) Voor S(n), de slaagkans wanneer er n stations deelnemen, geldt S(n) = 1 n 1 n 2 n 1 k=0 S(k). (We negeren in de berekening een eventuele k ingebouwde limiet op het aantal ronden.) In de Verbeterde Vervalprocedure zendt elk station precies één keer, namelijk in die ronde waarin hij voor t eerst een 0 trekt. (a) Geef hiervoor een programma (pseudocode). (b) Zij T (n) de kans dat de verbeterde procedure slaagt als er n stations aan beginnen; geef een recurrente betrekking voor T (n). (Kun je bewijzen dat T (n) S(n)?) Opgave 7.15 Je kunt in programma 7.6 ook een valse munt gebruiken; zij p de kans op een 1 en q (= 1 p) de kans op een 0. Als we p wat groter maken duurt het langer voor alle stations gestopt zijn met zenden, maar de kans op succes wordt groter. Welke p minimaliseert de verwachte tijd van het afgeleide Las Vegas algoritme? Opgave 7.16 Laat zien dat geldigheid in sectie 7.3 niet-trivialiteit impliceert. Waarom sluit stelling 6.4 het algoritme van Ben-Or niet uit? Opgave 7.17 Consensus. (Uit het tentamen van augustus 2000.) Voor het consensus-probleem bewezen Fischer, Lynch en Paterson een onmogelijkheidsresultaat, toch bestaat hiervoor het algoritme van Ben-Or. (a) Hoe luidt de stelling van Fischer, Lynch en Paterson? (b) Leg uit waarom deze stelling het algoritme van Ben-Or niet uitsluit. (c) Is Ben-Or s algoritme van het Las Vegas, Monte Carlo of Sherwood type? Leg uit waarom. Opgave 7.18 Bewijs de volgende versterking van lemma 7.9: Als aan het begin van een ronde meer dan 1 2n + t processen dezelfde waarde v (in {0, 1}) hebben, beslist elk correct proces in die ronde v.

126 Hoofdstuk 8 Foutdetectie Het is lastig dat het consensus-probleem niet oplosbaar is in asynchrone systemen (stelling 6.4) omdat het werken aan asynchrone programmatuur erg prettig is. Het belang van het consensusprobleem zal in de latere hoofdstukken verder duidelijk worden. Nu zijn er in hoofdlijnen drie manieren om het resultaat van Fischer, Lynch en Paterson te omzeilen, namelijk door versterking van het reken-model of door verzwakking van de eisen aan de oplossing: 1. Maak het model synchroon. In de praktijk zijn computersystemen niet helemaal asynchroon, het is bijvoorbeeld redelijk aan te nemen dat een eenvoudige optelling korter duurt dan een ingewikkelde berekening, of dat de meeste berichten binnen een jaar ontvangen worden. Je mag als programmeur echter nooit uitgaan van wat redelijk is of vaak gebeurt, maar uitsluitend gebruik maken van gespecificeerde (en gegarandeerde) eigenschappen van het onderliggende systeem. Je kunt een zekere mate van synchroniteit modelleren in je rekenmodel en er dan in je algoritme gebruik van maken. Dolev, Dwork en Stockmeyer [DDS87, DLS88] maakten een uitgebreide studie over hoeveel synchroniteit noodzakelijk is. We zien een voorbeeld van een synchroon algoritme in sectie Randomisering. Er zijn diverse voorbeelden die aantonen dat randomisering het gedistribueerde rekenmodel sterker maakt, zowel in complexiteit (met randomisering kun je door een deterministische ondergrens heen ) als in berekenbaarheid; zie hoofdstuk 7. Consensus is een voorbeeld van het laatste, en Ben-Or [BO83] gaf een gerandomiseerd algoritme voor consensus (sectie 7.3). 3. Foutdetectie. We nemen aan dat de processen enig inzicht hebben in welke processen zijn gecrasht. Dit inzicht berust vaak op gebruik van timers en dus impliciete aannamen over synchroniciteit van het systeem. De programmeur heeft echter met tijd-constanten en timers niets te maken omdat hij de detector als abstracte module aanspreekt, en bovendien zijn andere implementaties (dan met tijd) van detectors mogelijk. 8.1 Motivatie: synchrone consensus Neem aan dat er een maximum µ geldt op de looptijd van een bericht: op tijdstip t verzonden betekent vòòr t + µ ontvangen. Neem aan dat ρ de maximale tijd is voor het uitvoeren van de forall in de body van het hoofdprogramma van programma

127 8.1 Motivatie: synchrone consensus 117 V[1..n] // accumuleer invoer van alle processen telkens wanneer een vector W wordt ontvangen: { forall j in 1..n if ( V[j] and ) { V[j] = W[j] } } Hoofdprogramma voor i (start op tijd t0): V ) // x staat op positie i for (r = 1 ; r < n ; r++) { forall j in 1..n { send V to j } sleep until (t0 + r*(mu+rho)) } decide( MAJ(V) ) Programma 8.1: Synchrone stemming (consensus). Preconditie voor het programma is dat elk station op tijd t 0 over een invoerbit beschikt; het programma voert een stemming uit en kiest de vaakst voorkomende bit. In de stemming tellen de invoeren van alle correcte, en een deelverzameling van de crashende processen mee. Proces i spaart invoeren in een vector V, initiëel wordt daarvan de waarde V[i] ingevuld. In programma 8.1 zie je dat ontvangen waarden in de vector worden verwerkt, en dat het proces periodiek alle bekende waarden rondzendt (dit flood set principe wordt in veel algoritmen toegepast). Merk op dat een bericht dat in ronde r wordt verstuurd, nog in diezelfde ronde wordt ontvangen (waarom)? Na n 1 van deze ronden wordt op de meerderheid uit V beslist. Stelling 8.1 Programma 8.1 is een synchroon niet-triviaal consensus algoritme. Bewijs. Omdat het zenden en beslissen door de klok worden gestuurd zal een proces nooit wachten, er treedt geen deadlock op en elk correct proces zal op tijd t 0 + (n 1)(µ + ρ) een beslissing nemen. Dat deze beslissing in elk proces gelijk is volgt uit de gelijkheid van de V-vectoren van verschillende processen. Merk eerst op dat als proces j ooit iets invult in V[i], dit de juiste waarde van x i is. We gaan bewijzen dat correcte processen dezelfde waarden hebben ingevuld: Stel i en j zijn correct en na afloop heeft i V[k] ingevuld; dan heeft ook j V[k] ingevuld. Bewijs: Zij r de ronde waarin i de waarde van x k voor t eerst vernam; als r < n 1, dan zendt i de waarde in de volgende ronde, o.a. naar j, waarmee het zeker is dat ook j de waarde een keer gezien heeft. Maar wat als i de waarde x k in ronde n 1 voor het eerst zag? In elk van de ronden 1 tot n 1 is er een proces geweest dat deze waarde voor het eerst heeft verzonden; in het bijzonder heeft j die al een keer gezien. Uit het voorgaande blijkt dat processen dezelfde beslissing nemen (MAJ is een deterministische functie die de meest voorkomende waarde selecteert). In ieder geval tellen de invoeren van niet-crashende processen mee, waarmee ook niet-trivialiteit wordt bewezen. Het algoritme is niet vreselijk ingewikkeld, maar er zijn wel verschillende aannamen op het gebied van timing nodig en de diverse tijdconstanten vinden we allemaal terug in het

128 118 8 Foutdetectie Kader 8.2: Kritiek op het gebruik van foutdetectors Foutdetectie werd rond 1991 als abstract mechanisme geformuleerd door de onderzoeksgroep van Sam Toueg uit Cornell, en na 5 jaar verscheen een lijvig artikel [CT96] met veel fundamentele resultaten, waaraan onze definities zijn ontleend. Van verschillende kanten stond de aanpak onder kritiek, met name dat het mechanisme van foutdetectors niets oplost. Immers, voor de implementatie van detectors zijn weer onderliggende mechanismen nodig (zoals tijd, zie sectie 8.3), en zonder deze mechanismen blijven detectors onimplementeerbaar en dus consensus onoplosbaar. Nu is hetzelfde argument toepasbaar op vrijwel alles wat er in de informatica is uitgevonden, van programmeertalen tot devicedrivers. Ook de laatsten doen immers alleen maar dingen die je net zo goed (hm!) rechtstreeks vanuit de applicatie zou kunnen doen? Het invoeren van abstracties is echter een essentieel hulpmiddel in de gestructureerde bestudering van een onderwerp, en het onderbrengen van functionaliteit in een module maakt het gebruik ervan gemakkelijker en toegankelijker. programma. Het programma moet dus worden aangepast om het op een snel of een langzaam systeem te draaien; aanpassingen kunnen ingewikkeld zijn omdat er per systeem verschillende dingen kunnen zijn waar precies tijdgrenzen op bestaan. Een ander probleem met synchrone systemen is dat ze vaak onnodig veel tijd gebruiken omdat de bovengrenzen op de looptijd als worst case tijd zijn gespecificeerd. Een typische situatie is dat het systeem een bovengrens van 100ms op de looptijd garandeert, maar in het meeste gebruik komt het zelden voor dat een bericht langer dan 3ms onderweg is. Bij het bepalen van de timerwaarden moet je dan in programma 8.1 van 100ms per ronde uitgaan, terwijl een asynchroon programma (zoals programma 6.3) een ronde per 3ms afwerkt. Bij compatibele computersystemen zijn doorgaans wel de functionele, maar niet de temporele eigenschappen van de componenten gelijk. IBM heeft een kwart eeuw computers gebouwd met dezelfde architectuur (System/360, zie Kader 1.6), maar model was 2000 maal zo snel als model 30. Hier zou compatibiliteit afdwingen dat de wordt afgeremd tot het tempo van de 30. Wat is nu het geheim achter programma 8.1? Het programma kan werken omdat synchrone systemen een perfecte foutdetectie bieden: als aan het eind van een ronde niets van proces j is ontvangen, dan is j gecrasht. We willen graag de zegeningen van deze mogelijkheid gebruiken, zonder de programmeur op te zadelen met stapels timing-aannamen en constanten. Bovendien willen we weten hoe goed je fouten moet kunnen detecteren om consensus te kunnen bereiken. Voor beide doelen is de foutdetector een ideale abstractie. 8.2 Definities Een foutdetector is een module die aan ieder proces een verzameling verdachte processen levert; in de programma s schrijven we dit als de test j in D en die vertelt of proces j op dat moment

129 8.2 Definities 119 al dan niet verdacht is. Om over foutdetectors te kunnen redeneren moeten we deze uitvoer kunnen relateren aan de daadwerkelijke crashes, zoals uitgedrukt in het foutpatroon. In het vervolg is Π een (eindige) verzameling processen. Om uitdrukkingen in modale (temporele) logica te omzeilen voeren we een expliciete tijdsvariabele t in (met range T, bv. de natuurlijke getallen). De daadwerkelijk in een executie optredende fouten worden gemodelleerd als een wiskundige constructie, het foutpatroon, en de uitvoer van de detector als een foutdetectorhistory. De bruikbaarheid van de detector wordt natuurlijk uitgedrukt in termen van relaties die tussen deze twee begrippen gelden. Het foutpatroon F, de tijd t, de afgeleide verzamelingen Crash(F ) en Corr(F ), alsmede de foutdetector history zijn voor de processen niet observeerbaar! Het enige dat proces q ziet is de waarde van H(q, t), wanneer het op tijd t zijn foutdetector D aanspreekt. Foutpatroon en foutdetector-history. We beschrijven eerst de input van de detector, te weten het patroon van optredende fouten tijdens een executie. De crashes die tijdens een berekening optreden worden gemodelleerd met een foutpatroon F : T P(Π). Hier stelt Π de verzameling processen voor en P de powerset. Dan is F (t) de verzameling processen die op tijd t gecrasht zijn. Omdat we geen herstarts modelleren is F altijd monotoon: t 1 t 2 F (t 1 ) F (t 2 ), dus een proces dat éénmaal gecrast is blijft daarna altijd gecrasht. De verzamelingen van defecte en correcte processen zijn door F gedefiniëerd; processen gelden als defect als ze ooit tijdens de executie crashen, dus Crash(F ) = t T F (t). Ze zijn correct als ze dat niet doen, dus Corr(F ) = Π \ Crash(F ). De output van de detector bestaat uit de (mogelijk steeds wisselende) verzamelingen verdenkingen. Verdenkingen kunnen in de tijd en per proces verschillen, daarom modelleren we een foutdetector history als functie H : Π T P(Π). Hier is H(q, t) natuurlijk de verzameling processen die op tijd t door q worden verdacht. Een foutdetector is non-deterministisch: gegeven een foutpatroon zijn er altijd meerdere foutdetector histories mogelijk. Eisen op de histories. Tot nu toe hebben we geen verband gelegd tussen de fouten die in een executie optreden en de foutdetector history, maar om iets aan de foutdetector te hebben moeten we dat natuurlijk wel doen. De eisen zijn steeds van twee typen, namelijk Compleetheid (gecrashte processen worden verdacht) en Accuratesse (correcte processen worden niet verdacht). In de formules komen een foutpatroon F en een foutdetector history H vrij voor; de formule moet natuurlijk gelden voor alle combinaties van F en H die onder de foutdetector D mogelijk zijn. We bekijken compleetheid slechts in één verschijningsvorm: Definitie 8.2 Foutdetector D is compleet als geldt dat elk gecrasht proces uiteindelijk door elk correct proces wordt verdacht: t : t t : p Crash(F ) : q Corr(F ) : p H(q, t ). Accuratesse wordt in vier vormen onderscheiden. Naast een sterke (geen correct proces wordt verdacht) is er een zwakke variant waarin slechts wordt geëist dat er tenminste één correct proces is dat niet wordt verdacht. Van beide onderscheiden we bovendien een uiteindelijke versie, waarbij de detector zich in het begin willekeurig mag vergissen, maar na een eindige periode de gewenste accuratesse gaat vertonen. Definitie 8.3 Foutdetector D is sterk accuraat als geen enkel ongecrasht proces ooit wordt verdacht: t : p, q F (t) : p H(q, t).

130 120 8 Foutdetectie Kader 8.3: Sterke en zwakke compleetheid, reducties Soms wordt een onderscheid gemaakt tussen sterke en zwakke compleetheid. Bij sterke compleetheid wordt (definitie 8.2) geëist dat een gecrasht proces door elk correct proces zal worden verdacht. Bij zwakke compleetheid wordt alleen geëist dat een gecrasht proces door tenminste een correct proces zal worden verdacht. D T D D Algoritme A gebruikt D D Chandra en Toueg [CT96] definiëren reducties, dit zijn nog abstractere mechanismen die werken op foutdetectors. Veronderstel dat de processen beschikken over een foutdetector D die zwak compleet is; we willen echter een algoritme A gebruiken dat een sterk complete detector veronderstelt. De beoogde reductie T D D laat elk proces q regelmatig de verdenkingen (van D) rondsturen. Elk proces p houdt een verzameling H (p) bij; deze is initiëel leeg. Op ontvangst van een verzameling H van q wordt deze aangepast als H (p) = H (p) H \ {q}. De foutdetector D toont A de verzamelingen H. Wanneer een gecrasht proces door een correct proces wordt verdacht, zal de verdenking door de andere correcte processen worden overgenomen. Een proces wordt pas verdacht door D als het eerder door D verdacht was, en tijdelijke onterechte verdenkingen worden opgeheven. Het gevolg is dat D sterk compleet is, en aan dezelfde eigenschappen van accuratesse voldoet als D. Foutdetector D is zwak accuraat als er een correct proces is dat nooit wordt verdacht: p Corr(F ) : t : q F (t) : p H(q, t). Foutdetector D is uiteindelijk sterk accuraat als er een tijdstip bestaat waarna geen enkel correct proces meer wordt verdacht: t : t t : p, q Corr(F ) : p H(q, t ). Foutdetector D is uiteindelijk zwak accuraat als er een tijdstip bestaat en een correct proces dat na dat tijdstip niet meer wordt verdacht: t : p Corr(F ) : t t : q Corr(F ) : p H(q, t ). Hoewel de abstractie van de foutdetector beoogt een scheiding aan te brengen tussen de implementatie en het gebruik van de foutdetectie, worden de onderscheiden vormen van accuratesse toch gemotiveerd door zowel de implementatie als het gebruik. Bij de implementatie (sectie 8.3) zien we dat een gebruikt mechanisme onterechte verdenkingen niet helemaal kan uitsluiten, maar wel in uiteindelijke zin. Bij de implementatie van consensus (programma 8.6 en 8.7) zien we dat onterechte verdenkingen te tolereren zijn, mits tenminste één proces buiten verdenking is. Het is erg gemakkelijk een foutdetector te maken die compleet is (namelijk één die altijd iedereen verdenkt: H(q, t) = Π) en even gemakkelijk een die accuraat is (verdenk nooit

131 8.3 Implementaties van foutdetectors 121 set D // verdachte processen, initieel leeg timer rec[pi] // ontvang-timer voor elk proces // het zenden: op elk veelvoud van sigma: forall j { send IkLeef to j } // het ontvangen: telkens als IkLeef binnenkomt van j: rec[j] = sigma+mu // de time-out als de timer rec[j] afloopt: D = D + {j} Programma 8.4: Perfecte detectie in synchroon systeem. iets: H(q, t) = ). Interessante en nuttige detectors hebben daarom een combinatie van compleetheid en accuratesse. Definitie 8.4 Een foutdetector is perfect als hij compleet en sterk accuraat is; de klasse van deze detectors heet P. Een foutdetector is sterk als hij compleet en zwak accuraat is; de klasse van deze detectors heet S. Een foutdetector is uiteindelijk perfect als hij compleet en uiteindelijk sterk accuraat is; de klasse van deze detectors heet P. Een foutdetector is uiteindelijk sterk als hij compleet en uiteindelijk zwak accuraat is; de klasse van deze detectors heet S. 8.3 Implementaties van foutdetectors Een voordeel van de foutdetector als abstractie is dat we alleen met de eigenschappen en niet met de implementatie rekening hebben te houden. Toch gaan we kort op mogelijke implementaties in, o.a. als motivatie voor de misschien wat vreemd aandoende definities Synchrone systemen: perfecte detectie Neem weer aan dat er een bovengrens µ op de berichtenlooptijd is. Elk proces zendt periodiek (telkens na σ tijdseenheden) een IkLeef bericht aan alle anderen, en een proces waarvan zo n bericht gedurende µ + σ niet is ontvangen wordt verdacht; zie programma Partiëel synchrone systemen: uiteindelijk perfecte detectie Als het systeem wel een bovengrens op de looptijd heeft maar deze is onbekend, of je wilt hem niet in je programmatuur verwerken terwille van portability, is toch nog uiteindelijk perfecte

132 122 8 Foutdetectie set D // verdachte processen, initieel leeg timer rec[pi] // ontvang-timer voor elk proces int musch // schatting voor mu, initieel 1 // het zenden: op elk veelvoud van sigma: forall j { send IkLeef to j } // het ontvangen: telkens als IkLeef binnenkomt van j: if (j in D) { D = D - {j} musch += 1 } rec[j] = sigma + musch // de time-out als de timer rec[j] afloopt: D = D + {j} Programma 8.5: Uiteindelijk perfecte detectie. detectie mogelijk (programma 8.5). Het programma begint met een kleine schatting van µ (bv. 1), waardoor het mogelijk is dat er te snelle time-outs plaatsvinden en correcte processen worden verdacht. Wordt alsnog een bericht van zo n proces ontvangen, dan wordt het direct uit D verwijderd en de schatting voor µ wordt verhoogd. Je kunt nu bewijzen dat in elke executie eindig vaak een correct proces wordt verdacht, met andere woorden, de detectie is uiteindelijk perfect Ondersteuning door het besturingssysteem: incompleet In het besturingssysteem wordt een tabel bijgehouden van alle threads/processen. Een poging te communiceren met een zekere thread kan dan worden beantwoord met een melding dat de thread niet (langer) bestaat. Dergelijke meldingen zijn doorgaans accuraat: een proces verdwijnt uit de tabel als het crasht door het uitvoeren van een illegale actie, bv. delen door nul of stack overflow, en levende processen staan er nog in. Een proces kan echter ook crashen door in een oneindige lus terecht te komen, het blijft dan in de procestabel, het blijft ook actief rekenen maar geeft nooit meer respons. Bij dergelijke crashes kan de respons van het operating system incompleet zijn. Het is voor het operating system niet mogelijk uit te maken of een proces dat geen respons geeft, dat ooit nog weer gaat doen of dat het gecrasht is (dit heeft met het Halting Problem te maken). Een niet erg subtiele oplossing is de volgende: een proces dat gedurende een bepaalde tijd geen respons heeft gegeven wordt door het operating system afgebroken en uit de tabellen verwijderd. Nu is de respons compleet en accuraat, maar een dergelijke euthanasie door het o.s. kan de correcte werking van correct geprogrammeerde systemen behoorlijk verstoren.

133 8.4 Consensus met zwakke accuratesse 123 x = invoer for (r = 1 ; r <= n ; r++) { if ( i == r ) { forall j do send (x,r) to j } wait until ( received (x,r) from r OR r in D ) if received (x,r) { x = x } } decide (x) Programma 8.6: Een rotating-coordinator-algoritme met S. 8.4 Consensus met zwakke accuratesse In deze en de volgende sectie bekijken we twee consensus-algoritmen; ze voldoen aan terminatie en de consistentie wordt uitgedrukt als overeenstemming en geldigheid: Terminatie: Elk correct proces neemt eenmaal een beslissing. Overeenstemming: Alle genomen beslissingen zijn gelijk. Geldigheid: Een beslissing is gelijk aan de invoer van tenminste één proces. Geldigheid impliceert niet-trivialiteit. Hoe kun je foutdetectors in een programma gebruiken? Een complete detector kan gebruikt worden om blokkering in ontvangopdrachten te voorkomen. Een manier hiervoor die we al hebben gezien, bijvoorbeeld in programma 6.3, is om nooit meer dan n t berichten te ontvangen (t is de robuustheid). Deze methode is niet erg verfijnd omdat berichten van correcte processen buiten beschouwing blijven als er minder dan t crashes zijn. Het gebruik van time-outs zoals in programma 8.1 is natuurlijk alleen in synchrone systemen mogelijk. Foutdetectors werken preciezer omdat je kunt programmeren dat er een bericht van een met name genoemd proces q moet worden ontvangen, waarbij het wachten wordt afgebroken als q wordt verdacht. Een proces kan dan uit de wachttoestand komen door de ontvangst van een bericht, of door het verdenken van q, en in een programma geven we dit zo aan: wait until ( received msg from q OR q in D ) Als de detector compleet is, leidt dit nimmer tot blokkering van een correct proces. Steeds moeten we rekening houden met de mogelijkheid dat correcte processen verschillende (verzamelingen) berichten accepteren. Denk bijvoorbeeld aan opgaven 6.1 en 6.2. Bij het gebruik van foutdetectors kan een bericht gemist worden door onterechte verdenkingen en worden de verschillen in ontvangen berichten beperkt door de accuratesse van de detector. Programma 8.6 bereikt consensus met een zwak accurate foutdetector. Dit betekent dat er één proces is dat nooit verdacht wordt, maar uiteraard weten de processen niet welk proces dat is! Daarom gebruiken we een rotating coordinator: elke ronde kent een andere coordinator, zodat het onverdachte proces een keer aan de beurt komt (al weten we niet wanneer). De coordinator stuurt zijn waarde aan iedereen, en deze waarde wordt, indien ontvangen, overgenomen; als iedereen coordinator is geweest wordt beslist. Lemma 8.5 Elk correct process maakt alle ronden af en beslist.

134 124 8 Foutdetectie Bewijs. Terminatie van dit soort algoritmen wordt meestal met inductie naar het rondenummer bewezen. Vòòr ronde 1 wordt niet gewacht, dus elk correct proces bereikt ronde 1. Stel nu dat elk correct proces ronde r bereikt. Als de coordinator (proces r) correct is voltooit hij het zenden aan alle processen en de wachtende processen zullen uitendelijk ontvangen (als ze niet daarvoor r verdenken). Als de coordinator crasht zullen alle wachtende processen r uiteindelijk verdenken wegens compleetheid van de detector (als ze niet daarvoor het bericht hebben ontvangen). In beide gevallen maken alle correcte processen ronde r af en gaan door naar ronde r + 1 (of de beslissing, als r = n). Lemma 8.6 Als aan het begin van ronde r alle processen dezelfde waarde van x hebben, geldt dat aan het eind van de ronde ook. Bewijs. Een proces kan zijn waarde houden of de waarde van de coordinator overnemen, maar in het laatste geval neemt hij (volgens de premisse) dezelfde waarde over als die hij had. Stelling 8.7 Programma 8.6 is een correct consensus-algoritme. Bewijs. De terminatie is in lemma 8.5 bewezen. Wegens zwakke accuratesse is er een proces dat nooit wordt verdacht. In de ronde waarin dit proces coordinator is, breekt geen proces het wachten op een bericht af wegens verdenking van de coordinator. Dus hebben na afloop van die ronde alle processen dezelfde waarde van x, namelijk de waarde die werd verstuurd door de coordinator. Lemma 8.6 zegt dat de processen dan ook tot aan het eind gelijke waarden houden, dus alle beslissingen zijn gelijk (overeenstemming). Voor geldigheid moet je bewijzen (weer met inductie over de ronden) dat de waarde van x steeds een invoerwaarde is. 8.5 Uiteindelijke zwakke accuratesse In programma 8.6 wordt zwakke accuratesse gebruikt, waarmee het na een volledige cyclus van n ronden zeker is dat er een ronde is geweest met een onverdachte coordinator. Is de detector echter slechts uiteindelijk zwak accuraat, dan kunnen er in het begin (willekeurig lang) verdenkingen bestaan tegen alle processen, en pas na verloop van tijd stoppen de verdenkingen tegen tenminste één proces. Het kan dan willekeurig veel ronden duren voordat alle (of bijna alle) processen dezelfde waarde hebben, daarom herhaalt programma 8.7 de ronden, net zolang tot de coordinator overeenstemming constateert. Het programma gebruikt een parameter t die het maximale aantal crashes geeft waartegen het beschermd is, en de werking vereist dat t < n/3. Programma 8.6 gebruikt zo n parameter niet, en is bestand tegen n 1 crashes. Je kunt bewijzen dat, omdat de detectie niet accuraat is maar slechts uiteindelijk accuraat, er altijd zo n grens moet zijn, en dat t < n/2 voor alle mogelijke programmas moet gelden. De coordinator cr van ronde r is het (unieke) proces waarvan het nummer modulo n gelijk is aan r. Elk (levend) proces stuurt zijn waarde aan de coordinator; deze gebruikt nog niet zijn detector, maar wacht tot n t waarden zijn ontvangen. Hij bepaalt de meerderheid (v) en zendt die rond, daarbij aangevend (met bit d) of de ontvangen waarden hieraan unaniem

135 8.5 Uiteindelijke zwakke accuratesse 125 x = invoer ; r = 0 while (true) { r++ // nieuwe ronde cr = (r-1)%n + 1 // bepaal coordinator } // Fase 1: alle processen send (x,r) to cr // Fase 2: alleen coordinator if ( i == cr ) { wait until received n-t maal (vq,r) v = (meerderheid uit ontvangen vq) d = (forall q: vq == v) forall j { send (d,v,r) to j } } // Fase 3: alle processen wait until ( received (d,v,r) from cr OR cr in D ) if ( received (d,v,r) ) { x = v if ( d AND (ouput==@) ) decide(v) // beslis alleen eerste keer } Programma 8.7: Een rotating-coordinator-algoritme met S. gelijk waren. (Let op: dit impliceert niet dat alle correcte processen die waarde hadden aan het begin van de ronde!) De processen die de meerderheidswaarde van de coordinator ontvangen (de enige ontsnapping uit deze ontvangactie is de coordinator te verdenken) nemen die over. Er mag beslist worden als de waarde unaniem was. Merk op dat het programma wel aan terminatie voldoet, maar dat de executies niet eindig zijn: de processen gaan na een beslissing gewoon door om de processen die nog niet beslist hebben ook over de streep te trekken. Het correctheidsbewijs lijkt op dat van programma 8.6 maar de diverse resultaten zijn iets anders geformuleerd. Lemma 8.8 Voor elke r maakt elk correct proces ronde r af. Bewijs. Als lemma 8.5. Lemma 8.9 Als aan het begin van een ronde tenminste n t processen dezelfde waarde v hebben, geldt dat aan het eind van de ronde ook. Bewijs. Een proces kan in de ronde de coordinator verdenken en dan blijft zijn waarde gelijk; deze verandert alleen als hij een bericht met waarde v van de coordinator ontvangt. De coordinator heeft dan n t berichten ontvangen en daar de meerderheid van bepaald. Maar

136 126 8 Foutdetectie er zijn hoogstens t procesen die iets anders hebben dan v, dus onder de n t berichten die de coordinator ontving zijn er minstens n 2t met waarde v. En omdat t < n/3 is n 2t > t, dus de berichten met v zijn bij de coordinator in de meerderheid. De door de coordinator verspreide waarde v is dus gelijk aan v, waarmee bewezen is dat elk proces dat z n waarde verandert, ook v kiest. Lemma 8.10 Er zijn geen tegenstrijdige beslissingen: als p voor v beslist en q voor v, dan v = v. Bewijs. Een beslissing voor v impliceert dat er aan het begin van de ronde tenminste n t processen met waarde v waren; volgens lemma 8.9 blijft dat voor alle latere ronden zo. Een beslissing in dezelfde of een latere ronde is dus altijd ook een beslissing voor v. Lemma 8.11 Elk correct proces beslist. Bewijs. Wegens uiteindelijke zwakke accuratesse komt er een ronde r waarvan de coordinator c nooit meer wordt verdacht. Op zijn laatst in die ronde krijgen alle correcte processen dezelfde waarde; immers, ze ontvangen allemaal het bericht van c. Vanaf die ronde hebben alle correcte processen dezelfde waarde (bewijs conform lemma 8.9). Alle coordinators zullen daarna een uniforme waarde (met d = true) verspreiden. Op zijn laatst in de ronde waarin c weer coordinator is (r + n) wordt dit bericht door elk correct proces ontvangen, en na die ronde hebben alle correcte processen dus beslist. Stelling 8.12 Programma 8.7 is een correct consensus-algoritme met foutdetectie in S. Bewijs. We bewezen overeenstemming in lemma 8.10 en terminatie in lemma Met inductie over de ronden bewijs je dat alle waarden van x ooit een invoer zijn geweest, waarmee geldigheid ook is bewezen. Het resultaat is bevredigend omdat S de zwakste eigenschap van foutdetectors is die werd gedefinieerd (definitie 8.4). Een detector die aan deze zwakste eigenschap voldoet is dus al voldoende om het consensus-probleem op te lossen. Chandra, Hadzilacos en Toueg bewezen in een opmerkelijk artikel [CHT96] dat een nog zwakkere detector niet voldoet, oftewel, dat S de minimale eigenschap is van een detector waarmee consensus kan worden opgelost. In hun constructie gebruiken zij een willekeurige detector die consensus kan bereiken, om consensus te bereiken over tenminste een proces dat niet gecrasht is. Op deze manier blijft uiteindelijk een proces onverdacht, wat een van de eisen is voor een detector in S. Samenvatting en conclusies In gedistribueerde systemen wordt meestal een of ander mechanisme gebruikt waarmee processen informatie verkrijgen over het al dan niet correct functioneren van andere processen. Hiervoor kunnen bijvoorbeeld time-outs worden gebruikt. Om een helder ontwerp te kunnen maken, verdient het aanbeveling om deze functionaliteit niet te verweven met de rest van de applicatie, maar in afzonderlijke modules onder te brengen, de foutdetectors. Foutdetectie kan dan in afzondering worden bestudeerd, waarbij met name

137 Opgaven bij hoofdstuk de vraag voorop staat, hoe goed een foutdetector moet zijn om het consensus-probleem op te kunnen lossen. In dit hoofdstuk worden compleetheid en accuratesse van foutdetectors gedefinieerd, en enkele eenvoudige implementaties met timers worden getoond. Ook werden enkele consensusalgoritmen besproken die met foutdetectors werken. Opgaven bij hoofdstuk 8 Opgave 8.1 In programma 8.1 is (impliciet) aangenomen dat de klokken van de diverse processen exact gelijklopen. Hoe moet het programma worden aangepast aan de meer realistische aanname dat er een verschil van hoogstens ϵ tussen de uitlezingen van verschillende klokken bestaat? Als je dat te makkelijk vindt: de aanname dat de klokken niet even snel lopen maar begrensde drift hebben: in een tijdseenheid neemt de klokwaarde tussen 1 ϕ en 1 + ϕ toe. Opgave 8.2 In het bewijs van stelling 8.1 wordt gebruikt dat als i een waarde voor het eerst zag in ronde n 1, dan was er in elke ronde een proces dat het voor het eerst zag. Waarom is dat zo? Opgave 8.3 Bewijs dat als er hoogstens t processen kunnen crashen, een beslissing in programma 8.1 al genomen kan worden na t + 1 ronden (in plaats van n 1) van communicatie. Opgave 8.4 Zij F een fout-patroon; bewijs dat er een t is waarvoor geldt F (t) = Crash(F ). Waar gebruikt je bewijs dat Π eindig is? Opgave 8.5 Fout-detectie: eisen. (Uit het tentamen van augustus 2000.) Een fout-detector moet over het algemeen voldoen aan eisen van twee typen: compleetheid en accuraatheid. (a) Wat is compleetheid en wat is accuraatheid? (b) Waarom is het erg gemakkelijk een detector te implementeren die aan slechts één van deze twee eisen voldoet? (c) Geef een implementatie van een fout-detector met behulp van timers in een gedistribueerd systeem waar een maximale transmissietijd µ van berichten gegarandeerd is. Opgave 8.6 Sterke accuratesse is sterker dan de eis dat geen enkel correct proces ooit wordt verdacht: F : H D(F ) : t : p Corr(F ), q F (t) : p H(q, t). Geef een voorbeeld van een foutdetector history die hieraan voldoet, maar voor een sterk accurate detector niet is toegestaan. Opgave 8.7 Hoeveel tijd kan er in programma 8.4 verstrijken tussen een crash en de detectie daarvan? Opgave 8.8 Bewijs de uiteindelijke perfectie van programma 8.5. Geldt in elke executie uiteindelijk musch µ? Opgave 8.9 Proces p stuurt een bericht aan q en daarna aan r; dezen wachten op ontvangst van p of verdenking van p. Laat met een voorbeeld zien dat het zelfs ingeval van perfecte detectie mogelijk is dat q het bericht niet, en r het wel ontvangt.

138 128 8 Foutdetectie Opgave 8.10 Perfecte Fout-detectie. (Uit het tentamen van februari 2003.) (a) Hoe luiden de eisen waaraan een perfecte fout-detector moet voldoen? (b) Proces p stuurt een bericht aan q en daarna aan r; dezen wachten op ontvangst van het bericht of het verdenken van p. Laat met een voorbeeld zien dat het, zelfs bij perfecte foutdetectie, mogelijk is dat q het bericht niet, en r het bericht wel ontvangt. Opgave 8.11 Consensus. (Uit het tentamen van februari 2003.) Voor het consensus-probleem bewezen Fischer, Lynch en Paterson een onmogelijkheidsresultaat, toch bestaan hiervoor (1) het algoritme van Ben-Or en (2) enkele rotating-coordinatoralgoritmen. (a) Hoe luidt de stelling van Fischer, Lynch en Paterson? (b) Leg uit waarom deze stelling het algoritme van Ben-Or niet uitsluit. (c) Is Ben-Or s algoritme van het Las Vegas, Monte Carlo of Sherwood type? Leg uit waarom. (d) Waarom sluit de stelling van Fischer, Lynch en Paterson de rotating-coordinatoralgoritmen niet uit? Opgave 8.12 Het bewijs van lemma 8.5 is nogal slordig. Formuleer een nette inductiehypothese en bewijs hem. Opgave 8.13 Bewijs geldigheid van programma 8.6. Opgave 8.14 Rotating Coordinators. (Uit het tentamen van februari 2001.) Het Rotating Coordinator Algoritme (met S) werkt zo: (1) In elke ronde shout de coordinator zijn waarde x; (2) Alle stations wachten tot ze ofwel die waarde ontvangen, danwel de coordinator verdenken, en in het eerste geval nemen ze de waarde van de coordinator over. (a) Wat houden de eigenschappen Accuraat en Compleet voor een fout-detector in? (b) Leg uit waarom correcte processen nooit geblokkeerd raken (wachtend op een bericht) in het Rotating Coordinator Algoritme. (c) Hoeveel ronden zijn nodig en waarom is aan Overeenstemming voldaan? Opgave 8.15 Formuleer programma 8.7 als een eindig algoritme. Opgave 8.16 Verzin een executie van programma 8.7 waarin processen in verschillende ronden beslissen.

139 Hoofdstuk 9 Stabilisatie In dit hoofdstuk wordt een geheel andere benadering van fouttolerantie bekeken, namelijk die van stabiliserende algoritmen. Een algoritme is stabiliserend als het, ongeacht de globale toestand waarin het systeem zich bevindt, convergeert naar correct gedrag van de processen. De algoritmen die tot nu toe in dit boek zijn behandeld, werken alle bij de gratie van de bereikbaarheid van slechts een gedeelte van de mogelijke systeemconfiguraties, zelfs in de aanwezigheid van gebrekige processen. Stabiliserende algoritmen zijn om twee redenen interessant. Ten eerste omdat men zich geen moeite hoeft te geven het algoritme op juiste wijze te initialiseren; ongeacht de begintoestand zal het gewenste gedrag van het systeem na enige tijd op gaan treden. Ten tweede omdat stabiliserende algoritmen een grote mate van robuustheid hebben tegen het optreden van tijdelijke storingen. Zelfs indien een willekeurig groot aantal processen (tijdelijk) Byzantijns gedrag vertoont, zal het algoritme uiteindelijk terugkeren naar correct gedrag. In dit hoofdstuk wordt het onderwerp ingeleid en worden algoritmen voor wederzijdse uitsluiting in een ring van processen behandeld. Sectie 9.1 definiëert stabiliserende en pseudostabiliserende algoritmen, en sectie 9.2 spitst het probleem toe op wederzijdse uitsluiting en behandelt voor dit probleem twee oplossingen gegeven door Dijkstra [Dij74]. In sectie 9.3 wordt een uniforme oplossing voor dit probleem behandeld, die werd gegeven door Burns en Pachl [BP88]. Het hoofdstuk wordt besloten met de methode van Gafni en Bertsekas [GB81] voor routering in dynamische netwerken. 9.1 Begrippen rond stabilisatie In deze sectie wordt formeel gedefiniëerd wat het voor een systeem betekent om te stabiliseren naar een zekere specificatie. De definities en resultaten zijn ontleend aan Burns, Gouda en Miller [BGM93] Systemen en Berekeningen Een (gedistribueerd) systeem is in het hier gebruikte model vastgelegd door de verzameling van mogelijke configuraties van dat systeem en een transitierelatie op die verzameling. Definitie 9.1 Een systeem is een tuple (C, ), waar C een verzameling configuraties is en een binaire relatie op C. 129

140 130 9 Stabilisatie (De gebruikelijke verzameling van toegestane initiële configuraties ontbreekt.) De verzameling van alle configuraties van een gedistribueerd systeem wordt aangeduid als C. Er zal worden aangenomen, dat processen niet communiceren door het uitwisselen van berichten, maar door de mogelijkheid elkaars toestand te lezen. Daardoor bevat een configuratie nooit berichten die onderweg zijn, en zo is C de productruimte van de verzamelingen van toestanden van elk proces. De transitierelatie is een binaire relatie op C, dus C C. (γ, δ) betekent, dat het systeem in één enkele stap kan overgaan vanuit configuratie γ naar configuratie δ. Deze mogelijke transitie wordt weergegeven door de infix-notatie γ δ. Een systeem definiëert een bereikbaarheidsrelatie op configuraties en een collectie berekeningen. Definitie 9.2 In een systeem S = (C, ) is configuratie δ bereikbaar uit configuratie γ, notatie γ δ, als er een reeks (γ 0,..., γ k ) van configuraties is, zodanig dat γ = γ 0, δ = γ k, en γ i γ i+1 voor alle i. Een berekening is een reeks opeenvolgende configuraties, waarbij elke configuratie in een transitie uit de vorige is te bereiken. Er worden slechts maximale berekeningen beschouwd, dus berekeningen die oneindig lang zijn of eindigen in een configuratie van waaruit geen transitie meer mogelijk is. Definitie 9.3 Een berekening van S = (C, ) is een maximale reeks C = (γ 0, γ 1, γ 2, ), waarbij γ i C voor alle i en γ i γ i+1 voor alle i. Deze berekening start in configuratie γ 0. Voor een berekening C is C i de berekening (γ i, γ i+1,...). Zij B de verzameling van alle berekeningen van het systeem Specificaties Het gewenste correcte gedrag van een systeem wordt weergegeven in de formele specificatie van het systeem. De specificatie wordt hier gedefiniëerd als een deelverzameling van alle berekeningen van het systeem. Daar de specificatie onafhankelijk van het systeem moet kunnen worden weergegeven, zal deze in voorkomende gevallen worden gegeven als een predicaat op de verzameling van alle berekeningen. In het geval van wederzijdse uitsluiting is bijvoorbeeld (een deel van) de specificatie: in elke configuratie is precies één proces gemachtigd tot toegang in de kritieke sectie. De volgende definitie is voorlopig echter ook adequaat. Definitie 9.4 Een specificatie van S is een deelverzameling van alle rijen configuraties. Een klassieke correctheidseis voor systemen verlangt simpelweg dat de berekeningen van het systeem volgens de specificatie zijn. Definitie 9.5 Systeem S voldoet aan specificatie F als alle berekeningen van S een element van F zijn, oftewel: C B : C F. Omdat elke configuratie de eerste configuratie van een berekening is, impliceert deze definitie dat elke configuratie voor mag komen in een berekening volgens de specificatie. Het blijkt dan ook, dat deze definitie te weinig specificaties en systemen toelaat om interessant te kunnen zijn. Het is dan ook gebruikelijk om de definitie te beperken tot berekeningen waarvan de eerste toestand element is van een gegeven verzameling van initiële configuraties. Zoals reeds is opgemerkt wordt dit gebruik hier niet nagevolgd. We stellen als doel dat er altijd een configuratie wordt bereikt die een soortgelijk effect heeft: het systeem configureert zichzelf en vanaf dat punt is de berekening conform de specificatie.

141 9.1 Begrippen rond stabilisatie 131 Het systeem stabiliseert naar de specificatie als in elke berekening uiteindelijk een toestand wordt bereikt, van waaruit alleen berekeningen volgens de specificatie mogelijk zijn. Definitie 9.6 Systeem S stabiliseert naar specificatie F als voor alle C B (met C = (γ 0, γ 1,...)) geldt dat er een i is, zodanig dat elke berekening die start in γ i in F zit: C B : i : C = (γ i,...) B : C F. Bij de stabiliserende algoritmen zal steeds worden aangenomen dat de processen communiceren door het lezen van elkaars locale toestand. Het model van asynchrone algoritmen die communiceren door berichten is hier niet adequaat. In een asynchroon algoritme zal elk proces toestanden kennen, waarin het proces geen bericht kan zenden, maar alleen kan ontvangen, anders kunnen de processen oneindig vaak achter elkaar zenden en mag stabilisatie niet worden verwacht. Dan is vanuit een globale configuratie waarin geen berichten onderweg zijn en elk proces wacht op de ontvangst van een bericht, geen enkele transitie mogelijk. Een asynchroon, message-driven algoritme kan dus alleen stabiliseren naar een specificatie die het hangen in elke dergelijke configuratie toestaat. Deze specificaties zijn vrijwel alle zonder practische toepassing Pseudostabilisatie De definitie van stabilisatie eist, dat een systeem een configuratie bereikt waaruit een afwijking van de specificatie niet meer kan voorkomen. Een pseudostabiliserend systeem bereikt een configuratie waaruit een afwijking van de specificatie niet meer voorkomt. Een systeem pseudostabiliseert naar de specificatie als elke berekening een suffix in F heeft. Definitie 9.7 Systeem S pseudostabiliseert naar F als er voor alle C = (γ 0, γ 1,...) B geldt dat er een i is, zodanig dat C i in F zit: C B : i : C i F. Het verschil tussen de twee definities lijkt gering (net als tussen terminatie en terminatie met kans 1 ), maar de definities zijn niet equivalent, zoals in de volgende subsectie bewezen zal worden. Voor sommige specificaties is een stabiliserend systeem onmogelijk, terwijl een pseudostabiliserende oplossing wel mogelijk is, en in de praktijk even goed voldoet. Een voorbeeld is end-to-end controle op berichtoverdracht [Tel91, Hf. 8]. Stelling 9.8 Als S stabiliseert naar een specificatie F, dan pseudostabiliseert S naar F. De omgekeerde implicatie geldt niet. Bewijs. Daar de berekening C i start in γ i, volgt de pseudostabilisatie direct uit de stabilisatie. Beschouw nu een systeem S waar C = {p, q} en = {(p, p), (p, q), (q, q)}. Zij F de specificatie alle toestanden zijn gelijk, formeel bestaande uit twee toegestane oneindige rijen: {(p, p, p,...), (q, q, q,...)}. S stabiliseert niet naar F, omdat in elke configuratie van de berekening (p, p, p,...) de berekening (p, q, q,...) F start. S pseudostabiliseert wel naar F, want elke berekening van S is (p, p, p,...), (q, q, q,...) of van de vorm (p, p,..., p, q,...), en heeft dus een suffix in F.

142 132 9 Stabilisatie Het blijkt, dat voor sommige problemen stabilisatie slechts tegen hoge prijs is te bereiken. Burns et al. [BGM93] beargumenteren echter, dat voor practische doeleinden meestal kan worden volstaan met pseudostabilisatie. Immers, het doet er niet toe of een afwijking van de specificaties mogelijk is of niet, zolang die maar niet voorkomt. Dit standpunt mag pragmatisch gezien bruikbaar zijn, maar een probleem met pseudostabiliserende algoritmen is dat de convergentie naar gedrag in F niet binnen een begrensd aantal stappen plaatsvindt. Stelling 9.9 Zij S een systeem met M configuraties (d.w.z., #C = M). Als S stabiliseert naar F dan geldt C B : i < M : C i F. Wanneer S pseudostabiliseert naar F is dit niet noodzakelijk het geval. Bewijs. Neem aan dat S stabiliseert naar F en zij C B. Kies nu i minimaal, zodanig dat elke berekening die start in γ i, in F zit. Indien i M, dan komt γ i niet voor in γ 0... γ M 1, en daar er precies M configuraties zijn, volgt dat er j 1 < j 2 < M zijn met γ j1 = γ j2. Er is dus een berekening (namelijk die, waarin vanaf γ j2 de reeks van γ j1 naar γ j2 telkens herhaald wordt) waarvan alle configuraties in {γ 0,..., γ j2 } zitten. Uit de stabilisatie volgt dat er een γ j {γ 0,..., γ j2 } is, zodat alle berekeningen die starten in γ j in F zitten, en dit is tegenstrijdig met de keuze van i. Hieruit volgt de convergentie binnen M transities. Dat hetzelfde niet voor pseudostabilisatie geldt wordt aangetoond door hetzelfde voorbeeld uit stelling 9.8. Hier kan een berekening zelfs na willekeurig veel configuraties nog een sprong van p naar q maken. Wanneer bij een pseudostabiliserend systeem een bovengrens gegeven kan worden op het aantal transities waarbinnen convergentie optreedt, is dat systeem stabiliserend (zie opgave 9.1) Gesloten eigenschappen en normen Vaak gebruikt men bij gedistribueerde algoritmen een invariant als een eigenschap waarvan te bewijzen is dat die tijdens elke executie altijd geldt. Omdat bij stabiliserende algoritmen elke configuratie in een berekening kan voorkomen, is het niet mogelijk een niet-triviale invariant aan te geven. Een eigenschap is gesloten als hij onder transities behouden blijft: Definition 9.10 Eigenschap P van configuraties is gesloten als voor elke transitie γ δ geldt: als P (γ) dan P (δ). Om te bewijzen dat een zekere verzameling van legale configuraties ooit wordt bereikt, gebruikt men een norm. Definition 9.11 Een functie ϕ op configuraties is een norm als 1. Het bereik van ϕ is welgeordend (dwz., er is een totale ordening, maar de verzameling uitkomsten bevat geen oneindige dalende rijen). 2. Voor elke transitie γ δ geldt: ϕ(γ) > ϕ(δ) of δ is legaal. De eigenschappen van een stabiliserend systeem kunnen nu in de volgende stappen worden beredeneerd. Eerst toont men aan, dat elke configuratie tenminste een transitie toestaat; dit impliceert dat elke berekening oneindig lang is. Vervolgens toont men aan dat elke oneindige rij een legale configuratie bereikt; bv. door een norm te gebruiken. Tenslotte laat men zien, dat een berekening die eenmaal in een legale configuratie komt, altijd legaal blijft (geslotenheid van legaal-zijn), en aan de specificatie voldoet.

143 9.2 Uitsluiting: Niet-Uniforme Oplossingen 133 Welordeningen. Een verzameling X met daarop een totale ordening (X, <) is een welordening (Engels: well-order 1 ) als elke deelverzameling Y van X een kleinste element heeft. Deze definitie is equivalent met de door ons gebruikte eigenschap dat X geen oneindig lange dalende rijen bevat. Stelling 9.12 Zij (X, <) een totale ordening. De eigenschappen 1. Elke niet-lege deelverzameling Y van X bevat een kleinste element. 2. Elke dalende rij in X is eindig. zijn equivalent. Bewijs. Stel (niet 1): er is een deelverzameling Y van X die geen kleinste element heeft. Dus voor elke a Y is er een a Y waarvoor a < a. Neem dan a 0 Y, a 1 uit Y die kleiner dan a 0 is, en bij elke a i, kies een a i+1 Y zdd. a i+1 < a i. De gekozen elementen a i vormen een oneindige dalende rij (niet 2). Stel (niet 2) dat X een oneindig lange dalende rij bevat. Neem Y als de verzameling van alle elementen die in die rij voorkomen. Elk element uit de rij heeft een kleiner element in de rij (namelijk de volgende), wat aantoont dat Y geen kleinste element bevat (niet 1). We geven nog enkele voorbeelden van verzamelingen die wel of niet een welordening zijn. De verzameling van natuurlijke getallen is welgeordend. Immers, een dalende rij begint met een zeker, eindig getal. Elk volgend getal is kleiner, en daarmee is de lengte van de rij meteen begrensd door de eerste waarde plus 1. De verzameling van alle gehele getallen is niet welgeordend, immers, de rij (0, 1, 2, 3, 4,...) kan oneindig lang worden voortgezet. De reële en rationele getallen vormen geen welordening, ook niet als hiervan een begrensd interval wordt bekeken. Immers, de verzameling {x : 1 < x 2} heeft geen kleinste element (zowel voor reële als rationele getallen). De verzameling paren van natuurlijke getallen is welgeordend, een verzameling van alle eindige woorden (lexicografisch geordend) is dat niet (zie opgave 9.2 en 9.3). 9.2 Uitsluiting: Niet-Uniforme Oplossingen In de komende twee secties wordt het probleem van wederzijdse uitsluiting van processen op een ring bestudeerd. Elk proces is hierbij een eindige automaat P i met een verzameling Σ i van toestanden. Er geldt dan C = Σ 0... Σ N 1. De transities zijn toestandsovergangen van één automaat, en zijn alleen afhankelijk van de toestand van deze automaat en zijn buren in de ring. De transitierelatie i van proces P i is een relatie van Σ i 1 Σ i Σ i+1 naar Σ i. (De indices van processen worden altijd gerekend modulo N.) Hier betekent ((a, b, c), d) i dat, wanneer P i 1 in toestand a is, P i in toestand b en P i+1 in toestand c, dan kan P i overgaan in toestand d. Deze (mogelijke) transitie wordt genoteerd als a b c i d of a b c d. De specificatie voor het geval van wederzijdse uitsluiting is als volgt. In elke configuratie is er precies één proces gemachtigd (om de kritieke sectie uit te voeren), en in een berekening komt elk proces oneindig vaak aan de beurt. In de oplossingen die volgen geldt steeds (na convergentie) dat er één proces is waarvoor er een transitie mogelijk is, en per decreet is dit dan het proces dat de kritieke sectie uitvoert. In het vervolg zeggen we dat een proces een volmacht heeft als er een transitie van dat proces mogelijk is. 1 Zie

144 134 9 Stabilisatie Kader 9.1: De Dijkstra-Prijs. Met de uitvinding van de self-stabilizing token ring legde Dijkstra de basis voor de studie van stabiliserende algoritmen. Dijkstra legde zijn oplossingen altijd uit in termen van abstracte modellen, met helder geformuleerde wiskundige eigenschappen. De noodzaak van helder werken beschreef hij in dit citaat: Elegance is not a dispensable luxury but a factor that decides between success and failure. Over de relatie met computers zei de immer Engels sprekende Nederlander: Computer science is no more about computers than astronomy is about telescopes.. Om Dijkstra te eren voor deze bijdrage en veel andere dingen die hij voor het gedistribueerd rekenen heeft betekend, wordt vanaf 2000 jaarlijks de Dijkstra-prijs toegekend voor baanbrekend werk in het vakgebied. Om de prijs te krijgen, moet een bijdrage minstens tien jaar lang bewezen hebben een grote impact te hebben, maar de prijs wordt vaak aan nog ouder werk toegekend. De eerste gelauwerde was Leslie Lamport, die de prijs in 2000 ontving voor een 22 jaar oud werk, Time, Clocks, and the Ordering of Events in a Distributed System uit Vlak voor zijn dood, in 2002, ontving Dijkstra zelf de Dijkstra-prijs voor zijn paper Selfstabilizing systems in spite of distributed control uit Definitie 9.13 Een ring S stabiliseert naar wederzijdse uitsluiting als er een deelverzameling L C is (de legale configuraties) die aan de volgende eisen voldoet. 1. (Uitsluiting.) In elke configuratie van L heeft precies één proces een volmacht. 2. (Gesloten.) Voor elke λ L en λ µ geldt µ L. 3. (Fairness.) Voor elke i bevat elke berekening die start in een legale configuratie oneindig veel transities van P i. 4. (Convergentie.) Elke berekening van S bevat een legale configuratie. Een direct gevolg van de definitie is dat alle berekeningen oneindig zijn. Lemma 9.14 Zij S een ring die stabiliseert naar wederzijdse uitsluiting. Voor alle γ C is er een δ zodanig dat γ δ. Bewijs. Zij γ C. Als γ L, dan volgt uit de fairness dat er een volgende configuratie bestaat. Als γ L, merk op dat (convergentie) elke berekening die start in γ een configuratie in L bevat. Er is dus een opvolgende configuratie. Omdat deze zogenaamde deadlock-vrijheid een belangrijke stap is in het correctheidsbewijs van elk systeem, namen Burns en Pachl [BP88] hem op in de definitie. De ring heet uniform als alle processen gelijk zijn, dus Σ 0 =... = Σ N 1 en i =... = N 1. De ring heet gericht (unidirectional) indien de transities van P i slechts afhangen van P i en P i 1. In dat geval wordt de transitie genoteerd als a b d.

145 9.2 Uitsluiting: Niet-Uniforme Oplossingen 135 Symmetrie kan in gedistribueerde systemen niet worden gebroken met deterministische algoritmen. Het is dan ook eenvoudig te bewijzen dat een uniforme oplossing niet mogelijk is wanneer de ringgrootte geen priemgetal is. Het volgende bewijs werd gegeven door Dijkstra [Dij82]. Stelling 9.15 Er bestaat geen uniforme ring die stabiliseert naar wederzijdse uitsluiting waarin het aantal processen, N, samengesteld is. Bewijs. Zij N samengesteld (geen priemgetal), en kies n, 2 n < N een deler van N. Noem een configuratie γ = (σ 0,..., σ N 1 ) symmetrisch als voor alle i geldt σ i = σ i+n. Omdat in een symmetrische configuratie het aantal volmachten een veelvoud van N/n is, is een symmetrische configuratie niet legaal. Uitgaande van een symmetrische configuratie kan een berekening worden gevonden waarin oneindig veel symmetrische configuraties voorkomen. Hiertoe wordt de eerste transitie die vanuit die configuratie mogelijk is toegepast op N/n regelmatig over de ring verdeelde processen. Er wordt dus in deze berekening nooit een legale configuratie bereikt. In een oplossing voor algemene N zijn de processen dus niet alle gelijk. Dijkstra heeft verschillende oplossingen voor het probleem van wederzijdse uitsluiting gegeven in 1974 [Dij74]. In subsectie wordt een gericht protocol behandeld, waarbij het aantal toestanden van een proces afhangt van N. In subsectie wordt een protocol behandeld dat slechts een constant aantal toestanden voor elk proces kent, maar niet gericht is Een Gerichte Oplossing De gerichte oplossing van Dijkstra [Dij82] bevat één proces dat verschilt van de andere (namelijk P 0 ), en de andere processen zijn gelijk. De oplossing staat bekend als Dijkstra s token ring. In de oplossing is het al dan niet hebben van een volmacht gedefiniëerd in termen van gelijkheid van toestanden. De toestandsruimte is voor elk proces gelijk, dus Σ 0 =... = Σ N 1. De in [Dij82] gemaakte keuze van de conditie waaronder een proces een volmacht heeft impliceert direct dat er altijd tenminste één volmacht is, dus dat elke berekening oneindig is. Proces P i (i > 0) heeft een volmacht wanneer σ i σ i 1. Dus, wanneer geen der processen P 1,..., P N 1 een volmacht heeft geldt (uit de transitiviteit van de gelijkheid) σ 0 = σ N 1. Daarom heeft P 0 een volmacht wanneer σ 0 = σ N 1, waarmee het volgende resultaat onmiddelijk volgt. Propositie 9.16 In elke configuratie heeft tenminste één proces een volmacht. Vervolgens wordt de mogelijke transitie voor een proces met een volmacht bepaald. Deze transitie wordt zo gekozen, dat het proces zijn volmacht verliest (maar mogelijk doorgeeft aan een ander proces). Voor een deadlock door het verlies van alle volmachten hoeft men niet te vrezen omdat reeds bewezen is dat er in elke toestand tenminste één volmacht is. Om zijn volmacht te verliezen maakt P i (als het een volmacht heeft) zijn toestand gelijk aan σ i 1 ; door de eerder gemaakte keuze is P i de volmacht dan kwijt. De transitie is als volgt (voor i 0). if a b then a b a Proces P 0 maakt juist door een transitie zijn toestand ongelijk aan die van zijn voorganger P n 1. Neem aan dat de toestand van een proces een getal modulo K is (voor zekere K > 1) en zij nu de transitie voor P 0 : a a (a + 1) mod K

146 136 9 Stabilisatie Dit lijkt een nogal specifieke keuze op dit punt, maar het is hier de meest algemene, zinvolle keuze, zoals aan het eind van deze subsectie zal worden beargumenteerd. Voor alle processen geldt nu dat ze door een transitie hun volmacht verliezen, en bovendien dat ten gevolge van een transitie in proces P i geen ander proces een volmacht kan krijgen dan proces P i+1. Het aantal volmachten op de ring neemt dus door geen enkele transitie toe, waarmee het volgende resultaat onmiddelijk volgt. Propositie 9.17 Zij λ een configuratie met één volmacht en λ µ. configuratie met één volmacht. Dan is µ weer een In een configuratie met één volmacht (zeg in proces P i ) is alleen een transitie in proces P i mogelijk, en hierdoor ontstaat een configuratie waarin er slechts één volmacht is, en wel in proces P i+1. Het volgende resultaat volgt onmiddelijk. Propositie 9.18 Elke berekening die begint in een configuratie met één volmacht bevat oneindig veel transities van elk proces. Vervolgens wordt bewezen dat elke berekening een configuratie bevat met precies één volmacht. Het bewijs gebruikt het volgende hulpresultaat. Propositie 9.19 Een reeks transities zonder een transitie van P 0 is eindig. Bewijs. Dit wordt bewezen door het aangeven van een normfunctie op de configuraties die strikt daalt voor elke transitie van een proces ongelijk P 0. Kies hiervoor de som over alle volmachten, van het aantal stappen dat dit proces nog af ligt van P 0 : definieer F (γ) = ( (N i) ). i>0, P i heeft volmacht Elke transitie van een proces P i P 0 doet F afnemen. Immers, de volmacht verdwijnt of wordt overgedragen aan P 0 of aan een proces met hogere index. Omdat N welgeordend is, volgt dat er slechts eindig veel transities zonder P 0 mogelijk zijn. (Omdat de waarde van F begrensd is door 1 2 N 2 volgt zelfs een concrete bovengrens op het aantal achtereenvolgende stappen dat kan plaatshebben zonder P 0.) Lemma 9.20 Als K N bereikt elke berekening een configuratie met precies één volmacht. Bewijs. Uit lemma 9.16 volgt dat elke berekening oneindig is en uit lemma 9.19, dat P 0 oneindig vaak een stap doet. Omdat het aantal toestanden (K) tenminste het aantal stations (N) is, en P 0 voor zijn stap dezelfde toestand heeft als zijn buur, is er voor de eerste stap van P 0 een toestand, zeg a, die niet voorkomt. Telkens als P 0 een stap doet, verhoogt hij zijn toestand met 1, wat impliceert dat hij de toestand a ooit bereikt, die dan uniek is. Deze nieuwe toestand a kan alleen door kopiëren opschuiven naar de andere processen; immers die creëren geen toestanden, maar kopiëren alleen maar. De volgende keer dat P 0 een stap doet, heeft ook P N 1 toestand a. Omdat de toestand van P 0 in de tussentijd steeds a was, en de processen alleen toestanden van voorgangers kopiëren, impliceert dit, dat op dat moment alle processen toestand a hebben. Bij de volgende stap van P 0 is hij dus de enige met een volmacht. Stelling 9.21 Het hierboven gedefiniëerde systeem stabiliseert naar wederzijdse uitsluiting.

147 9.2 Uitsluiting: Niet-Uniforme Oplossingen 137 Kader 9.2: Rock, Paper, Scissors De toestandsruimte van de compacte oplossing is geïnspireerd op het spel Papier, Steen, Schaar. Met de hand wordt een van deze drie uitgebeeld: een vlakke hand is papier, een vuist is een steen, gespreide vingers zijn een schaar. Hierbij is papier beter dan een steen (want papier pakt een steen in), een steen beter dan een schaar (want een steen maakt de schaar bot) en een schaar beter dan papier (de schaar knipt papier). Als de spelers een gelijk gebaar maken is het remise. De drie symbolen worden door 0, 1 en 2 gerepresenteerd. Wanneer P 0 wint van P 1 kiest hij die waarde waarmee hij verliest van P 1. Wanneer een proces in het middenstuk wint van een van zijn buren, neemt hij die waarde over. Als P N 1 wint van zijn buur, verandert hij zijn waarde zodat de buur winnaar wordt. Om te voorkomen dat bij allemaal gelijke waarden het systeem stopt, kan P N 1 (als enige) een stap doen wanneer zijn waarde gelijk is aan die van beide buren. Bewijs. Kies L als de verzameling configuraties met één volmacht. De uitsluitingseis is dan precies de definitie van L, de geslotenheid is bewezen in propositie 9.17, de fairness in propositie 9.18, en de convergentie in lemma Tenslotte komen we terug op de keuze van de toestandsruimte en de vorm van de transitie van P 0. Met de tot op dat moment reeds gemaakte keuzen moet de transactie, om de volmacht van P 0 te verliezen, van de vorm a a f(a) zijn, waar f een functie op Σ 0 is. Als er uiteindelijk een configuratie wordt bereikt waarin er steeds één proces een volmacht heeft, dan komen er in alle volgende configuraties slechts toestanden voor uit een lus die door de functie f wordt gedefiniëerd in Σ 0. Het is geen verlies van algemeenheid om de toestanden in deze lus te nummeren van 0 tot K 1 (waarna f een verhoging modulo K wordt, en de transitie de eerder gegeven vorm krijgt). De aanname dat de gehele toestandsruimte slechts uit deze lus bestaat sluit geen oplossingen uit, immers, het is altijd mogelijk dat wordt uitgegaan van een configuratie waarin alle toestanden elementen van deze lus zijn Een Compacte Oplossing In deze subsectie wordt een oplossing van Dijkstra [Dij86] behandeld waarin elk proces slechts drie toestanden heeft, Σ i = {0, 1, 2} voor elke i. Optelling in deze verzameling is modulo 3. Omdat de oplossing uit de vorige subsectie ook slechts drie toestanden gebruikt als N 3, wordt verder aangenomen dat N > 3. De kleinere toestandsverzameling heeft wel een prijs: de oplossing is niet langer gericht. Processen 0 en N 1 hebben afwijkende transities, de overige processen zijn alle gelijk. De transities zijn als volgt. Voor proces P 0 : x a a + 1 a + 2.

148 138 9 Stabilisatie Voor stap Na stap Proces type s i s i+1 s i s i+1 Y P 0 1 (n.v.t.) (n.v.t.) P i 3 0 (niet P of P N 1 ) P N 1 7 (n.v.t.) (n.v.t.) 1 8 (n.v.t.) (n.v.t.) +1 Tabel 9.3: Effect van de transities op de symbolen. Voor proces P i (0 < i < N 1): Voor proces P N 1 : if a = b + 1 or c = b + 1 then a b c b + 1. if b a + 1 then a b a a + 1. Merk allereerst op dat als alle toestanden gelijk zijn, proces P N 1 (als enige) een volmacht heeft. De operatie van dit systeem wordt duidelijker onder de volgende transformatie. Beschrijf de configuratie als een string van N 1 symbolen (s 1,..., s N 1 ), waarbij s i = als σ i 1 = σ i, s i = als σ i 1 = σ i 1 en s i = als σ i 1 = σ i + 1. Om convergentie te kunnen bewijzen gebruiken we als norm de waarde Y, het aantal pijlen naar links plus tweemaal het aantal pijlen naar rechts: Y (γ) = #( ) + 2 #( ). Vervolgens kijken we, wat elke transitie doet in termen van de s i en hoe Y ten gevolge van een transitie verandert. Propositie 9.22 Tabel 9.3 geeft het effect van de transities weer in termen van de symbolen s i en de functie Y. Propositie 9.23 In elke configuratie is er minstens één volmacht. Bewijs. Als er geen pijl is, zijn alle toestanden gelijk en heeft P N 1 een volmacht. Als er een pijl is, heeft het proces waarnaar die pijl wijst een volmacht. Zij L de verzameling configuraties waarvoor er precies één pijl in de string is. Propositie 9.24 In elke configuratie van L is er precies één volmacht. Bewijs. Zoals uit tabel 9.3 blijkt, is elke transitie (behalve van type 8) alleen mogelijk als er een pijl naar het betreffende proces wijst. Als er precies één pijl is, is een transitie van type 8 niet mogelijk; want s N 1 is een pijl of σ N 2 σ 0. Dus heeft ten hoogste één proces een volmacht. Dat er minstens één volmacht is, is al in propositie 9.23 bewezen. Met behulp van tabel 9.3 is het niet moeilijk, te analyseren wat er gebeurt indien het systeem een legale configuratie bereikt. Propositie 9.25 Zij γ L en γ δ, dan δ L.

149 9.3 Uitsluiting: Een Uniforme Oplossing 139 Bewijs. Zoals uit tabel 9.3 blijkt, is er na een transitie waarin slechts sprake is van één pijl (type 1, 2, 3 of 7), steeds weer één pijl. Propositie 9.26 In een berekening die start in een configuratie met één pijl krijgt elk proces oneindig vaak een volmacht. Bewijs. De pijl verschuift (in de richting van de pijl) tot het eind van de string (transities van type 2 en 3) en keert bij het eind om (transities van type 1 en 7). In een cyclus van de pijl komt van elk proces minstens een transitie voor. Er rest te bewijzen, dat een legale toestand in een eindig aantal transities wordt bereikt. Propositie 9.27 Een reeks transities zonder transitie van P 0 is eindig. Bewijs. Beschouw een reeks transities zonder een transitie van P 0. Uit de transitie voor P N 1 volgt dat er tussen elke twee transities van P N 1 minstens een transitie van P 0 zit; een transitie van P N 1 zet σ N 1 op σ 0 + 1, en zolang σ N 1 = σ heeft P N 1 geen volmacht. Een reeks van transities zonder transitie van P 0 bevat dus ten hoogste een transitie van P N 1. Dus neemt Y in de reeks transities slechts eenmaal toe, en zo zijn er maar eindig veel transities van type 4 en 5. In een transitie van type 6 neemt het aantal pijlen af en daar zijn er dus ook maar eindig veel van. Elk type transitie komt maar eindig vaak voor; dus is de reeks eindig. Proposities 9.23 en 9.27 impliceren dat elke berekening oneindig is, en dat in elke berekening P 0 oneindig veel transities doet. Lemma 9.28 Elke berekening bereikt binnen eindig veel transities een legale configuratie. Bewijs. Als de berekening begint zonder pijl heeft (alleen) P N 1 een volmacht, en na één transitie is een legale configuratie bereikt. Neem verder aan dat er tenminste één pijl is. Elke berekening is oneindig (propositie 9.23) en er komen oneindig veel transities van P 0 voor (propositie 9.27). Voor een transitie van P 0 wijst de meest linkse pijl naar links, maar na de transitie van P 0 wijst de meest linkse pijl naar rechts. Dus is er telkens tussen twee transities van P 0 een transitie waardoor de uitspraak De meest linkse pijl wijst naar rechts onjuist wordt. Dat kan alleen gebeuren in een transitie van type 4, 5 of 7. Als het gebeurt in een transitie van type 7 is s N 1 op dat moment de meest linkse pijl, en dus is er slechts één pijl en de configuratie is legaal. Als het gebeurt in een transitie van type 4 of 5 is er een netto afname van Y met tenminste 1 tussen de twee transities van P 0 ; immers, een transitie van P 0 en P N 1 doen Y samen met ten hoogste 2 toenemen, en de transitie van type 4 of 5 die er tussen zit doet Y met 3 afnemen. Het kan dus maar voor eindig veel (namelijk, O(N)) opeenvolgende transities van P 0 het geval zijn dat er geen legale configuratie is bereikt. 9.3 Uitsluiting: Een Uniforme Oplossing In deze sectie wordt een uniforme oplossing voor het probleem van stabilisatie naar wederzijdse uitsluiting behandeld. De oplossing is gericht, gebruikt Ω(N 2 ) toestanden in elk proces en werd gegeven door Burns en Pachl [BP88].

150 140 9 Stabilisatie De toestand van een proces is een tweetal bestaande uit een label en een tag. Beide zijn een getal uit Z N 1, en bovendien is de tag nooit 1. De toestandsruimte voor elk proces is dus formeel gegeven als volgt. Σ = {a.t : a {0,, N 2} en t {0} {2,, N 2} } Merk op dat #Σ = (N 1)(N 2). Alle aritmetiek die op labels en tags wordt bedreven is modulo N 1. Alle aritmetiek op proces indices is modulo N. De transities van het systeem (voor alle processen gelijk, want het systeem is uniform) zijn van twee types. A. if b a + 1 and (b 0 or t b a or t < u or t = 0) then a.t b.u (a + 1).(b a) B. if t u and a then a.t (a + 1).u (a + 1).t De verzameling legale configuraties is als volgt gedefiniëerd. Van elk proces is de tag gelijk aan 0. Van precies één proces is de label gelijk aan het label van zijn voorganger, van elk ander proces is het label 1 hoger dan dat van zijn voorganger. Om dit formeel te omschrijven, schrijf σ i = l i.t i, en dan is L gedefiniëerd als L = {λ C : ( i : t i = 0) ( i : (l i = l i 1 ) ( j i : l j = l j 1 + 1))} Legale configuraties zijn dus cyclische verschuivingen van de volgende reeks toestanden: (i 1).0 i.0 i.0 (i + 1).0 (N 2).0. In deze configuratie heeft het proces met onderstreepte toestand een volmacht. Het is niet moeilijk het gedrag van het systeem te analyseren wanneer eenmaal een legale toestand is bereikt. Lemma 9.29 In elke legale configuratie heeft precies één proces een volmacht, een transitie vanuit een legale configuratie brengt het systeem weer in een legale configuratie, en elke berekening die start in een legale configuratie bevat oneindig veel transities van elk proces. Bewijs. Zij γ een legale configuratie en P i het proces waarvoor l i = l i 1. Transities van type B zijn niet mogelijk, daar alle tags gelijk zijn. Een transitie van type A is mogelijk voor proces P i omdat l i = l i 1, dus l i l i 1 + 1, en t i 1 = 0. Voor een proces P j P i is een transitie van type A niet mogelijk, omdat l j = l j Proces P i is dus het enige proces met een volmacht. De enige transitie die mogelijk is in configuratie λ is de transitie van type A door P i. Na deze transitie geldt nog steeds t i = 0, en er geldt l i = l i = l i+1. Dus is de nieuwe configuratie weer legaal, maar nu met P i+1 het (enige) proces waarvoor het label gelijk is aan dat van de voorganger. In elke transitie vanuit een legale configuratie wordt de volmacht overgedragen aan het volgende proces in de ring. De volmacht circuleert dus en komt inderdaad oneindig vaak bij elk proces.

151 9.3 Uitsluiting: Een Uniforme Oplossing 141 Het bewijs van de convergentie van dit algoritme is werkelijk zeer gecompliceerd en daaraan is de rest van deze sectie dan ook geheel gewijd. Net als bij het bewijs van het compacte systeem van Dijkstra wordt de werking van het systeem duidelijker wanneer er wat abstracter tegenaan wordt gekeken. Dit keer niet in termen van pijlen en strepen, maar van segmenten en gaten, die door transities over de ring kunnen schuiven. De volgende definitie beschrijft gaten en segmenten precies binnen de context van een vaste configuratie. Definitie 9.30 Zij γ C een configuratie. Er zit in γ een gat tussen twee opeenvolgende processen P i en P i+1 als l i+1 l i + 1. Dit gat wordt genoteerd als (l i.t i, l i+1.t i+1 ) en het heeft grootte g(i, γ) = l i+1 l i. Een segment van γ is een maximale reeks opeenvolgende processen s = (P i,..., P j ) zonder gat. De gat-grootte van s is g(j, γ). P i is het linkereinde en P j het rechtereinde van s. Segment s is nul-gericht als l i = 0. Merk op, dat de segmenten en gaten alleen afhangen van de labels van de processen; de tags doen in deze definitie niet terzake. Voor de zinvolheid van de verdere uitspraken is het van belang tevens op te merken dat er in elke configuratie tenminste één gat is. Dit volgt uit vergelijking 9.1, waarin k het aantal gaten is en de gat-grootte wordt gesommeerd over alle gaten. (N k) + gaten h g(h) = 0 (mod N 1). (9.1) Het wordt aan de lezer overgelaten zich van de juistheid hiervan te overtuigen. Een voorbeeld. Zij γ de volgende configuratie van 7 processen De configuratie heeft 2 gaten, namelijk (3.4, 5.2) en (1.4, 0.4), van grootte 2 en 5. Er zijn dus ook twee segmenten, namelijk (P 6, P 0, P 1, P 2 ) en (P 3, P 4, P 5 ). Lemma 9.31 Als N een priemgetal is, dan is er in elke configuratie tenminste één proces met een volmacht. Bewijs. Bekijk transities van type A en merk op dat die transitie mogelijk is in het linkereinde van elk segment (conditie b a + 1) dat niet nul-gericht is (conditie b 0). Indien γ dus segmenten bevat die niet nul-gericht zijn dan is een transitie van type A mogelijk. Veronderstel daarom verder dat γ alleen nul-gerichte segmenten bevat. Als de configuratie slechts uit één segment bestaat dat nul-gericht is, dan is er slechts één gat, zeg (a.t, b.u) en er geldt dan a = 0. Dit volgt uit b = 0 en de lengte van het (enige) segment. Dus is b a = 0 en daaruit volgt dat t b a of t = 0, waarmee transitie A mogelijk is. Veronderstel daarom verder dat er meerdere segmenten zijn. Een segment heeft een lengte van ten hoogste N 1 en de labels in een segment zijn dus verschillend. Het label in het linkereinde is 0, dus alle andere labels zijn ongelijk 0. Als er een segment is waarin niet alle tags gelijk zijn, is in een proces in dat segment transitie B mogelijk. Veronderstel daarom verder dat in een segment alle tags gelijk zijn. Als er twee segmenten zijn waarvan de tags verschillen, dan is er in de ring een plaats waar de tags dalen dus is er een gat (a.t, b.u) met t < u, en dan is transitie A mogelijk. Veronderstel daarom verder dat alle tags gelijk zijn. Als er een gat (a.t, b.u) is waar t ongelijk is aan de gat-grootte dan is weer

152 142 9 Stabilisatie transitie A mogelijk; veronderstel daarom verder dat voor elk gat t gelijk is aan de gat-grootte. Omdat nu elk segment nul-gericht is en gelijke gat-grootte heeft, volgt dat elk segment even lang is. Maar nu kan worden gebruikt dat N een priemgetal is: er volgt dat er N segmenten zijn, elk van lengte 1. Dus zijn alle labels 0 en ook alle gat-groottes (en dus de tags) 0; en dus is transitie A mogelijk in elk proces. Zo is dus elke berekening oneindig lang. Er zal nu worden aangetoond, dat het aantal segmenten niet toeneemt tijdens een berekening, en elke berekening heeft dus een oneindig lange suffix waarin het aantal segmenten constant is. Propositie 9.32 Als γ δ dan heeft δ ten hoogste zoveel segmenten als γ. Bewijs. Transitie B verandert geen labels en heeft dus geen invloed op de ligging en het aantal der segmenten. Transitie A wordt slechts uitgevoerd door het linkereinde van een segment (conditie b a + 1) en door de transitie verdwijnt dit gat. Een nieuw gat kan alleen worden gecreëerd tussen het actieve proces en zijn opvolger (waardoor het gat een plaats naar rechts verschuift ). Het aantal gaten neemt dus niet toe. Merk op, dat het mogelijk is dat ten gevolge van een transitie van type A het aantal segmenten afneemt. Dit is het geval bij toepassing van de transitie in het onderstreepte proces in de volgende situatie (waarin gaten met een verticale streep worden weergegeven).... a.t b.u (a + 2).v... De configuratie na toepassing van transitie A is... a.t (a + 1).(b a) (a + 2).v.... Noem vanaf nu een berekening waarin het aantal segmenten constant is een stille berekening. Uit propositie 9.32 volgt, dat elke berekening een suffix heeft die een stille berekening is. We gaan aantonen dat in een stille berekening het aantal segmenten gelijk is aan 1. Zij C = (γ 0, γ 1,...) een stille berekening. De gaten in de configuraties schuiven over de ring en de plaats van die gaten wordt weergegeven door een functie die we een schuivend gat noemen. Definitie 9.33 Een schuivend gat van C is een functie z : N Z N waarvoor geldt: 1. z(0) is het rechtereinde van een segment in γ Als γ j+1 uit γ j wordt verkregen door een transitie van type A in proces P z(j)+1, dan is z(j + 1) = z(j) + 1, anders is z(j + 1) = z(j). Voor een schuivend gat z is steeds in configuratie γ j P z(j) het rechtereinde van een segment van γ j. Dit is het geval omdat er, per definitie, geen gaten verdwijnen in een stille berekening. Noem dit segment s z (j), en nu is s z een functie van N naar segmenten die een schuivend segment wordt genoemd. De gat-grootte van een schuivend segment verandert bij het schuiven niet. Propositie 9.34 Voor een schuivend gat z geldt g(z(j), γ j ) = g(z(j + 1), γ j+1 ). Bewijs. Dit wordt aan de lezer over gelaten.

153 9.3 Uitsluiting: Een Uniforme Oplossing 143 Noem de constante waarde van de gat-grootte g(z). Bedenk dat de term gat-grootte slaat op het verschil in label tussen de processen aan weerszijden van een gat. De lengte van een schuivend segment is niet constant; een schuivend segment kan groeien en krimpen wanneer de gaten waardoor het wordt begrensd in ongelijke stappen verschuiven. Het volgende lemma beschrijft dat de segmenten inderdaad van plaats veranderen, en zegt iets over de toestanden van de processen rond een gat. Lemma 9.35 Elk proces voert oneindig veel transities van type A uit. Voor een schuivend gat z geldt j : (σ z(j) in γ j ) = 0.g(z). Bewijs. Er is al bewezen dat elke berekening oneindig is. Een stap van type B verandert niets aan de segmenten, en in elk segment kan hij slechts eindig vaak worden uitgevoerd totdat alle tags in het segment gelijk aan elkaar zijn. Dus zijn er oneindig veel transities van type A. Wanneer proces P i een transitie van type A heeft gedaan geldt l i = l i en P i kan geen transitie van type A meer doen totdat l i 1 is veranderd, dus tot P i 1 een transitie van type A heeft gedaan. Hieruit volgt dat elk proces oneindig veel transities van type A maakt. Een schuivend gat blijft dus inderdaad voortgang maken bij het schuiven. Telkens nadat het gat opschuift (door een transitie van type A) is de tag van het rechtereinde (dus, van z(j)) gelijk aan de gat-grootte van het segment. Het label van het nieuwe rechtereinde is steeds 1 groter dan het label van het vorige rechtereinde, dus dit is na enige tijd gelijk aan 0. Noem een segment mooi als alle tags in dat segment gelijk zijn aan de gat-grootte. Lemma 9.36 Als er een configuratie in C is waarin een schuivend segment z mooi is, dan blijft z in alle volgende configuraties mooi. In elke stille berekening is er een configuratie waarin alle segmenten mooi zijn. Bewijs. Geen proces in een mooi segment kan transitie B uitvoeren. Het linkereinde kan transitie A uitvoeren en bij een ander segment gaan horen, maar daarmee wordt het mooi zijn van het segment niet verstoord. Het segment wordt niet aan de linkerkant uitgebreid door een fusie van segmenten, omdat de berekening stil is. Het segment kan aan de rechterzijde met een proces worden uitgebreid, maar dit proces heeft dan ook de tag gelijk aan de gat-grootte van het segment, zie transitie A. Vervolgens bewijzen we dat elk segment mooi wordt. Wegens lemma 9.35 is er een configuratie γ j1 waarin het rechtereinde P z(j1 ) van een schuivend segment s z de toestand 0.g(z) heeft, en een latere configuratie γ j2 waarin proces P z(j1 ) het linkereinde van dat segment is geworden. In configuratie γ j2 hebben alle processen in het segment hun tag verkregen door het uitvoeren van transitie A (dan is de tag gelijk aan g(z)) of door het (indirect) copieren van de tag van P z(j1 ) of een nieuwer proces in het segment. Het is hier belangrijk, op te merken dat P z(j1 ) niet de transitie B mag uitvoeren. Er volgt dat het segment in configuratie γ j2 mooi geworden is. Omdat er slechts eindig veel schuivende segmenten zijn, en een mooi segment mooi blijft, zijn binnen eindige tijd alle segmenten mooi. Lemma 9.37 Een stille berekening heeft slechts één schuivend segment. Bewijs. Zij C een stille berekening en laat C de suffix van C zijn waarin alle segmenten mooi zijn (die bestaat wegens lemma 9.36). In C zijn geen transities van type B meer. Eerst bewijzen we dat voor elk schuivend gat z geldt g(z) = 0. Veronderstel namelijk dat dit niet het geval is. Daaruit volgt dat er meer dan één segment is, want als de ring uit één

154 144 9 Stabilisatie segment bestaat dan heeft dat segment gat-grootte 0. Er zijn dan schuivende gaten z en z waarbij z voor z uit schuift, g(z) g(z ) en g(z) > 0. Er komt een configuratie γ j1 voor waarin de toestand van het linkereinde van s z, dus P z(j1 )+1, 0.g(z ) is, en de toestand van P z(j1 ) is dan a.g(z). Maar in die configuratie is de transitie van type A niet mogelijk in proces P z(j1 )+1. Dit proces kan pas weer een transitie doen nadat P z(j1 ) een transitie van type A heeft gedaan. Die transitie van P z(j1 ) zou echter het aantal segmenten doen afnemen, en bij aanname was de berekening stil; een tegenspraak. Omdat dus alle gat-groottes 0 zijn (gebruik vergelijking 9.1) is het aantal gaten 1 of N! Maar in een configuratie met N segmenten (van lengte 1) doet elke transitie van type A het aantal segmenten afnemen en een berekening met een dergelijke configuratie is niet stil. Het volgt nu dat er precies één gat en dus ook één segment is. Stelling 9.38 Het systeem stabiliseert naar wederzijdse uitsluiting. Bewijs. De verzameling legale configuraties is al gedefiniëerd en uitsluiting, de geslotenheid en de fairness zijn bewezen in lemma Er rest alleen te bewijzen dat het systeem altijd een legale configuratie bereikt. Wegens propositie 9.32 en lemma 9.36 heeft elke berekening een stille suffix waarin alle segmenten mooi zijn. In die suffix is er wegens lemma 9.37 slechts één segment, en dat segment heeft dan gat-grootte 0. Het segment is mooi, en dus zijn alle labels dan 0, en dus zijn alle verdere configuraties legaal. We besluiten deze sectie met het zonder bewijs vermelden van een verder resultaat van Burns en Pachl. Stelling 9.39 Voor elk priemgetal N bestaat er een uniforme, gerichte ring die stabiliseert naar wederzijdse uitsluiting en waarin het aantal toestanden van een proces O(N 2 / log N) is. 9.4 Routering in MANETs Een MANET of voluit Mobile Ad-Hoc Network is een netwerk waarvan de stations erg beweeglijk zijn. Ook kunnen er vaak stations verdwijnen of bijkomen, of hun toestandsinformatie kwijtraken door een herstart. Een snelle (her)configuratie van deze netwerken is daarom wenselijk; we bekijken nu een algoritme hiervoor van Gafni en Bertsekas [GB81]. In deze sectie kijken we naar het routeren van informatie naar een zekere knoop, de target. (Om verkeer tussen alle knopen mogelijk te maken, moet elke knoop afzonderlijk de rol van target vervullen; het beschreven algoritme moet dus vele malen gedupliceerd worden.) De communicatiestructuur wordt gemodelleerd als een graaf; we veronderstellen dat een knoop de toestand van al zijn buren kan zien, en afhankelijk daarvan zijn eigen toestand kan wijzigen. Het doel van het systeem is, alle kanten een richting te geven, zodanig dat elk gericht pad eindig is en stopt in de target. Definition 9.40 Zij G = (V, E) een ongerichte graaf met t V. Een (t-gerichte) orientatie van V is een gerichte graaf G o = (V, A) waarbij: 1. G o bevat geen cykel. 2. t is de enige sink.

155 9.4 Routering in MANETs 145 variabele van v: h[v] int // niet-negatief Stap van de target t: if (h[t] /= 0) { h[t] = 0 } Stap van non-target v met buren N[v]: if (forall w in N[v] : h[v] < h[w]) { h[v] = 1 + MAX { h[w] : w in N[v] } } Programma 9.4: Link Reversal algoritme. (Een sink is een knoop zonder uitgaande kanten). Een t-gerichte orientatie kan worden gebruikt om packets naar t te routeren. Een knoop ongelijk t heeft een uitgaande kant (eig. 2) en kan dus het packet over een gerichte kant versturen. Een packet kan nooit terugkeren waar het is geweest (eig. 1), en zal dus uiteindelijk in t belanden Orientaties Als informatie over een netwerk gedistribueerd wordt bijgehouden, kan de consistentie een probleem zijn. Veronderstel dat knopen per aangrenzende kant opslaan of deze uitgaand of inkomend is. Dan moet expliciet voorkomen worden dat beide knopen de kant als uitgaand of beide als inkomend registreren. Als dit probleem is opgelost en een sluitende registratie per kant is ingevoerd, ontstaat het probleem, hoe cykels te voorkomen. Beide consistentieproblemen worden tegelijk opgelost met de hoogte-representatie van kantrichtingen. Elke knoop x krijgt een getal h x ; we veronderstellen dat alle getallen verschillend zijn en definieren de richting van een kant als xy A als h x > h y. Proposition 9.41 De graaf G o = (V, A) bevat geen cykel. Proof. Omdat kanten altijd naar lagere knopen wijzen, neemt h langs elk pad monotoon af en daarom bestaat er geen cykel. Een detail wat we verderop negeren is, hoe te garanderen dat alle hoogtes verschillend zijn. Hiervoor gebruiken we de identiteit van een knoop om de kant te richten bij gelijke h. Dus, als h x = h y, dan is xy A als x > y. Dit heeft hetzelfde effect als het opnemen van de identiteit van x achter de komma van de hoogte. Bv. knoop 17 met hoogte 6 heeft dan hoogte Link Reversal en t-gerichtheid De representatie met hoogtes geeft de afwezigheid van cykels, maar we willen ook dat t een sink is, en bovendien de enige. Allereerst maken we t tot sink door hem hoogte 0 te geven: h t = 0. Hoogtes van andere knopen worden niet-negatief. Hiermee is t nog niet per se de enige sink. Een knoop is een sink, wanneer zijn hoogte een locaal minimum is, dwz., kleiner dan de hoogte van alle omringende knopen. Een sink die niet de target is, keert al zijn inkomende kanten tegelijk om naar buiten, door een hoogte kiezen die groter is dan al zijn buren (Programma 9.4).

156 146 9 Stabilisatie Een sink die zijn kanten naar buiten keert houdt weliswaar op een sink te zijn, maar kan anderen tot sink maken. Als die hun kanten ook weer omdraaien, kan de eerste knoop opnieuw sink worden en de kant weer omdraaien. Een knoop kan dus meerdere malen de actie van Programma 9.4 uitvoeren. Tussen twee stappen van een knoop, zit wel altijd minstens één stap van elk van zijn buren. Theorem 9.42 Programma 9.4 stopt na eindig veel stappen en op dat moment is G o een t-gerichte orientatie. Proof. Een knoop op afstand i van t doet hoogstens i + 1 stappen, wat je kunt bewijzen met inductie naar i. Elke berekening is dus eindig, en stopt in een configuratie waarin geen station een stap kan doen. Omdat een non-target sink zijn h mag verhogen, zijn er in die situatie geen non-target sinks, dus G o is een t-gerichte orientatie. Opmerkingen over Link Reversal. aangepast en verbeterd. Het algoritme kan nog op diverse punten worden 1. Lengte van paden. Het beschreven systeem garandeert dat paden een eindige lengte hebben, maar niet dat ze zo kort mogelijk zijn. Kortste paden zijn stabiliserend te berekenen door als hoogte te kiezen: 1 plus de hoogte van de laagste (ipv. hoogste) buur. 2. Hoogte van knopen. De waarde die h x kan bereiken is willekeurig hoog. Mocht het systeem per ongeluk starten in een situatie waarin sommige knopen hun h al op maxint hebben staan, dan kan het niet verder. Je kunt het systeem uitbreiden met een compactificatie-stap : als een station constateert dat er een gat zit tussen zijn hoogte en die van lagere buren, dan verlaagt hij h. De correctheid van het algoritme blijft gelden omdat deze stap geen invloed heeft op de richting van kanten. 3. Partial Reversal. In de beschreven versie draait een sink alle kanten om; er is ook een versie van dit systeem waarbij slechts een deel van de kanten worden omgedraaid. Samenvatting en conclusies In dit hoofdstuk werd gekeken naar het concept van stabiliserende systemen. Als zo n systeem in een willekeurige beginconfiguratie wordt gestart, is gedurende een eindige tijd het gedrag ongecontroleerd, maar na enige tijd gaat het systeem weer aan zijn specificatie voldoen. Stabiliserende systemen configureren zichzelf, en herstellen hun gedrag na een storing die in principe de toestand van alle processen kan beïnvloeden. We hebben enkele definities en redeneermethoden rond stabilisatie gezien in sectie 9.1. Deze technieken werden geïllustreerd aan de hand van een uitsluitings-probleem, Dijkstra s token ring, in sectie 9.2. Een uniforme oplossing van het uitsluitingsprobleem is niet mogelijk als de ring-grootte samengesteld is, maar voor priem-ringen is er wel een oplossing, zoals beschreven in sectie 9.3. De bruikbaarheid van stabilisatie voor de organisatie van mobile ad-hoc netwerken werd aangetoond in sectie 9.4. Meer algoritmen voor het berekenen van netwerk-eigenschappen kun je vinden in [Tel91].

157 Opgaven bij hoofdstuk Opgaven bij hoofdstuk 9 Opgave 9.1 Zij S een systeem dat pseudostabiliseert naar F, waarbij een bovengrens geldt op het aantal transities waarbinnen convergentie optreedt: Bewijs dat S stabiliseert naar F. C B : i K : C i F. Opgave 9.2 De verzameling paren van natuurlijke getallen wordt lexicografisch geordend, dwz., (a, b) < (c, d) geldt als a < c, of (a = c en b < d). (a) Laat zien dat (tenzij a = 0) er oneindig veel paren (c, d) bestaan die kleiner zijn dan (a, b). (b) Neem a > 0; laat zien dat er voor elk getal k een dalende rij is beginnend in (a, b) die k of meer paren bevat. (c) Laat zien dat deze verzameling welgeordend is. (Deze verzameling kan dus met startpunt (a, b) wel onbegrensd lange, maar geen oneindig lange dalende rijen bevatten.) Opgave 9.3 Bekijk de verzameling van alle strings over het alfabet { a, b,..., z } met hierop de lexicografische ordening. Laat zien dat deze verzameling niet welgeordend is (maw., een oneindig lange dalende rij bevat). Opgave 9.4 Waar of onwaar: Nadat Dijkstra s gerichte token ring (sectie 9.2.1) een legale configuratie bereikt, doorloopt het een herhalende cyclus die alle legale configuraties bevat. Opgave 9.5 Dijkstra s Stabiliserende Token Ring. (Uit het tentamen van juli 2011.) In Dijkstra s Token Ring vergelijkt een station steeds zijn toestand met die van zijn linkerbuur. Bij ongelijkheid wordt het station kritiek en daarna kopieert het de buurtoestand. Eén speciaal station werkt anders: dat wacht juist op gelijkheid, wordt dan kritiek en verhoogt zijn toestand. Er zijn N stations, elk met K toestanden. (a) Bewijs dat een verandering alleen kan leiden tot een afname, nooit een toename van het aantal kritieke stations. (b) Bestaan er configuraties waarin geen station kritiek is? (c) Er is bewezen dat, nadat het speciale station een zeker aantal stappen heeft gedaan, de configuratie legaal is. We rusten daarom dit station uit met een stappenteller, die bij elke stap wordt opgehoogd. Bij welke waarde van deze stappenteller kan het speciale station de conclusie trekken dat het systeem gestabiliseerd is? Opgave 9.6 Bewijs dat, in Programma 9.4, een knoop op afstand i van t hoogstens i + 1 stappen doet. Opgave 9.7 Om kortste paden te berekenen kiest elke knoop als hoogte: 1 meer dan de hoogte van zijn laagste buur. (De target houdt altijd hoogte 0.) Waarom is je bewijs uit opgave 9.6 niet meer geldig? Kun je iets bewijzen over het aantal stappen?

158 Hoofdstuk 10 Wachtvrije implementaties met registers Atomiciteit van operaties vereist dat het resultaat van operaties altijd is alsof de operaties na elkaar zijn uitgevoerd; de operaties gelden dan als ondeelbaar. Vanaf de jaren vijftig heeft dit begrip ondeelbaar niet alleen een belangrijke rol gespeeld in de betekenis van atomiciteit, maar ook bij de implementatie ervan. Steeds moest een thread voor het uitvoeren van een operatie op data een exclusieve toegang tot de data of code verkrijgen, met als resultaat dat de toegang voor andere threads geblokkeerd was. De steeds modernere ontwikkelingen betroffen alleen de manier waarop dit wachten was geregeld: eerst via mutual exclusion code, later via semaforen en monitors of database locks. Halverwege de jaren tachtig ontstond belangstelling voor de vraag, in hoeverre atomaire operaties geïmplementeerd kunnen worden zonder dat onderling wachten noodzakelijk is. Dit betekent dat alle instructies van de operaties zelf, dus niet alleen van de speciaal ontworpen of beschermde synchronisatiecode, willekeurig kunnen worden afgewisseld. Het blijkt wel degelijk mogelijk om programma s te ontwerpen die aan deze zware eis kunnen voldoen. Wel bleek al snel dat uitgaande van atomaire registers de mogelijkheden beperkt waren, maar gebruikmakend van speciale parallellisme-ondersteunende instructies zoals de test-and-set kon er al meer worden geïmplementeerd. De komende drie hoofdstukken bestuderen enkele belangrijke concepten uit deze theorie van wachtvrije implementaties. Dit hoofdstuk definieert wat een wachtvrije implementatie is en er wordt ook gekeken naar enkele voorbeelden die met registers mogelijk zijn. In hoofdstuk 11 worden de grenzen van wat met registers mogelijk is overschreden: we zien dat het consensusprobleem niet met registers kan worden opgelost, maar wel met een test-and-set of ander primitief. In hoofdstuk 12 zullen we zien wat de krachtigste primitiva zijn die er voor wachtvrij rekenen bestaan, en dat de compare-and-swap tot deze categorie van krachtigste objecten behoort. De resultaten zijn op een objectgeoriënteerde manier geformuleerd. De wachtvrije synchronisatie heeft, ondanks de duidelijke voordelen, nog nauwelijks zijn weg gevonden naar operationele producten. 148

159 10.1 Voordelen van wachtvrije synchronisatie Voordelen van wachtvrije synchronisatie Zowel bij het klassiek programmeren met gedeelde variabelen (hoofdstukken 1 tot 4) als bij programmeren met berichtuitwisseling (hoofdstukken 5 tot 8) hebben we te maken gehad met processen die op de een of ander manier op elkaar moeten wachten. We hebben daarvan diverse nadelen gezien, waaronder de mogelijkheid tot deadlock en starvation (hoofdstuk 4). Als de berekening snelle en langzame processen bevat (bijvoorbeeld doordat het netwerk naast supercomputers ook nog wat oude trage stations bevat), is er de mogelijkheid dat de snelle processen voor hun voortgang afhankelijk zijn van de trage, zodat er van snel werken weinig komt. Tenslotte zijn wachtende processen extreem kwetsbaar voor het crashen van het proces waarop gewacht wordt; daarom moest in hoofdstuk 4 altijd de aanname worden gemaakt dat een filosoof eindig lang eet. In een wachtvrij programma komen, zoals de naam al zegt, geen wacht-opdrachten voor; om ook een impliciete busy-wait uit te sluiten wordt bovendien geëist dat elke operatie wordt afgerond binnen een begrensd aantal instucties van het proces. De eisen worden straks preciezer geformuleerd in een objectgeoriënteerde context. Onderling wachten wordt hiermee inderdaad uitgesloten, en de volgende voordelen van programmeren met wachtvrije objecten zijn natuurlijk de negaties van de nadelen van wachten: 1. Geen deadlocks: Omdat er bij wachtvrije oplossingen niet wordt gewacht, is ook een circulaire wacht onmogelijk, en er treden dus geen deadlocks op. 2. Begrensde responstijd: Omdat er niet wordt gewacht en een operatie binnen een begrensd aantal instructies wordt afgerond, is een proces voor zijn voortgang niet afhankelijk van de snelheid van andere componenten. Anders gezegd, een proces heeft zijn responstijd helemaal in eigen hand. 3. Fouttolerant: Als processen halverwege operaties afbreken heeft dit op de voortgang van andere processen geen invloed, want die hoeven niet te wachten. Bovendien is de werking van de (asynchrone) operaties zo, dat als de processen de operaties zouden afmaken, de integriteit van de objecten blijft gelden Probleemstellingen modelleren als objecten We zullen zowel de systeemprimitieven als de op te lossen problemen formuleren als objecten, met een toestand en toepasbare methoden. De methoden zijn altijd atomair! Het resultaat van (deels) overlappende methode-aanroepen is dus hetzelfde als wanneer de methoden in één of andere volgorde na elkaar waren aangeroepen. Het wekt weinig verwondering dat eenvoudige (of complexe) datastructuren, zoals registers of queues, als objecten gemodelleerd kunnen worden. Voor coördinatieproblemen, zoals we die in gedistribueerde programma s tegenkomen (electie, consensus), ligt dit minder voor de hand. Immers, we denken hierbij altijd aan programma s waaraan de processen tegelijk bezig zijn en met elkaar interacteren, terwijl we bij (atomaire) objecten juist denken aan processen die na elkaar methoden uitvoeren. Toch is het heel goed mogelijk om electie of consensus als een object met atomair gedrag te modelleren; het electie-object kan zelfs op twee manieren (zie verderop).

160 Wachtvrije implementaties met registers Definities van gebruikte objecten We behandelen nu de diverse classes (objecttypen) die we bij de bestudering van dit onderwerp tegen zullen komen. De inpassing van willekeurige types zoals die in een programma voor zullen komen vindt in hoofdstuk 12 plaats. Register. Een register (soms ook read-write register genaamd) is een object met als toestandsruimte een willekeurige verzameling V en twee methoden, namelijk read en write(x). De eerste levert de toestand van het object op, de tweede verandert de toestand in x en levert niets op. Over het type van het register (integer, boolean, char, etc., m.a.w. wat de verzameling V is) zullen we ons niet druk maken. Een binair register, waarbij V = {0, 1}, wordt ook wel een bit genoemd. Als V meer dan twee elementen bevat spreken we van een multivalued of meerwaardig register. Bij registers maken we ook een onderscheid naar het aantal processen dat er toegang toe heeft. Heeft slechts één proces de mogelijkheid de methode write aan te roepen en één proces de mogelijkheid om de read methode aan te roepen, dan spreken we van een single writer, single reader register. Bij een single writer, multiple reader register is er eveneens één proces dat kan schrijven, maar alle processen kunnen het register lezen. En wat zou een multiple writer, multiple reader register zijn? Test-and-set. Een test-and-set is een object met twee toestanden, 0 en 1, en eveneens twee methoden, TaS en reset. De kracht van dit type zit erin, dat het in één methode mogelijk is zowel de toestand te veranderen als de (oude) toestand te lezen. De TaS levert de toestand van het object op en zet de toestand op 1. De reset zet de toestand op 0. In sectie 3.3 bleek het test-and-set object al een handig hulpmiddel omdat je er heel gemakkelijk iets mee kunt doen waar je met registers ingewikkelde programma s voor nodig hebt. We zullen in hoofdstuk 11 zien, dat er op het gebied van wachtvrij rekenen iets mee kan worden gedaan wat met registers onmogelijk is. Queue. Een queue heeft als toestandruimte de verzameling V, de rijtjes over een verzameling V, en methoden enq(x) en deq. De methode enq(x) voegt x aan de queue toe, dwz verandert toestand σ V in σ x, en levert niets op. De deq levert het eerste element op en verwijdert dit: dwz. als de toestand y σ is, levert de methode y op en verandert de toestand naar σ. Als de queue leeg is blijft de toestand onveranderd en de returnwaarde is een speciaal (niet in V ). Bij een queue veronderstellen we geen methode front, die het eerste element oplevert maar niet verwijdert. Als die methode beschikbaar is spreken we van een front-queue. De queue is de objectgeörienteerde formulering van het producer-consumer probleem dat we in sectie 3.4 hebben besproken. Daar werd echter bij een lege buffer gewacht tot de producer weer een element had toegevoegd, maar het woord wachten is vanaf dit hoofdstuk uit ons woordenboek geschrapt. Compare-and-swap. Een compare-and-swap is een wat uitgebreidere variant op het thema test-and-set; ook hier vinden we een methode die tegelijk de toestand leest en beïnvloedt. Toestandsruimte is weer een verzameling V en er zijn methoden read en CaS(x, y). De methode read levert de toestand op net als bij een register. De methode CaS(x,y) levert ook

161 10.2 Probleemstellingen modelleren als objecten 151 Kader 10.1: Compare-and-Swap in IBM System/370. Een Compare-and-Swap instructie werd aan de IBM/370 architectuur (zie ook Kader 1.6) toegevoegd in 1973 om multiprocessing te ondersteunen. Weliswaar bestond er al een Testand-Set instructie, maar de ervaring had geleerd dat hiermee minder krachtige operaties konden worden ondersteund. Met de Test-and-Set kun je semaforen verkrijgen, maar alle ingewikkelder operaties vereisen dan alsnog het uitsluiten van andere threads door zo n semafoor. In 1973 was al bekend dat Compareand-Swap meer kan doen in een enkele instructie, wat locking overbodig maakt. De studie van wacht-vrij rekenen vanaf 1990 formaliseerde dat dit niet slechts een ervaringsfeit, gebaseerd op huidige kennis is. Compare-and-Swap is bewijsbaar in staat problemen op te lossen die met Test-and-Set niet oplosbaar zijn. de toestand op maar verandert deze in y als hij gelijk aan x was; je kunt je zijn werking dus zo voorstellen: CaS(x,y): tmp = state if (tmp == x) then { state = y } return tmp De test-and-set is eigenlijk een speciaal geval van de compare-and-swap; wel is het zo dat de compare-and-swap op het gebied van wachtvrij rekenen bewijsbaar meer mogelijkheden biedt. Snapshot. Een snapshotobject is een array waarvan elk proces een locatie kan schrijven, en die als geheel gelezen kan worden. De toestandsruimte is dan V n en er zijn methoden update(x) en scan. De methode update(x), aangeroepen door proces i, verandert component i van de toestand in x en de methode scan levert de toestand op. Electie. In het electieprobleem hebben stations geen invoer (behalve eventueel het identificatienummer), en daarom heeft de enige methode, elect, geen parameter. De zwakke electie vertelt elk station of het leider is of niet, en de sterke electie vertelt, wie er leider is geworden. Formeel gezegd, de elect-methode van het zwakke electie-object levert een waarde uit {leider, nonleider} op. De enige correctheidseis waar het object verder aan moet voldoen is, dat ongeacht hoeveel aanroepen er plaatsvinden (mits meer dan 0), er exact eenmaal het antwoord leider wordt gegeven. De elect-methode van het sterke electie-object levert een een getal op, namelijk de identiteit van de leider. De correctheidseis is dan dat alle antwoorden gelijk zijn, en wel aan de identiteit van een van de aanroepende stations. In een klassieke gedistribueerde omgeving is het verschil tussen deze twee vormen nauwelijks relevent; de leider (bij het zwakke object) kan de anderen laten weten wie leider is, zodat het

162 Wachtvrije implementaties met registers zwakke object in feite het sterke implementeert. In het volgende hoofdstuk zullen we zien, dat in een wacht-vrije context het verschil wel degelijk relevant is. Consensus. Een consensusobject is een objectformulering van het consensus probleem. Het kan door elk proces hoogstens éénmaal worden aangeroepen, met als argument een element van V ; de aanroep levert weer een element van V op, maar natuurlijk voor alle aanroepen hetzelfde element. Omdat we bovendien eisen dat de gemeenschappelijke returnwaarde voorkomt onder de invoeren, volgt hieruit dat elke aanroep van het object de eerste invoer oplevert: het object kan immers niet in de toekomst kijken om bij de eerste aanroep alvast een latere invoer op te leveren. We kunnen daarom het consensusobject definiëren met toestandsruimte V {@} en één methode submit(x) met deze werking: Als de is, levert submit(x) de waarde x op en verandert de toestand in x, als de toestand is wordt y opgeleverd. Het zal blijken dat een consensusobject krachtiger is naarmate er meer processen gebruik van kunnen maken; een n-consensusobject kan door precies n processen worden aangeroepen Implementaties Van sommige classes mag de programmeur de beschikbaarheid veronderstellen, bijvoorbeeld van registers en, in modernere computersystemen, soms de test-and-set of compare-and-swap. Doel van het programmeren is dan, classes met algemenere eigenschappen te implementeren met behulp van de gegeven classes. Definitie 10.1 Een wachtvrije implementatie van class B bestaat uit 1. een specificatie van objecten A 1 t/m A k waaruit een B-object wordt geïmplementeerd, met hun initiële toestanden; 2. voor elk proces i 1...n, en elke methode meth van B, een toegangsprocedure P i,meth die beschrijft hoe proces i meth kan aanroepen, in termen van de onderliggende objecten A 1 t/m A k. Elke toegangsprocedure is wachtvrij, dwz., bevat geen wait instructies en termineert na een begrensd aantal instructies. De toegangsprocedures gebruiken, behalve de gedeelde objecten A 1 tot A k alleen locale variabelen van de processen. Bedenk dat de eis van een begrensd aantal instructies per toegangsprocedure sterker is dan te eisen dat er een eindig aantal operaties wordt gedaan. Begrensd betekent hier, dat er een a priori bovengrens is op het aantal instructies, en dat aantal geldt voor elke aanroep van de betreffende procedure. Het aantal stappen dat een correct proces doet voor een decide in programma 8.7 bijvoorbeeld, is wel eindig maar niet begrensd. Merk ook op dat de toegansprocedure voor elk proces anders mag zijn, ook al is de semantiek van een bepaalde methode van B voor elk proces gelijk. Nu nemen registers in onze beschouwing een bijzondere plaats in omdat we altijd aannemen dat ze in het systeem aanwezig zijn. In de definitie van A implementeert B mag je daarom altijd extra registers gebruiken. Definition 10.2 Class A implementeert class B, notatie A = B, als er een wachtvrije implementatie van B-objecten is, waarin de gedeelde objecten A 1 tot A k elk van class A of Register zijn.

163 10.3 Registers: van single naar multiple reader 153 Zoals te verwachten was, is implementatie transistief: Lemma 10.3 Als A = B en B = C, dan A = C. Bewijs. De gegeven implementatie van een C-object bestaat uit een aantal B-objecten en registers. Vervang elk B-object door de A-objecten en registers die het implementeren, en vervang in de toegangsprocedures voor C alle aanroepen van een B-methode door de toegangsprocedure die deze methode implementeert uit de A-objecten. De nieuwe toegangsprocedures implementeren C-objecten vanuit A-objecten en registers. De toegangsprocedures in de gegeven implementaties gebruiken alle een begrensd aantal stappen, en de procedures voor de methoden van C gebruiken daarom maar een begrensd aantal aanroepen van methoden van B. Omdat elk van die aanroepen op zijn beurt weer een begrensd aantal stappen kost, is het totale aantal stappen in de nieuwe toegangsprocedures begrensd. Omdat implementatie ook reflexief is, kun je nu bewijzen dat wederzijdse implementatie een equivalentierelatie is. Definition 10.4 Classes A en B zijn equivalent, notatie A B, als A = B en B = A Registers: van single naar multiple reader In dit hoofdstuk beperken we ons tot objecten die vanuit registers implementeerbaar zijn. Bij registers is de scheiding tussen het weggeven van informatie en het verkrijgen van informatie heel strikt. De write verandert de toestand van het object, maar verkrijgt volstrekt geen informatie over die toestand: een thread die de write uitvoert wordt op geen enkele manier en in geen enkele situatie beïnvloed door de (oude) toestand van het object. De read verkrijgt wel informatie uit het object, zelfs alle informatie want de hele toestand wordt opgeleverd, maar verandert de toestand volstrekt niet: op geen enkele manier en in geen enkele situatie is aan het object zichtbaar dat er een read is uitgevoerd. De objecten die met registers te implementeren zijn hebben deze scheiding ook. Van objecten die gecombineerde operaties hebben (bv. een queue waarvan de deq operatie een waarde uit het object oplevert en meteen verwijdert) zullen we in hoofdstuk 11 zien dat ze met registers niet te implementeren zijn. De implementaties die we gaan zien zijn (1) een multiple-reader register uit single-reader registers; (2) een snapshotobject (sectie 10.4). Bij het gebruik van registers gaat men er doorgaans vanuit dat ze door alle processen te lezen en te schrijven zijn, hoewel een dergelijke flexibiliteit vanuit hardware-oogpunt verre van vanzelfsprekend is. Met name overlappende schrijfacties door verschillende processen geven soms onverwachte effecten. Registers met sterke concurrency-eigenschappen zijn echter softwarematig uit registers met zwakke eigenschappen samen te stellen, waarvan we in deze sectie een voorbeeld zien. De behandeling is overgenomen uit [AW98, Sec ]. We laten zien dat multiple-reader registers vanuit single-reader registers te implementeren zijn; dit blijkt lastiger dan het lijkt en geeft een aardige indruk hoe gecompliceerd wachtvrij rekenen kan zijn. Er komen ook een paar voor dit gebied typische redeneerwijzen en technieken aan de orde. We beschouwen één writer, proces w, en n readers, 1 tm n. Om de methoden van de Single-Reader registers en van het Multiple-Reader register te onderscheiden zullen we ze SRread, etc., noemen.

164 Wachtvrije implementaties met registers var Val[1..n] : SRregister method MRwrite(x): { for ( i=1 ; i <= n ; i++ ) { Val[i].SRwrite(x) } } method MRread voor proces i: { v = Val[i].SRread return v } Programma 10.2: Incorrecte implementatie van MR-register Hoe het niet moet Omdat elk door w beschreven register door maar één proces gelezen kan worden, is het nodig dat de writer zijn waarde in meerdere registers schrijft, voor elke lezer apart. Een voor de hand liggende, maar incorrecte, uitwerking (programma 10.2) is het gebruik van één SR-register Val[i] voor elke reader i. In een MRwrite(x) zal de writer achtereenvolgens alle SR-registers met x beschrijven, en een reader leest gewoon het voor hem bestemde register. Figuur 10.3 laat zien wat hier mis aan is (bij 2 readers): de writer overschrijft de waarde 0 met 1, maar tussen het beschrijven van Val[1] en Val[2] kan er een leesactie van 1 worden gevolgd door één van 2. De eerste leesactie krijgt dan de nieuwe waarde van het register al te zien terwijl de latere leesactie de oude waarde krijgt. Er is sprake van schending van de atomiciteit omdat het niet mogelijk is elke operatie een contractiepunt binnen het tijdsinterval van die operatie te geven. Een klassieke oplossing zou zijn, de registers te gebruiken om exclusieve toegang tot het object te garanderen, zodat het onmogelijk is dat er iemand leest tussen de twee schrijfacties van de writer. Het zal duidelijk zijn dat dit niet strookt met de eis van wachtvrijheid die we ons hebben opgelegd. De writer kan immers tussen twee SR-writes willekeurig lang wachten, en in de tussentijd moet het object voor de readers beschikbaar zijn zonder dat zij moeten wachten. Ook is in te zien dat een busy wait een onbegrensd aantal malen de test kan uitvoeren en dus niet aan de formele eis van een begrensd aantal operaties voldoet. Je kunt het programma ingewikkelder gaan maken door meerdere keren te gaan lezen of w Val[1].SRwrite(1) 1 Val[1].SRread Val[2].SRread 2 Val[2].SRwrite(1) MRwrite(1) MRread MRread Figuur 10.3: Tegenvoorbeeld voor programma 10.2.

165 10.3 Registers: van single naar multiple reader 155 schrijven, of zelfs te eisen dat de writer behalve x nog aanvullende informatie wegschrijft. Alles is tevergeefs, want tegenvoorbeelden blijven mogelijk, en de uiteindelijke oplossing gebruikt ook SR-registers die door de readers worden beschreven! Een leesactie van een reader zal dus de interne toestand van het MR-register wijzigen; in feite op een manier die ervoor zorgt dat een deels afgewikkelde MR-write van de writer wordt voltooid en in de objecttoestand definitief wordt. Het is in de informatica gebruikelijk om altijd te blijven zoeken naar betere oplossingen. In dit geval echter is bewezen, dat de genoemde concessie noodzakelijk is; er hoeft dus niet te worden gezocht naar een oplossing waarin de readers niet schrijven. Over het algemeen kan een bewijs van onmogelijkheid of ondergrens knap lastig zijn zoals we eerder hebben gezien. Echter de strenge eisen die op wachtvrije implementaties gelden werken nu in ons voordeel: een veronderstelde implementatie moet heel veel interleavings van instructies toestaan. Van die interleavings kunnen we vaak gemakkelijk wat eigenschappen combineren om te bewijzen dat geen enkel programma ooit aan de eisen kan voldoen. Stelling 10.5 Er bestaat geen wachtvrije implementatie van MR-register uit SR-registers waarin niet tenminste één van de readers een SR-register beschrijft. Bewijs. Veronderstel dat er een implementatie bestaat waarin geen van de readers schrijft in een gedeeld object. Neem nu aan dat het MR-register waarde 0 heeft; we bekijken een MRwrite(1) door de writer w. Omdat de enige gedeelde objecten SR-registers zijn met writer w, bestaat een MRwrite(1) uit een reeks SRwrites, zeg W 1 tot en met W m. Uit de aanname dat de readers niet schrijven volgt dat deze reeks, dus de activiteit van de writer, niet door overlappende MRreads worden beïnvloed. Omdat elk SR-register slechts door één proces gelezen kan worden, kunnen we de SR-registers verdelen in S 1, S 2,..., waar de SR-registers in S i slechts door proces i worden gelezen. We gaan nu even kijken welke waarden worden opgeleverd als er een complete MRread door een van de readers wordt uitgevoerd tussen twee SRwrites van de writer. Op grond van de wachtvrijheid van de implementatie is dit altijd mogelijk. Zo n MRread kan uit een aantal SRreads zijn opgebouwd, maar de eisen van wachtvrijheid dwingen af dat de gehele transactie hoe dan ook kan aflopen voordat de writer zijn volgende SRwrite doet. Zij x i,j de waarde die wordt opgeleverd in een MRread door proces i vlak na W j (en x i,0 de waarde die wordt opgeleverd door een MRread vòòr W 1 ). De eis van atomiciteit van het te implementeren register impliceert dat elke reader eerst steeds 0 leest, en vanaf een zeker moment steeds 1, dus: 0 = x 1,0 =... = x 1,k 1, x 1,k =... = x 1,m = 1, 0 = x 2,0 =... = x 2,l 1, x 2,l =... = x 2,m = 1, De waarde die reader i leest kan namelijk alleen veranderen als er een register in S i wordt geschreven, en dit impliceert dat l k. Stel k < l. Bekijk een executie die tussen W k en W k+1 een complete MR-read van reader 1, en daarna een van reader 2 heeft. Omdat (volgens de aanname) reader 1 niet schrijft tijdens zijn MR-read, verloopt de MR-read van reader 2 precies hetzelfde als wanneer de MR-read van reader 1 niet had plaatsgevonden. Hoewel dan de read van reader 1 is afgelopen voordat die van reader 2 begint, ziet reader 1 al de nieuwe waarde (1) en reader 2 nog de oude. Het register is dus niet atomair. Het geval l < k wordt analoog behandeld. Je kunt met dit bewijs nog iets verder gaan en, voor het geval er meer dan twee readers zijn, iets bewijzen over het aantal SR-registers dat nodig is.

166 Wachtvrije implementaties met registers SR-register Val[n] : initieel (v0, 0) SR-register Report[n,n] : initieel (v0, 0) integer seqnr : initieel 0 MRwrite(x): { seqnr ++ for (i=1 ; i <= n ; i++) { Val[i].SRwrite((x,seqNr)) } } MRread voor proces i: { (v,s) = Val[i].SRread for (j=1 ; j <= n ; j++) { (w,t) = Report[j,i].SRread if (t>s) then v,s = w,t } for (j=1 ; j <= n ; j++) { Report[i,j].SRwrite((v,s)) } return v } Programma 10.4: MR-register uit SR-registers Oplossing: readers helpen elkaar In onze oplossing laten de readers aan elkaar weten welke waarde ze het laatst hebben gezien. De gedachte hierachter is het onderling helpen van threads, in dit geval is het de writer die geholpen wordt door snelle readers. In de situatie die in het bewijs werd uitgebuit, was de writer zo ver gevorderd dat zijn operatie zichtbaar was geworden voor één van de readers, maar nog niet voor een andere. Het help-mechanisme zorgt ervoor dat een reader die een nieuwe waarde heeft gezien, helpt deze voor de andere readers zichtbaar te maken voor het geval de writer zelf traag is in het afmaken van zijn MR-write. Een reader kan nu verschillende waarden te zien krijgen, namelijk rechtstreeks van de writer of van zijn collega s en daarom is het belangrijk dat een reader oude en nieuwe waarden kan onderscheiden. De ordening van waarden wordt bijgehouden met een serienummer. Daarom gebruiken we, voor de implementatie van een MR-register van type V, een aantal SR-registers van type V N. De writer schrijft zijn waarde, samen met het serienummer, in een SR-register voor elke reader, vergelijkbaar met programma Een reader begint met het lezen van het voor hem bedoelde register, maar leest ook een waarde van elke andere reader; voor elk paar readers (i, j) is er een SR-register Report[i,j]. De meest recente waarde wordt weggeschreven voor de andere readers, en opgeleverd; zie programma Hierin is v0 de initiële waarde van het MR-register. Let op hoe mooi wachtvrij het programma is: een MRwrite(x) gebruikt n, en een MRread gebruikt 2n + 1 operaties op gesharede objecten, zonder tussendoor op bepaalde condities te wachten. Let ook op het gebruik van de SR-registers in de implementatie: elk register heeft

167 10.3 Registers: van single naar multiple reader 157 één writer en één reader. Het register Var[i] heeft w als writer en reader i als reader, en Report[i,j] heeft reader i als writer en reader j als reader. De variabele seqnr wordt door de writer gelezen en geschreven, en wordt eigenlijk niet als een gedeeld object meegeteld. Om te bewijzen dat het programma inderdaad een atomair MR-register implementeert construeren we vanuit een executie van SR-operaties een volgorde van MR-operaties. Zij dus α een executie: dit is een reeks van stappen (SR-operaties en locale instructies) van de diverse processen genomen volgens het programma. Dat betekent dat als je uit α de stappen van één proces isoleert, dan zie je een aantal achtereenvolgende executies van de methode van dat proces, waarbij de laatste eventueel incompleet is. Nu vormen we een reeks π van MR-operaties. Neem eerst de rij van alle MRwrite(x) operaties, in hun volgorde in α: deze volgorde is gedefiniëerd omdat er maar 1 writer is. Vervolgens gaan we de voltooide MRreads ertussen vlechten. De MRreads die de waarde opleveren die werden weggeschreven met serienummer t zetten we tussen de t e en (t + 1) ste MRwrite, in de volgorde van hun beëindiging. Door deze keuze levert elke MRread precies de waarde op die is geschreven in de laatst voorafgaande MRwrite. Wat we nog moeten aantonen is dat deze volgorde niet strijdig is met de werkelijke timing van de operaties. Lemma 10.6 Als Op1 en Op2 MR-operaties zijn, zodanig dat Op1 afliep voordat Op2 begon (in α) dan komt Op1 voor Op2 in π. Bewijs. We onderscheiden vier gevallen: 1. Op1 en Op2 zijn MRwrites. Verschillende MRwrites overlappen niet en de constructie behoudt hun volgorde. 2. Op1 is een MRwrite en Op2 een MRread door proces i. Het schrijven van Val[i] in de MRwrite is al voltooid voordat proces i in de MRread Val[i] leest: proces i ziet dus meteen een serienummer dat tenminste dat van Op1 is. De uiteindelijk opgeleverde waarde kan alleen maar een hoger, geen lager serienummer hebben. 3. Op1 is een MRread door proces i en Op2 een MRwrite. Voordat een MRwrite met serienummer t begint, zijn er alleen maar kleinere serienummers te zien. Op1 levert dus een waarde op die met een kleiner serienummer is geassocieerd, en wordt dus vòòr Op2 in de reeks π geplaatst. 4. Op1 is een MRread door proces i en Op2 een MRread door proces j. Voor deze situatie hebben we het onderling rapporteren van readers: zij t het serienummer van de waarde die door Op1 is opgeleverd. Omdat Op1 is afgelopen voordat Op2 begint, leest proces j uit Report[i,j] een serienummer van tenminste t, en het serienummer t van de opgeleverde waarde is dus tenminste t. Als t gelijk is aan t, zit Op2 toch achter Op1 in π omdat MRreads met dezelfde waarde in volgorde van beëindiging zijn opgenomen. Als t > t zit Op2 na de t e MRwrite, dus achter Op1. Deze eigenschap van π impliceert dat we elke MR-operatie kunnen samentrekken in een punt dat binnen het interval ligt dat de operatie heeft geduurd. Het lijkt erop dat we ten opzichte van stelling 10.5 nogal wat hebben ingeleverd: we bewezen dat het schrijven door tenminste één reader noodzakelijk is, maar onze uiteindelijke oplossing

168 Wachtvrije implementaties met registers Kader 10.5: Multiple-Writer snapshot objecten In sectie 10.4 nemen we steeds aan dat elk proces alleen schrijft in de eraan toegewezen geheugenlokatie (Fragment[i] voor proces i). Faith Fich vertelt op de Sofsem conferentie van 2005 in Liptovský Ján over een algemenere variant, waarbij een proces in elk van de locaties van het object mag schrijven. De hier beschreven beperktere versie noemt Fich de single-writer variant. Het algoritme voor de multiplewriter variant is vrijwel gelijk aan dat voor de single-writer versie, maar er is een uitbreiding van de serienummers nodig. Probeer zelf te bedenken waarom dat zo is (inclusief een concreet tegenvoorbeeld van hoe het anders mis kan gaan!) en probeer zo n uitbreiding te bedenken. laat alle readers schrijven voor alle readers. In totaal wordt de waarde van het register n(n+1) maal gedupliceerd opgeslagen! Door het bewijs van de genoemde stelling kritisch te bezien en verder uit te breiden kan men echter bewijzen dat een Ω(n 2 )-voudige duplicatie noodzakelijk is in elke oplossing; zie Opgaven 10.3 en Het snapshotobject Een belangrijk probleem in gedistribueerde berekeningen is het vastleggen van een consistente globale toestand [Fic05]. Het achtereenvolgens vastleggen van toestanden van alle componenten is niet correct, omdat de verkregen combinatie van deeltoestanden misschien nooit als globale toestand is voorgekomen. Denk bijvoorbeeld aan een systeem met meerdere sensoren of meters, waarvan waarden op willekeurige momenten kunnen veranderen, en waarvan we een totaaloverzicht van één moment willen bepalen. Een globaal consistente toestand is ook nuttig indien een berekening na een opgetreden fout herstart moet worden. Het snapshotobject heeft als toestand een vector van n waarden, en een proces kan middels een update(x) zijn eigen component in x veranderen en middels een scan de hele vector lezen. Dit wordt gebruikt in situaties waar je op één plaats informatie over de hele verzameling processen wilt bijhouden. Met een update kan een thread zijn eigen informatie wijzigen, en de scan moet dan een combinatie van waarden geven die op een bepaald moment tegelijk zijn voorgekomen. Het snapshotobject verschilt van een register in deze zin, dat de eenheid van schrijven kleiner is dan de eenheid van lezen. Bij een register wordt immers de gehele inhoud van het object in één keer gelezen, maar ook de gehele inhoud in één keer geschreven. Als elementaire oplossing kan men denken aan een rij van registers die samen de toestand implementeren: een update is dan een simpele write, en voor een scan worden de waarden achter elkaar gelezen. Maar net als programma 10.2 lijdt zo n oplossing aan het probleem van

169 10.4 Het snapshotobject 159 Fragment[1] Fragment[1].read modificatie van Fragment[i] Fragment[n] Scan Figuur 10.6: Contractiepunt van een directe scan. nieuw/oud inversie. Bovendien kan het achtereenvolgens lezen van de waarden worden onderbroken door updates, waardoor niet eens is gegarandeerd dat de opgeleverde combinatie ook ooit tegelijkertijd is voorgekomen. Het blokkeren van het object tijdens uitlezen is natuurlijk weer uit den boze. Kan dan de hele vector als een enkel register (van type V n ) worden geïmplementeerd? Bij een update van proces i moeten de waarden van andere processen ongewijzigd blijven, maar het schrijven in een register overschrijft de gehele toestand. Dus moet proces i eerst de hele vector lezen, dan zijn eigen component wijzigen en de hele vector weer wegschrijven. Uiteraard geeft dit problemen met de consistentie wanneer tussen het lezen en terugschrijven van de vector een andere thread de vector leest of schrijft. De oplossing van Afek et al. [AAD + 93] maakt uitsluitend gebruik van registers, en we hebben aan single writer registers genoeg. Net als in het vorige algoritme wordt elke waarde x (in de methode update(x)) opgeslagen samen met een serienummer, zodat een proces die deze waarde leest, kan vergelijken en zien of er sinds het vorige lezen van dit register iets veranderd is. De vector van n waarden wordt in n registers Fragment[i] opgeslagen, waarbij Fragment[i] wordt geschreven door proces i en door allen wordt gelezen. In de methode update(x) wordt éénmaal de waarde x naar Fragment[i] geschreven: het moment waarop dit gebeurt is het contractiepunt van de update operatie. Op dit moment wordt de toestand van het geïmplementeerde snapshot object veranderd. Het doel van de scan is nu om binnen een begrensd aantal operaties een rij waarden te vinden die tegelijk zijn voorgekomen binnen het tijdsinterval van de scan. Het tijdstip waarop die waarden tegelijk voorkwamen is dan het contractiepunt van de scan. Anders dan in het vorige programma zien we dat de control flow bij het scannen sterk wordt beïnvloed door de waarden die worden gelezen; het aantal instructies is echter begrensd zodat het programma toch wachtvrij is. Het idee achter een scan (uitgevoerd in de locale procedure makescan) is dat een proces de hele vector, component voor component, leest, en dan nog een keer leest: dit principe heet double collect. Als de tweede verkregen vector gelijk is (inclusief de serienummers!) aan de eerste, lag voor geen enkele i het contractiepunt van een update(x) tussen de twee inspecties van Fragment[i]; zie figuur Dit impliceert dat de verkregen vector gelijk is aan de toestand van het object op het tijdstip tussen de twee lees-ronden, en dit moment wordt gekozen als het contractiepunt van een dergelijke scan. Als er wel tussentijds is geschreven moet een nieuwe scan-poging worden gewaagd, daarom bevat programma 10.8 een lus in de makescan procedure, waarin de double collect wordt herhaald. (De nieuwe collect in array b wordt dan vergeleken met de tweede collect van de ronde ervoor.) Nu zou het te simpel zijn om het double collecten net zo lang te herhalen totdat er

170 Wachtvrije implementaties met registers.. Fragment[j] was onveranderd update(x) update(x).. Fragment[j] was veranderd Figuur 10.7: Begin van update ligt binnen scan. een keer geen tussentijdse update heeft plaatsgevonden; immers we moeten garanderen dat dit slechts begrensd vaak gebeurt! Hiertoe wordt een helpmechanisme ingebouwd ten behoeve van scannende threads die te vaak opnieuw moeten collecten wegens tussentijdse wijzigingen. Als een scanner voor de tweede maal constateert dat eenzelfde proces j tussentijds heeft geschreven (zie figuur 10.7), ligt het beginstuk van de tweede interfererende update binnen het interval van de scan. We laten nu elke update beginnen met het maken van een scan (ook weer via procedure makescan), die samen met de nieuwe waarde wordt opgeslagen. Een scan die voor de tweede maal een verandering in Fragment[j] constateert, levert de vector op die met Fragment[j] is opgeslagen. Het type van Fragment[j] is dan ook V N V n, en Fragment[j] heeft drie componenten: Fragment[j].v is de geschreven waarde, Fragment[j].sn is het serienummer en Fragment[j].s is de bijbehorende scan. De makescan procedure is door toevoeging van het helpmechanisme wachtvrij omdat hij termineert zodra wordt geconstateerd dat zeker proces tweemaal zijn Fragment heeft veranderd. Het maximale aantal slagen van de hoofdlus is dan n + 1 (na zoveel slagen moet een thread herhaald als geupdate j voorkomen) en hieruit volgt een bovengrens op het aantal instructies in de makescan. Beschouw nu een executie van het programma; we gaan elke scan en update een contractiepunt binnen zijn tijdsinterval geven en aantonen dat de semantiek van het object klopt. Zoals gezegd, het contractiepunt van een update(x) is het moment waarop x (samen met een serienummer en een scan) naar Fragment[j] wordt geschreven. Een uitvoering van makescan noemen we direct als de opgeleverde scan wordt verkregen doordat tweemaal dezelfde vector werd gelezen, en indirect als de scan uit een Fragment[j].s werd overgenomen. Lemma 10.7 Een directe makescan levert de toestand van het object op een tijdstip binnen de uitvoering van de makescan. Bewijs. Het betreffende tijdstip is een moment dat tussen de twee lees-ronden ligt, cf. figuur Omdat Fragment[j] niet geschreven werd tussen de eerste en tweede lezing, is de waarde op het moment van die tweede lezing gelijk aan de waarde op het contractiepunt. Lemma 10.8 Een indirecte makescan levert de toestand van het object op zoals die is berekend in een andere makescan, die geheel binnen de eerste makescan ligt. Bewijs. De opgeleverde vector, zeg door i, is geconstrueerd met makescan door j en weggeschreven in een update. Dit wegschrijven gebeurde tussen twee achtereenvolgende inspecties van Fragment[j] door i, en de makescan van j vond dus voor de laatste inspectie plaats; zie figuur Er was echter al eerder een verandering van Fragment[j] geconstateerd, en

171 10.4 Het snapshotobject 161 V,integer,V[n] Fragment[n] // Gedeeld integer seqnr[n] // seqnr[i] alleen voor i procedure makescan { boolean change[n] = (F,F,F,...,F) boolean succeed = F for (j=1 ; j<=n ; j++) { a[j] = Fragment[j].read } // first collect while not succeed do { for (j=1 ; j<=n ; j++) { b[j] = Fragment[j].read } // second collect } if (exists j: a[j].sn < b[j].sn) // update door j { if change[j] // tweede update { sc = b[j].s ; succeed = T } // indirecte scan else { change[j] = T a = b } } else { for (j=1 ; j<=n ; j++) { sc[j] = b[j].v } // directe scan succeed = T } } return sc scan door proces i: { return makescan } update(x) door proces i: { sc = makescan seqnr[i] ++ Fragment[i].write((x,seqNr[i],sc)) } Programma 10.8: Implementatie van snapshotobject deze eerdere update van j overlapte ook al met de makescan van i. De makescan waarvan uiteindelijk het resultaat is overgenomen is daarna begonnen, en ligt dus geheel binnen de makescan van i. Aannemende dat de makescan van j de toestand oplevert op een tijdstip binnen zijn uitvoering, volgt hetzelfde voor i, immers de makescan van j ligt geheel binnen die van i. Het bewijs

172 Wachtvrije implementaties met registers is nu formeel af te maken met een inductie over de nesting-structuur van makescan aanroepen: die nesting is eindig diep omdat er maar n processen tegelijk bezig kunnen zijn. De basisstap (een directe makescan) is bewezen in lemma 10.7, en de inductie-stap in lemma Samenvatting en conclusies In dit hoofdstuk is een begin gemaakt met de behandeling van wachtvrije synchronisatie. Doel hierbij is het implementeren van objecten, waarbij een thread nooit hoeft te wachten in een methode. De instructies van verschillende threads worden dan willekeurig geïnterleaved. Bij wachtvrije implementaties is vaak sprake van onderling helpen door threads. Als een thread signaleert dat een andere thread aan een operatie is begonnen, maakt hij hem af zodat de andere threads het resultaat ervan ook kunnen zien. Bij het bewijzen van de correctheid van de implementaties wordt vaak geredeneerd over het overlappen of juist voorafgaan van operaties. Er is speciale aandacht nodig voor het bewijzen van een bovengrens op het aantal stappen dat in een methode wordt gedaan. Registers hebben per methode slechts een richting van informatieuitwisseling. In een read verkrijgt een proces toestandsinformatie, maar de toestand wijzigt niet. In een write wijzigt de objecttoestand, maar het proces verkrijgt geen informatie over de toestand zoals die was. Het snapshotobject dat in dit hoofdstuk met registers werd geïmplementeerd vertoont een ingewikkelder gedrag, maar behoudt wel deze registerachtige scheiding tussen lezen en schrijven. Opgaven bij hoofdstuk 10 Opgave 10.1 Wacht-vrije implementatie. (Uit het tentamen van mei 2002.) Waaruit bestaat een wacht-vrije implementatie van class B en hoe komt hierin de eis van wacht-vrijheid tot uiting? Laat zien dat de relatie = tussen classes transitief is. Opgave 10.2 Implementeer een 2-consensusobject met een test-and-set object. Opgave 10.3 Breid het bewijs van stelling 10.5 uit om te laten zien dat het aantal SR-registers in gebruik voor communicatie tussen readers onderling, tenminste 1 2n(n 1) bedraagt. Opgave 10.4 Het is niet zo gemakkelijk om voor programma 10.4 een contractiepunt van de MRwrite aan te geven. Construeer een executie waarin de writer een 1 schrijft, die door reader 1 wordt gezien, waarna er een volledige read door reader 2 plaatsvindt die nog de oude waarde 0 oplevert. Laat door keuze van contractiepunten zien dat deze executie aan atomiciteit voldoet. Laat ook zien dat het contractiepunt van de MRwrite wordt vervroegd als reader 1 zijn operatie sneller afwikkelt. Opgave 10.5 Verander programma 10.4 zo dat minder SR-registers worden gebruikt. Kun je het aantal uit opgave 10.3 halen?

173 Opgaven bij hoofdstuk Opgave 10.6 Multiple-read registers. (Uit het tentamen van januari 2006.) In de implementatie van een Multiple-Reader register uit Single-Reader registers, werden n 2 Report registers gebruikt waarin readers elkaar informeren over de waarde die ze opgeleverd hebben. In de oplossing hiernaast wordt het aantal registers ongeveer gehalveerd: een reader schrijft alleen voor hoger genummerde readers, en een reader leest alleen van lager genummerde readers. Beargumenteer dat inversies zijn uitgesloten: als een read-operatie r 1 is afgelopen voordat read-operatie r 2 begint, dan is de waarde die r 2 oplevert, ten minste zo recent als die welke r 1 oplevert. MRwrite(x): { seqnr ++ for (i=1 ; i <= n ; i++) { Val[i].SRwrite((x,seqNr)) } } MRread voor proces i: { (v,s) = Val[i].SRread for (j=1 ; j < i ; j++) { (w,t) = Report[j,i].SRread if (t>s) then v,s = w,t } for (j=i+1 ; j <= n ; j++) { Report[i,j].SRwrite((v,s)) } return v } Opgave 10.7 Wacht-vrij Snapshot Object. (Uit het tentamen van mei 2000.) (a) Beschrijf de toestandsruimte en de methoden van een Snapshot object. In de implementatie gebruikten we een procedure makescan die onderscheid maakte tussen een directe en een indirecte scan. (b) Wanneer leidt een double collect tot een directe scan en waarom is de opgeleverde waarde dan correct? Opgave 10.8 Snapshot object. (Uit het tentamen van mei 2001.) (a) Wat zijn de methoden van een snapshot object (namen en semantiek). (b) Leg uit hoe (in de behandelde implementatie) een double collect kan leiden tot een directe scan en waarom het opgeleverde resultaat correct is. (c) Leg ook uit hoe threads elkaar helpen om te komen tot een indirecte scan. Opgave 10.9 Stel dat we programma 10.8 vereenvoudigen door de serienummers weg te laten, en de vergelijking tussen de first en second collect te doen met de test: if (exists j: a[j].v!= b[j].v) Laat ten eerste zien dat het mogelijk is dat er tussen de twee collects iets is veranderd aan het object maar dat dit niet wordt gemerkt. Probeer dan dit verschijnsel zo in een executie in te bedden dat er een incorrecte uitkomst ontstaat (er wordt een vector opgeleverd die nooit de toestand is geweest). Opgave Wat is het maximale aantal register-operaties (reads) in een makescan in programma 10.8? Opgave Wachtvrije counter. Het counter object heeft een integer toestand, initieel 0, en een methode inc die de waarde met 1 verhoogt en een show die de waarde oplevert. Dit is een wachtvrije implementatie van een counter voor 2 processen:

174 Wachtvrije implementaties met registers object counter var s[2] : register type integer, initieel 0 inc voor thread i: show voor thread i: t = s[i].read t1 = s[1].read t = t+1 t2 = s[2].read s[i].write(t) return t1+t2 (a) Waarom is dit een wachtvrije implementatie met registers? (b) Bewijs dat de methodes atomair zijn. (Hint: (1) definieer de toestand van de counter in termen van s[1] en s[2] en (2) kies als contractiepunt voor de inc het moment van schrijven in s[i].) (c) Bewijs (na lezing van hoofdstuk 11), dat het niet mogelijk is om met een counter 2- Consensus te implementeren.

175 Hoofdstuk 11 Wachtvrije consensus Met de definite van diverse objecttypen komt als vraagstelling naar voren: gegeven objecten van type A, is het mogelijk hiermee objecten van type B te implementeren. (Onder implementatie verstaan we in dit hoofdstuk altijd een wachtvrije implementatie.) Het zal blijken dat consensusobjecten een zeer centrale rol spelen bij het beantwoorden van deze vragen. In dit hoofdstuk zullen we enkele voorbeelden zien, waarin consensus wordt gebruikt om de onmogelijkheid van een implementatie aan te tonen. Er wordt bijvoorbeeld aangetoond dat een object B wel, en een object A niet in staat is om consensus te implementeren, waaruit volgt dat A, B niet implementeert. Deze redenering is alleen bruikbaar voor deterministische algoritmen, want, zoals we zullen zien in sectie 11.4, kan consensus gerandomiseerd met alleen registers worden geïmplementeerd Consensusobject met registers We herinneren ons dat een consensusobject slechts één methode, submit(x) heeft, en elke submit levert de invoer van de eerste submit op. De werking van een consensusobject is weergegeven als programma Uiteraard is dit programma niet bruikbaar als implementatie omdat bij meerdere processen de werking door interleaving van statements wordt verstoord. Een consensusobject dat door n processen kan worden aangesproken noemen we een n-consensus object. register toestand V + {@} submit(x): { if (toestand {toestand = x} return toestand } Programma 11.1: Werking van een consensusobject. 165

176 Wachtvrije consensus Natuurlijk kun je met registers een consensusobject maken dat werkt voor één enkel proces: je hebt dan van concurrency geen last en programma 11.1 kan als implementatie worden gebruikt. We tonen nu aan, dat een consensus object voor meer dan één proces niet met registers kan worden geïmplementeerd. Stelling 11.1 Er bestaat geen implementatie van 2-consensus met registers. Bewijs. Het bewijs is in zekere zin een verkorte en uitgeklede versie van het bewijs van Fischer, Lynch en Paterson; voor een uitleg van het zeer centrale begrip valentie verwijzen we naar sectie Stel dat er een wacht-vrije implementatie van 2-consensus bestaat. Een configuratie is in deze context: de toestanden van de gedeelde objecten en de processen (inclusief program counters). In een configuratie C zijn diverse stappen mogelijk doordat de volgende instructie van verschillende processen afkomstig kan zijn; met p i (C) bedoelen we de configuratie die verkregen wordt als vanuit C proces p i één stap doet. De eigenschappen van het wachtvrije rekenmodel kunnen nu worden vertaald naar eigenschappen van de executieboom van de veronderstelde oplossing. Propositie 11.2 Vanuit elke configuratie C bestaat er voor elk proces p i een reeks van stappen van uitsluitend p i, waarbij p i zijn submit operatie afmaakt. Bewijs. Het kan gebeuren dat het systeem in configuratie C is en dan alle processen behalve p i traag gaan werken. Er worden dan een tijdje lang alleen maar stappen van p i gedaan, en de wacht-vrijheid van de oplossing dwingt af dat p i zijn submit voltooit. De geëiste wachtvrijheid van de oplossing staat ons toe, conclusies te trekken als twee configuraties er, vanuit het oogpunt van een enkel proces, hetzelfde uitzien: Definitie 11.3 Configuraties C 1 en C 2 zijn i-gelijk, notatie C 1 i C 2, als de toestand van de gedeelde objecten en van p i in C 1 en C 2 overeen komen. Configuraties C 1 en C 2 verschillen slechts in de toestand van processen p j anders dan p i, en de wachtvrijheid verbiedt p i afhankelijk te zijn van het verkrijgen van informatie uit proces p j. Daarom mag met propositie 11.2 een conclusie getrokken worden over de valentie van i-gelijke configuraties. (Het lemma gaat over univalente configuraties; het is wel mogelijk dat van i-gelijke configuraties de ene univalent is en de andere bivalent. Dit maakt het mogelijk randomiserende oplossingen voor consensus te maken, maar hierop gaan we niet in.) Lemma 11.4 Zij C 1 en C 2 univalente configuraties en er is een i zdd. C 1 i C 2 ; dan is de valentie van C 1 en C 2 gelijk, dwz C 1 is v-valent dan en slechts dan als C 2 v-valent is. Bewijs. Stel het systeem zit in configuratie C 1 ; de wacht-vrijheid dwingt af dat er een reeks van stappen, slechts van p i, is waarmee p i zijn operatie afmaakt en een resultaat krijgt (propositie 11.2). Omdat C 1 v-valent is, is dit resultaat v. Maar dezelfde reeks van stappen van p i is geheel toepasbaar in C 2, immers, in C 2 zit p i in dezelfde toestand, en p i ziet dezelfde waarde van alle gedeelde objecten. Net als in sectie 6.3 construeren we vanuit een bivalente configuratie een oneindige run van bivalente configuraties, maar omdat de eisen op de oplossing hier wat sterker zijn, hebben we het bij de bewijsvoering gemakkelijker. Een bivalente initiële configuratie is bijvoorbeeld gemakkelijk gevonden. Zij C 0 de configuratie die ontstaat als p 1 een submit(0) start en p 2 een

177 11.2 Consensus met test-and-set, queue en compare-and-swap 167 submit(1), maar er is nog geen stap gezet. Bewering: C 0 is bivalent. Omdat de return-waarde van elke submit de invoer van de eerste submit is, zal, als p 1 vanuit C 0 voldoende stappen doet de waarde 0 afgedwongen worden en als p 2 voldoende stappen doet de waarde 1. Als C een bivalente configuratie is en p i (C) is univalent noemen we p i kritiek in C; dit zegt feitelijk dat p i de mogelijkheid heeft een beslissing af te dwingen in de (vooralsnog onbepaalde) configuratie C. Een cruciaal punt in het verhaal is dat de processen niet alle kritiek zijn: Lemma 11.5 Als C bivalent is, is tenminste één proces niet kritiek. Bewijs. Als alle processen kritiek zijn is p i (C) voor elke i univalent, maar omdat C zelf bivalent is moeten zich onder deze configuraties zowel 0- als 1-valente configuraties bevinden (opgave 6.11). Zij p i (C) 0-valent, en p j (C) 1-valent. Wat voor stappen kunnen er vanuit C door processen p i en p j gezet worden? Als één ervan geen register gebruikt of ze gebruiken verschillende registers, dan geldt p i (p j (C)) = p j (p i (C)), en dit is strijdig met de verschillende valentie van p i (C) en p j (C). Hetzelfde geldt als beide processen lezen uit hetzelfde register. Het overblijvend geval is waar één proces (zeg p i ) schrijft in een register dat door p j wordt gebruikt (voor lezen of schrijven). Echter, wie schrijft in een register maakt de voorafgaande actie op dat register onzichtbaar: de toestand van het register is in p i (p j (C)) gelijk aan die in p i (C). Omdat ook de interne toestand van p i gelijk is en de overige gedeelde objecten überhaupt niet veranderd zijn in deze stappen, geldt p i (p j (C)) i p i (C), hetgeen met lemma 11.4 tegenspreekt dat p j (C) 1-valent is. Vanuit configuratie C 0 kan het systeem willekeurig lang bivalent blijven, namelijk als steeds een stap van een niet-kritiek proces wordt uitgevoerd. Dit is een tegenspraak met de wacht-vrijheid en bewijst stelling Consensus met test-and-set, queue en compare-and-swap Als we de mogelijkheid onderzoeken om consensus te implementeren met andere objecten uit hoofdstuk 10, blijkt al snel dat sommige tot meer in staat zijn dan registers. In sectie zien we dat test-and-set wel 2-consensus, maar geen 3-consensus kan implementeren. Daarna nemen we aan dat objecten van type queue aanwezig zijn en onderzoeken de mogelijkheden om daarmee consensusobjecten te maken. In sectie zullen we zien dat er conclusies te trekken zijn die verder gaan dan die over consensusobjecten Consensus met test-and-set Programma 11.2 implementeert 2-consensus met een test-and-set. Initiëel bevat de test-andset arbiter een 0, en in een submit(v) schrijft een proces eerst zijn waarde op (in register proposal[i]) en doet een TaS. Komt daar 0 uit, dan was het proces de eerste die een TaS deed en daarmee wint zijn invoer de strijd om de opgeleverde waarde te worden. Leverde de TaS 1 op, dan was het andere proces eerder en wordt diens invoer overgenomen (als er twee processen, 1 en 2 zijn, is 3 i het nummer van het andere proces).

178 Wachtvrije consensus register proposal[2] : // schrijf invoer weg testandset arbiter : init 0 // wie was eerst? submit(v) voor proces i: { proposal[i] = v test = arbiter.tas if (test == 1) { return proposal[3-i] } else { return proposal[i] } } Programma 11.2: Implementatie van 2-consensus met test-and-set Belangrijk in de werking is de eigenschap van de TaS methode die atomair informatie over de toestand geeft en die wijzigt. Hierdoor kan een thread zien of hij de eerste was die een operatie uitvoerde, of niet, en dit blijkt voldoende voor 2-consensus. Je kunt een soortgelijke truuk uithalen met een queue (sectie ) of een stack object. Willen we programma 11.2 uitbreiden naar 3 processen, en dat willen we, dan stuiten we op een probleem. Een proces dat 1 uit de arbiter krijgt weet, dat het niet als eerste was, maar weet vervolgens niet welk van de andere twee processen wel als eerste was. Dit is niet op te lossen door een proces, na de TaS, naar een shared object te laten schrijven of het al dan niet de consensus gewonnen heeft. Tussen de TaS en die schrijfactie kan willekeurig lang verlopen waardoor de andere processen niet wachtvrij verder kunnen. Je denkt misschien dat het nu tijd is om een list te verzinnen, maar geen list is slim genoeg: de test-and-set is onvoldoende krachtig om 3-consensus te implementeren, wat we gaan bewijzen met een uitbreiding van stelling Hoewel de constructie in programma 11.2 alleen de TaS methode van test-and-set gebruikt, moeten we in het bewijs natuurlijk over alle mogelijkheden van het object redeneren, en ook de reset bekijken. Stelling 11.6 Er bestaat geen implementatie van 3-consensus met test-and-set. Bewijs. Definitie 11.3 en lemma 11.4 zijn ook in een systeem met drie processen van toepassing, en op dezelfde manier als in het bewijs van stelling 11.1 vinden we een bivalente beginconfiguratie. Slechts lemma 11.5 moet voor het mogelijk gebruik van test-and-set worden uitgebreid. Lemma 11.7 Als C bivalent is, is tenminste één proces niet kritiek. Bewijs. Als alle processen kritiek zijn is p i (C) voor elke i univalent, maar omdat C zelf bivalent is moeten zich onder deze configuraties zowel 0- als 1-valente configuraties bevinden. Zij p i (C) 0-valent, p j (C) 1-valent, en p k het derde proces (ongelijk p i en p j ). Wat voor stappen kunnen er vanuit C door processen p i en p j gezet worden? Als ze verschillende objecten gebruiken of hetzelfde register volgt een tegenspraak als in het bewijs van lemma 11.5; in de andere gevallen gebruiken p i en p j dezelfde test-and-set T.

179 11.2 Consensus met test-and-set, queue en compare-and-swap 169 Als p i en p j beide een T.TaS doen: De TaS methode heeft geen argumenten, en het effect van een TaS operatie door p i is voor p k niet te onderscheiden van een operatie door p j. Er geldt p i (C) k p j (C) ( p k kan niet zien wie er getast heeft ) en een tegenspraak volgt met lemma (Merk op dat niet geldt p i (C) i p j (C) en ook niet p i (C) j p j (C). Immers, p i en p j zien zelf wel verschil tussen een configuratie waarin ze al wel, of nog niet de TaS hebben gedaan. Ook kunnen ze, afhankelijk van de volgorde, een ander resultaat van de TaS krijgen.) Als p i en p j beide een T.reset doen: De reset methode heeft geen argumenten, en het effect van een reset operatie door p i is voor p k niet te onderscheiden van een operatie door p j. Er geldt p i (C) k p j (C) ( p k kan niet zien wie er gereset heeft ) en een tegenspraak volgt met lemma Als p i een T.TaS doet en p j een T.reset: In configuratie p i (C) kan p j nog altijd de reset doen, wat leidt tot een configuratie p j (p i (C)), die volgens de aanname (dat p i (C) 0-valent is), 0-valent is. De reset methode resulteert altijd in waarde 0 van het object, ongeacht de waarde die het daarvoor had, met andere woorden, de reset door p j, in configuratie C of p i (C), verbergt voor p k of de TaS door p i heeft plaatsgevonden. Er geldt p j (p i (C)) k p j (C) ( p k kan niet zien of p j getast heeft ) en een tegenspraak volgt met lemma (Het geval dat p i een T.reset doet en p j een T.TaS verloopt analoog.) Net als in het bewijs van stelling 11.1 is hiermee bewezen dat het systeem willekeurig lang bivalent kan blijven, hetgeen het bewijs van stelling 11.8 voltooit Queue implementeert 2-consensus Programma 11.3 implementeert 2-consensus met een queue, op een manier vergelijkbaar met programma Initiëel bevat de queue één getal (1), en in een submit(v) schrijft een proces eerst zijn waarde op en probeert dan uit de queue te lezen. Komt er iets uit, dan was het proces de eerste die uit de queue las en daarmee wint zijn invoer de strijd om de opgeleverde waarde te worden. Leverde de op, dan was het andere proces eerder en wordt diens invoer overgenomen. Net als de TaS methode van test-and-set, kan de deq methode atomair informatie over de toestand geven en die wijzigen. Hierdoor gebruik je alleen maar van de queue dat een thread kan zien of hij de eerste was die er een operatie op uitvoerde of niet. Je kunt een soortgelijke truuk uithalen met een zwakke electie of een stack object Queue implementeert geen 3-consensus Willen we programma 11.3 uitbreiden naar 3 processen, en dat willen we, dan stuiten we op een probleem. Net als de test-and-set, kan de queue een proces wel laten weten of hij de eerste was met een bepaalde operatie of niet, maar niet welk proces dan eventueel wel eerst was. Ook van de queue gaan we bewijzen dat hij onvoldoende krachtig is om 3-consensus te implementeren, weer met een uitbreiding van stelling Het basisidee van het bewijs is hetzelfde, alleen er moet natuurlijk met de specifieke eigenschappen van de queue worden geredeneerd en daardoor pakt één van de te behandelen gevallen veel ingewikkelder uit. Herinner, dat er bij een queue geen front methode wordt verondersteld.

180 Wachtvrije consensus register proposal[2] : // schrijf invoer weg queue arbiter : init <1> // wie was eerst? submit(v) voor proces i: { proposal[i] = v test = arbiter.deq if (test { return proposal[3-i] } else { return proposal[i] } } Programma 11.3: Implementatie van 2-consensus met queue Stelling 11.8 Er bestaat geen imlementatie van 3-consensus met queues. Bewijs. Definitie 11.3 en lemma 11.4 zijn ook in een systeem met drie processen van toepassing, en op dezelfde manier als in het bewijs van stelling 11.1 vinden we een bivalente beginconfiguratie. Slechts lemma 11.5 moet voor het mogelijk gebruik van queues worden opgerekt. Lemma 11.9 Als C bivalent is, is tenminste één proces niet kritiek. Bewijs. Als alle processen kritiek zijn is p i (C) voor elke i univalent, maar omdat C zelf bivalent is moeten zich onder deze configuraties zowel 0- als 1-valente configuraties bevinden. Zij p i (C) 0-valent, p j (C) 1-valent, en p k het derde proces (ongelijk p i en p j ). Wat voor stappen kunnen er vanuit C door processen p i en p j gezet worden? Als ze verschillende objecten gebruiken of hetzelfde register volgt een tegenspraak als in het bewijs van lemma 11.5; in de andere gevallen gebruiken p i en p j dezelfde queue Q. Als p i en p j beide een Q.deq doen geldt p i (C) k p j (C) ( p k kan niet zien wie er eerst gedequeued heeft ) en een tegenspraak volgt met lemma Als p i een Q.enq(x) doet en p j een Q.deq en Q is niet leeg in C, dan is p j (p i (C)) = p i (p j (C)) en dit is in tegenspraak met de verschillende validiteit van p i (C) en p j (C). Als p i een Q.enq(x) doet en p j een Q.deq en Q is leeg in C, zal p j het wel merken of p i eerst een Q.enq(x) uitvoert, maw. in p j (p i (C)) heeft p j een andere toestand dan in p j (C); p j kreeg in het eerste retour. Echter, voor p k is niet te zien of p j al dan niet gelezen heeft voor het enqueuen door p i omdat een Q.deq oplevert de toestand van Q niet verandert. Er geldt in dit geval p i (C) k p i (p j (C)) en dit is wegens lemma 11.4 weer in tegenspraak met de verschillende valenties van p i (C) en p j (C). Het geval dat p i een Q.deq doet en p j gevallen. een Q.enq(x) is analoog aan de vorige twee Het laatste geval is waar p i een Q.enq(a) doet en p j een Q.enq(b); zie figuur In configuratie C staat dus zowel p i als p j op het punt om een Q.enq te doen (van waarde a, respectievelijk b). Merk op dat het voor de locale toestand van p i en p j niet uitmaakt

181 11.2 Consensus met test-and-set, queue en compare-and-swap 171 C : b p i (C) : 0 p j (C) : 1 C 0 : 0 C 1 : 1 C.. 0 = σ (C 0 ) : 0 C 1 Q.deq = σ (C 1 ) : 1 D 0 = τ (C 0 ) : 0 D 1 = τ (C 1 ) : 1 Q.deq. σ(c 0 ) : 0... τ(c 0 ) p 1 (p 2 (D 0 )) : 0 p 1 (p 2 (D 1 )) : 1 Figuur 11.4: Processen p i en p j doen een Q.enq(). welk van de twee processen het eerst een stap doet, want bij een Q.enq verkrijgt een proces geen informatie uit de queue. Toch hangt de valentie volgens de aanname af van de volgorde waarin de processen hun Q.enq uitvoeren: C 0 = p j (p i (C)) is 0-valent en C 1 = p i (p j (C)) is 1-valent, en deze twee configuraties verschillen slechts in de volgorde waarin a en b in de queue staan. De rest van het bewijst preciseert de volgende intuïtie. Het verschil tussen de twee configuraties zit alleen in de volgorde van slechts twee elementen in de queue. Het uitlezen van informatie uit een queue is destructief, dwz. dat een proces alleen iets aan de queue kan zien door het eruit te halen (maar vergelijk opgave 11.6!). Daarom kunnen slechts twee threads het verschil tussen de twee configuraties direct waarnemen. En ze zijn met z n drieën, maar het derde proces mag er niet van afhankelijk zijn dat één van de andere twee hem vertelt wat hij uit de queue heeft gelezen, dit is nl. strijdig met de wachtvrijheid. Daarom construeren we een executie waarin twee processen de queue uitlezen; deze reeks is toepasbaar in C 0 en C 1, en levert twee configuraties die voor het derde proces gelijk zijn. Zij ρ de inhoud van Q in configuratie C. Omdat het systeem wachtvrij is, is er (volgens propositie 11.2) vanuit C 0 een reeks σ van stappen van alleen p 1 waarin p 1 iets retourneert; omdat C 0 0-valent is, is dit de waarde 0. Omdat dezelfde reeks van stappen niet toepasbaar is vanuit C 1 (die is immers 1-valent en van daaruit is geen 0-beslissing mogelijk) wordt in σ op z n minst de waarde a door p 0 gelezen. Nu houden we p 1 even tegen op het moment dat hij die Q.deq wil doen: σ is de prefix van σ tot aan die stap en C 0 = σ (C 0 ). Merk op dat de reeks σ ook in C 1 toepasbaar is (het verschil in queue-toestand wordt pas geobserveerd als a of b uit de queue gelezen wordt), en zij

182 Wachtvrije consensus C 1 = σ (C 1 ). Omdat het systeem wacht-vrij is, is er vanuit C 0 een reeks τ van stappen van alleen p 2 waarin p 2 iets retourneert; omdat C 0 0-valent is, is dit de waarde 0. Omdat dezelfde reeks van stappen niet toepasbaar is vanuit C 1 (die is immers 1-valent en van daaruit is geen 0-beslissing mogelijk) wordt in τ op z n minst éénmaal uit de queue gelezen. Nu houden we p 2 even tegen op het moment dat hij dat wil doen: τ is de langste prefix van τ waarin p 2 geen Q.deq uitvoert. Merk op dat de reeks τ ook in C 1 toepasbaar is. Zij nu D 0 = τ (C 0 ) en D 1 = τ (C 1 ). Zowel in D 0 als in D 1 staan zowel p 1 als p 2 op het punt een Q.deq uit te voeren, het enige verschil tussen D 0 en D 1 is dat in de ene Q begint met ab en in de andere met ba, en de valentie verschilt. Als nu beide processen hun Q.deq uitvoeren, is er voor p 3 geen verschil meer te observeren: p 1 (p 2 (D 0 )) 3 p 1 (p 2 (D 1 )), hetgeen met lemma 11.4 een tegenspraak is met de verschillende valentie van D 0 en D 1. Net als in het bewijs van stelling 11.1 is hiermee bewezen dat het systeem willekeurig lang bivalent kan blijven, hetgeen het bewijs van stelling 11.8 voltooit Het consensusgetal De resultaten tot nu toe hadden allemaal betrekking op consensus, maar we kunnen nu concluderen dat registers onvoldoende sterk zijn om een queue te implementeren. Corollarium Er bestaat geen implementatie van een queue met registers. Bewijs. Omdat de queue 2-consensus implementeert (sectie ) en implementatie transitief is (lemma 10.3) zou een dergelijke implementatie bewijzen dat registers 2-consensus implementeren, wat in tegenpraak is met stelling We herinneren ons dat type queue het klassieke producer-consumer probleem beschrijft; we hebben net bewezen dat in een systeem dat alleen registers biedt, dit probleem niet wachtvrij op te lossen is. Het wordt duidelijk dat we een gereedschap in handen hebben om het niet bestaan van een implementatie te bewijzen: als object A voor meer processen consensus kan implementeren dan object B, dan bestaat er geen implementatie van A met B. Definitie Het consensusgetal van class A, notatie CN(A), is het hoogste aantal n waarvoor er een implementatie van n-consensus met A bestaat ( als zo n implementatie voor alle n bestaat). Stelling Als CN(A) > CN(B) dan B = A in een systeem met meer dan CN(B) processen. Het bewijs (met transitiviteit van =) laten we graag aan de lezer over; we vatten de reeds bekende consensusgetallen samen in de volgende stelling. Stelling Registers hebben consensusgetal 1, Queues hebben consensusgetal 2. Het is een erg populaire sport om van allerlei obscure typen het consensusgetal te achterhalen. Men kan zich na het bestuderen van deze sectie wel voorstellen dat een implementatie van n-consensus met zeker object nog redelijk overzichtelijk is. Het bewijs dat (n + 1)-consensus onmogelijk is, is daarentegen vaak behoorlijk gecompliceerd; stelling 11.8 is hier nog maar een simpel voorbeeld van! Een kleine greep uit wat er bekend is:

183 11.3 Wachtvrije hiërarchieën 173 compare-and-swap toestand: submit(v): { w = toestand.cas(@,v) if (w { return v } else { return w } } Programma 11.5: Consensus met compare-and-swap. Het consensusgetal van test-and-set en stack is 2. Voor elk natuurlijk getal n bestaat er een object met consensusgetal n Consensusobject met compare-and-swap De compare-and-swap blijkt bijzonder krachtig: hij heeft consensusgetal oneindig, omdat hij voor elke n, n-consensus implementeert en wel volgens programma Voor het implementeren van een consensusobject dat waarden uit verzameling V kan verwerken, gebruiken we een compare-and-swap object met toestandsruimte V {@}, betekent dat er nog geen waarde is beslist. De CaS-methode van compare-and-swap staat toe dat een proces atomair bekijkt of de toestand is, en in dat geval de waarde vervangt door zijn invoer Wachtvrije hiërarchieën Een klassificatie van wachtvrije objecten in sterke en minder sterke objecten volgens een scherp gedefiniëerd criterium noemen we een wachtvrije hiërarchie. In dit hoofdstuk baseerden we de indeling op de mogelijkheid, met het object deterministisch het consensus probleem op te lossen; daarom heet deze indeling de consensushiërarchie. Hij werd voorgesteld door Herlihy in 1991 [Her91], maar er zijn ook andere mogelijk. De consensushiërarchie is heel geschikt om individuele classes te vergelijken; er geldt namelijk: Als CN(A) < CN(B), dan A = B. Dit is stelling 11.12; de conclusie refereert natuurlijk aan deterministische implementaties. Object A implementeert (deterministisch) alle classes, dus ook consensus, in elk systeem met hoogstens CN(A) processen. Dit wordt bewezen in hoofdstuk 12. Voor het vergelijken van combinaties van classen is de consensushiërarchie niet geschikt. Er zijn diverse voorbeelden bekend van typen A 1 en A 2, beide met consensusgetal k, die in combinatie met elkaar een class met consensusgetal > k implementeren. Ook wanneer naar randomiserende algoritmen wordt gekeken, is de hiërarchie niet geschikt, omdat er dan geen verschil meer is tussen de consensus-klassen. In sectie 11.4 laten we zien

184 Wachtvrije consensus dat met randomisering, registers in staat zijn consensus te implementeren voor willekeurig veel processen. En mocht je denken dat met twee trema s in één woord het maximum van de Nederlandse taal wel bereikt is: het is mogelijk op een abstracter niveau de verschillende hiërarchieën te vergelijken en te klassificeren in een hiërarchieënhiërarchie (waarvan er weer meerdere zijn trouwens) Consensus met randomisering Net als in het berichten-model is randomisering voldoende om het onmogelijkheidsresultaat voor Consensus te doorbreken. Programma 11.6 toont een algoritme van Buhrman et al. [BPSV06], dat Consensus bereikt voor een willekeurig aantal processen; het gebruikt randomisering en geen andere gedeelde objecten dan (multiple-reader, multiple writer) registers. Een proces dat v submit, begint aan ronde 1 met waarde v. We zeggen dat waarde v ronde p heeft bereikt, als een proces het register bereik[v, p] op True heeft gezet. Een proces dat (met v) ronde p bereikt zet dit register, en kijkt dan, welke ronde al is bereikt door de andere waarde (1 v). Als de andere waarde de volgende ronde (p + 1) al heeft bereikt, wordt een overstap overwogen; als de andere waarde ook ronde p heeft bereikt wordt een random keuze gedaan; als de andere waarde pas in ronde p 1 is zal het proces waarde v houden, als zelfs ronde p 1 nog niet is gehaald, wordt op v beslist. Een overwogen overstap wordt geannuleerd als de eigen waarde inmiddels ronde p + 1 heeft bereikt. Merk allereerst op, dat er geen gaten in de bereikte waarden van v vallen: bereik[v,p] kan alleen True worden als bereik[v,p-1] dat al is. Noemen we het front voor v de hoogste p waarvoor bereik[v,p] True is, dan kunnen de fronten dus steeds met één positie tegelijk opschuiven, waarbij ze elkaar willekeurig vaak in kunnen halen. Een belangrijke observatie voor de geldigheid is dat het front voor v alleen positief wordt als minstens een proces v submit. Belangrijk voor de overeenstemming is dat als het front voor v ooit twee posities voorkomt op dat van 1 v, het front van 1 v nog hoogstens 1 positie wint. Lemma Als op tijdstip t, bereik[v,p+1] True is en bereik[1-v,p] False, dan blijft bereik[1-v,p+1] daarna altijd False. Bewijs. Een proces dat na t met waarde 1 v aan ronde p begint, kan bereik[1-v,p] nog op True zetten, maar de constatering dat op dat moment bereik[v,p+1] al True is zal dit proces doen overwegen, over te stappen naar v. Die overstap wordt inderdaad gemaakt omdat bij de laatste test, bereik[1-v,p+1] nog steeds op False staat. Uit lemma volgt dat als het front voor v twee plaatsen voorligt, alle processen daarna met die waarde verder gaan en, op z n laatst in ronde p+2, op v beslissen. De fronten schuiven atomair op, maar niet tegelijk en daarom ligt er telkens een van de waarden één positie voor. Het voorliggende front kan zijn voorsprong vergroten tot twee met een probabilistisch duwtje in de rug, dat bestaat uit een uniforme random keuze in één ronde. Terminatie volgt uit lemma 11.15, dat impliceert dat elke ronde een kans van minstens 1/2 n heeft om de situatie uit het vorige lemma te realiseren. Voor willekeurige ronde p is hierin v de waarde die het eerst ronde p bereikt. Lemma Als (1) op tijdstip t, bereik[v,p] True is en bereik[1-v,p] False, en (2) alle random keuzes in ronde p zijn voor v, dan blijft bereik[1-v,p+1] altijd False.

185 11.4 Consensus met randomisering 175 register bereik[2, int] initieel bereik[0, 0] = bereik[1, 0] = True submit(v) voor proces i: p = 1 while (dec { bereik[v, p] = True if (bereik[ 1-v, p+1]) { overweeg = 1-v } else if (bereik[ 1-v, p]) { overweeg = random(0,1) } else if (bereik[ 1-v, p-1 ]) { overweeg = v } else { dec = v } if (not bereik[v, p+1]) { v = overweeg } p = p+1 } return dec Programma 11.6: Gerandomiseerde n-proces consensus. Bewijs. Zolang bereik[1-v,p+1] inderdaad nog op False staat, is het gedrag van processen na t als volgt. Een proces dat met v ronde p inkomt (of daar al is op t), constateert niet dat de andere waarde de volgende ronde al heeft bereikt, en overweegt dus een random keuze of v te houden. De tweede premisse impliceert dat dit proces in ieder geval met v naar de volgende ronde gaat. Een proces dat met v 1 ronde p ingaat, doet dat na t. Het ziet misschien dat v al ronde p + 1 heeft bereikt, het overweegt dan v, maar anders ziet het zeker dat v ronde p heeft bereikt en overweegt een random keuze, die conform de tweede premisse voor v uitvalt. Daar bereik[1-v,p+1] nog False is, kiest het proces die waarde inderdaad. We zien hieruit dat elk proces dat door ronde p komt, met v naar ronde p+1 gaat, waardoor bereik[1-v,p+1] inderdaad False blijft. Uit de twee lemma s is de correctheid van het algoritme gemakkelijk af te leiden. Net als Ben-Or s algoritme (sectie 7.3) is de complexiteit erg hoog; de kans op succes in een ronde is namelijk exponentieel klein. Hiervoor is een oplossing bedacht, namelijk de shared coin; dit is een gedistribueerd protocol waarin stations op fout-tolerante manier een random trekking doen, maar op zo n manier dat gelijkheid met significante kans geldt. De complexiteit van Ben-Or s algoritme en van programma 11.6 wordt hiermee polynomiaal, maar we gaan hierop verder niet in (zie [BPSV06]). Een interessante eigenschap van programma 11.6 is, dat er geen registers worden gebruikt waarvan de betekenis in relatie staat tot een bepaald proces. Ook wordt in het programma de index i van een proces helemaal niet gebruikt. Het kan daarom worden gebruikt in situaties waar de processen niet door unieke namen of nummers worden onderscheiden, en Buhrman et al. [BPSV06] noemen een dergelijk algoritme anoniem.

186 Wachtvrije consensus Samenvatting en conclusies In dit hoofdstuk werd gereedschap ontwikkeld om diverse wachtvrije objecten onderling in sterkte te kunnen vergelijken. Er wordt, voor een object A, gekeken naar het hoogste aantal processen waarvoor een of meerdere A-objecten een consensusobject kunnen implementeren. Dit aantal heet het consensusgetal, CN(A), van A. We hebben gezien dat registers geen consensus kunnen implementeren voor twee processen, en dus consensusgetal 1 hebben. Een queue kan wel consensus implementeren voor twee processen, maar niet voor drie, en heeft dus consensusgetal 2. Een compare-and-swap implementeert consensus voor willekeurig veel processen en heeft consensusgetal. Hoe sterk is een object met een hoog consensusgetal? In dit hoofdstuk werden consensusgetallen gebruikt (zie stelling 11.12) om het niet bestaan van een implementatie aan te tonen. De stelling beantwoordt niet de vraag of er een implementatie van queue vanuit compare-andswap is. De premisse van de stelling is hier niet van toepassing omdat het consensusgetal van compare-and-swap hoger is dan van queue. Maar dat we de stelling niet kunnen toepassen impliceert nog niet dat de conclusie onwaar is. Met andere woorden, om ook het bestaan van implementaties te kunnen concluderen uit consensusgetallen moeten we nog meer dingen bewijzen; dit is het onderwerp van hoofdstuk 12. De consensus-hiërarchie is alleen een zinvol instrument voor het bekijken van deterministische implementaties. Met randomisering kan consensus met registers worden geïmplementeerd, waardoor er een ineenstorting van de hiërarchie plaatsvindt. Opgaven bij hoofdstuk 11 Opgave 11.1 Laat zien dat test-and-set consensusgetal 2 heeft. Opgave 11.2 Laat zien hoe 2-consensus kan worden geïmplementeerd met een stack. (Hint: ga uit van programma 11.3.) Opgave 11.3 Laat zien dat de stack geen 3-consensus implementeert. stelling 11.8.) (Hint: ga uit van Opgave 11.4 Een fetch-and-increment object heeft toestandsruimte N (initiële toestand 0) en een methode FaI die de toestand oplevert en met 1 verhoogt. Wat is het consensusgetal van dit type? Opgave 11.5 Consensus met Fetch-and-Increment. (Uit het tentamen van mei 2000.) Een fetch-and-increment object heeft als toestand een integer (initiëel 0), en als enige methode FaI, die de waarde van het object verhoogt en de nieuwe waarde oplevert. (a) Geef een implementatie van Consensus voor 2 processen met een fetch-and-increment object. (b) Bewijs dat er geen implementatie van 3-Consensus met dit object bestaat. (Je mag de begrippen en lemma s uit het dictaat gebruiken.) (c) Wat is het consensus getal van fetch-and-increment? Opgave 11.6 Het bewijs van stelling 11.8 gebruikte essentiëel dat je uit een queue niet kunt lezen zonder de toestand te veranderen. Een front queue heeft behalve de methoden enq(v) en

187 Opgaven bij hoofdstuk deq een methode front die de eerste waarde uit de queue oplevert bij lege queue) maar niet verwijdert. Bewijs dat deze front queue consensusgetal heeft. Opgave 11.7 Consensus met Front-Queue. (Uit het tentamen van februari 2003.) Een Front-Queue object heeft een rijtje als toestand en methoden: enq(x) die x achteraan toevoegt, deq die het voorste element verwijdert en oplevert (@ als het rijtje leeg is), en front die het voorste element oplevert (@ als het rijtje leeg is) maar niet verwijdert. (a) Beargumenteer of de Front-Queue krachtig genoeg is om Consensus te implementeren voor 1 thread en voor 2 threads. (b) Kun je met een Front-Queue consensus implementeren voor 3 threads? (c) Wat is het Consensus-getal van de Front-Queue? Opgave 11.8 Consensus met Ticket Dispenser. (Uit het tentamen van februari 2001.) Een Ticket Dispenser object heeft een integer toestand (initiëel 1) en één methode: ticket, die de waarde oplevert èn verhoogt. Beargumenteer, of de Ticket Dispenser krachtig genoeg is om Consensus te implementeren voor 1, 2, of 3 threads. Wat is het Consensus-getal van de Ticket Dispenser? Opgave 11.9 Geef een implementatie van 2-consensus met type queue (waarbij je er wel twee mag gebruiken) waarbij de gebruikte queues initiëel leeg zijn. Opgave Wat is het consensusgetal van een snapshotobject? Opgave In hoofdstuk 10 was sprake van twee soorten electie-object. Het zwakke type retourneert op een elect-methode aan exact één thread leider en aan de anderen nonleider. Het sterke type retourneert aan elk proces de identiteit van een van de aanroepende processen. (a) Bewijs dat het eerste type consensusgetal 2 heeft. (b) Bewijs dat het tweede type consensusgetal heeft. Opgave Universaliteit van Memory-Swap. (Uit het tentamen van mei 2001.) De memory-swap klasse bestaat uit een array van registers. Elk register kan individueel gelezen (met read(i)) en geschreven (met write(i,x)) worden. Daarnaast is er een swap(i,j) methode die de waarden op locaties i en j verwisselt. (a) Wanneer is een klasse universeel (voor een systeem)? (b) Bewijs dat een memory-swap object met k registers universeel is in een systeem met k 1 threads. Opgave Consensus met Swap-register. (Uit het tentamen van januari 2006.) Een swap-register is een object dat als toestandsruimte een vector van waarden heeft. Er zijn operaties write(i,x) die de waarde x in locatie i schrijft, en read(i) die de waarde van locatie i oplevert. Daarnaast is er een operatie swap(i,j) die de waarden van locaties i en j verwisselt. We gaan laten zien dat een swap-register Arb met n + 1 locaties (genummerd 0 t/m n), n-consensus kan implementeren. (a) In de submit(x) operatie zal thread i precies eenmaal een instructie Arb.swap(0,i) uitvoeren. Hoe kun je de initiële waarde van Arb kiezen om ervoor te zorgen dat de eerste swap-operatie iets aan de inhoud verandert, maar de latere swaps niet meer? (b) Kan een thread na het uitvoeren van zijn swap zien, als hoeveelste hij de swap deed? (c) Kan een thread na het uitvoeren van zijn swap zien, welke thread als eerste de swap deed? (d) Kan een thread na het uitvoeren van zijn swap zien, welke thread als tweede de swap deed? (e) Geef een wachtvrij algoritme dat een swap-register gebruikt en n-consensus implementeert.

188 Wachtvrije consensus Opgave Counters maken met Snapshot. (Uit het tentamen van november 2007.) We willen een wacht-vrije counter maken; zo n object heeft een integer toestand (initieel 0) en operaties inc, dec, read die deze waarde ophogen, aflagen, en uitlezen. Maak gebruik van een object van type snapshot, waarin de i de waarde voorstelt: het aantal incs min het aantal decs dat is uitgevoerd door thread i. (a) Wat zijn de operaties van een snapshot object? (b) Geef een implementatie van de counter, gebruikmakend van snapshot. (c) Beargumenteer, waarom de uitkomst van een read correct is. (d) Een readcounter is een object dat een gecombineerde read plus increment als methode heeft: de operatie rinc geeft je de (oude) waarde van de counter en verhoogt die. Is de readcounter wachtvrij met een snapshot te implementeren? Opgave Counters en Snapshot. (Uit het tentamen van juli 2011.) Een Counter object heeft als toestand een integer (initieel 0) en operaties inc (die de waarde met 1 verhoogt) en read (die de waarde oplevert). Een Counter is wachtvrij te maken met een Snapshot object. (a) Welke operaties kent een Snapshot en wat doen ze? (b) De Counter is te maken met een Snapshot door in fragment i te zetten: het aantal inc dat is voltooid door thread i. Beschrijf de initialisatie en hoe de twee operaties van Counter worden geïmplementeerd. (Je hoeft niet te zeggen hoe de Snapshot-operaties geïmplementeerd zijn.) Een TicketDispencer is een integer-object dat een operatie heeft, ticket, die de waarde ophoogt en oplevert. Je denkt: van de Counter kan ik de inc en de read wel combineren tot een ticket. De TicketDispencer T bevat dan een Counter C en de T.ticket methode doet dit: { C.inc; return C.read; }. (c) Dit idee is onjuist; beschrijf een mogelijke uitkomst van deze code, die niet is toegestaan voor een TicketDispencer. (d) Kun je een TicketDispencer wachtvrij implementeren met een Counter? Opgave Consensusgetal van Swapper. (Uit het tentamen van februari 2002.) Een Swapper-object heeft een waarde als interne toestand, en een methode swap(x) die deze toestand oplevert en vervangt door x. Wat is het consensusgetal van de Swapper?

189 Hoofdstuk 12 Universaliteit van consensus In hoofdstuk 11 zagen we enkele voorbeelden van implementaties die (deterministisch) niet mogelijk zijn, en hoe dit bewezen kan worden met consensusgetallen. In dit hoofdstuk (sectie 12.1) zien we enkele implementaties die wel mogelijk zijn. Ook hier spelen consensusgetallen een belangrijke rol, maar de werkelijkheid is iets gecompliceerder dan dat een class zondermeer alle classes met kleiner consensusgetal zou kunnen implementeren. Belangrijkste resultaat is, dat een n-consensus object universeel is in een systeem met n processen: het kan daar elk object implementeren, en dit zullen we zien in sectie Een consequentie is dat van de operaties die men tegenwoordig in hardware aangeboden ziet, de compare-and-swap de krachtigste is wanneer het op de ondersteuning van (deterministisch) wachtvrij rekenen aankomt. In gerandomiseerd rekenen is niet consensus, maar het uitdelen van unieke namen het meest cruciale primitieve object (sectie 12.4) Compare-and-swap implementeert test-and-set De test-and-set heeft consensusgetal 2, en compare-and-swap heeft consensusgetal. Het laatste type is dus sterker dan het eerste als het er op aan komt, consensus te implementeren. Maar het is ook in andere zin sterker; compare-and-swap implementeert test-and-set; zie programma Het test-and-set object kent, zoals vermeld in sectie , toestanden 0 en 1. De reset methode zet de toestand op 0 en hoeft dus alleen iets te veranderen als de toestand 1 is: de CaS(1,0) methode op de toestand is precies wat we nodig hebben. De TaS methode zet de toestand op 1, en verandert dus alleen iets als de toestand 0 is: de CaS(0,1) doet dit en levert de oude toestand op zoals ook voor de TaS methode nodig is. In programma 12.1 zijn de toegangsprocedures onafhankelijk van het proces dat ze uitvoert, en er zijn geen objecten die per proces informatie bijhouden. De implementatie werkt voor een willekeurig groot aantal processen Fetch-and-increment We gaan nu enkele voorbeelden bekijken met een nieuwe class, de (finite) fetch-and-increment, waarvan we het consensusgetal kunnen vaststellen zonder onmogelijkheidsbewijzen te hoeven 179

190 Universaliteit van consensus CompareAndSwap toestand set: { toestand.cas(0,1) } reset: { toestand.cas(1,0) } TaS: { return toestand.cas(0,1) } Programma 12.1: Test-and-set met compare-and-swap. geven. Een fetch-and-increment is een object dat een methode FaI heeft, die de waarde van het object oplevert en verhoogt. Het object geeft in volgorde de natuurlijke getallen uit: de i e aanroep geeft dus i. Het is daardoor te gebruiken als een ticket dispenser want het werkt precies als de nummertjesautomaat bij de bakker of in het postkantoor. Een finite fetch-and-increment geeft na een aantal aanroepen steeds hetzelfde getal. Preciezer gezegd, een k-fetch-and-increment object heeft toestandsruimte {1,..., k} en methode FaI die de waarde oplevert en, indien < k, met 1 ophoogt. De class k-fetch-and-increment heeft consensusgetal minstens 2, want je kunt er voor twee processen consensus mee implementeren op dezelfde manier als dat met de queue kan (programma 11.3). De fetch-and-increment is dus met registers niet te implementeren. (Opmerking: dit laatste geldt alleen voor k > 1!) Compare-and-swap implementeert fetch-and-increment Vaak is een implementatie wel wat gecompliceerder dan die in programma 12.1, waar elke methode van het geïmplementeerde object precies één aanroep op een onderliggend object vereiste. Met een compare-and-swap kan het wel (zie programma 12.2), maar de FaI methode CompareAndSwap next: init 1 FaI: { t = 1 ; s = False while ( t<k and not s) { r = next.cas(t,t+1) if (r==t) { s = True } else { t = r } } return t } Programma 12.2: k-fetch-and-increment met compare-and-swap.

191 12.2 Fetch-and-increment 181 TestAndSet weg[k-1] init 0 FaI: { t = 1 ; s = 1 while ( t <= k-1 and s==1 ) { s = weg[t].tas // Probeer nummer t te pakken if (s==1) { t++ } } return t } Programma 12.3: Finite fetch-and-increment met test-and-set. gebruikt een aantal achtereenvolgende aanroepen van compare-and-swap. Het compare-and-swap object next geeft de waarde van het volgende getal dat moet worden uitgedeeld, en het proces dat erin slaagt next te verhogen van t naar t+1 krijgt nummer t. Een proces begint pas aan next.cas(t,t+1) wanneer zeker is dat de waarde van next tenminste t is Test-and-set implementeert fetch-and-increment Niet alleen zijn soms meerdere operaties op een gebruikt object nodig, maar ook moet je soms van meerdere instanties van een class gebruik maken; hiervan volgt nu een voorbeeld. De k- fetch-and-increment is ook te bouwen uit een reeks van k 1 test-and-set objecten: test-and-set weg[i] geeft dan aan dat getal i al is uitgedeeld. Een proces gaat proberen nummer i te pakken als het heeft geconstateerd dat alle kleinere nummers al vergeven zijn; zie programma Het bestaan van een implementatie van fetch-and-increment uit test-and-set laat een conclusie toe over het consensusgetal van fetch-and-increment. Omdat fetch-and-increment consensus implementeert voor twee processen en zelf uit test-and-set (met consensusgetal 2) is te implementeren, heeft hij consensusgetal 2. De implementaties die we gezien hebben zijn gemakkelijk aan te passen om gebruik te maken van andere objecten, bv. de queue, van consensusgetal 2 of hoger Universaliteit Nu zijn er zeer veel klassen, en het zou ondoenlijk zijn voor elke klasse die een andere implementeert een implementatie expliciet te geven. En telkens als je een nieuwe klasse wilt implementeren moet je weer gaan onderzoeken of de beschikbare klassen deze kunnen implementeren. Gelukkig is het soms mogelijk van een bepaalde klasse te bewijzen dat je er in jouw systeem alles mee kunt implementeren wat je maar wilt. Definitie 12.1 Een klasse A is universeel (voor een zeker systeem) als er voor elke klasse B een implementatie van B uit A bestaat. Heb je bewezen dat een klasse universeel is, dan weet je meteen dat een implementatie van elk denkbaar object in je systeem bestaat. Het bewijs van universaliteit (zie sectie 12.3) is

192 Universaliteit van consensus constructief, dwz., geeft een implementatie. Die implementatie is echter meestal niet de best denkbare. We zullen bewijzen dat n-consensus universeel is in een systeem met n (of minder) processen. Daaruit volgt dat een class met consensusgetal n in die systemen alle objecten kan implementeren Consensus is universeel De centrale rol die consensus speelt in alle beschouwingen over gedistribueerde systemen, wordt gemotiveerd door de volgende stelling; het doel van deze sectie is, die stelling te bewijzen. Theorem 12.2 (Herlihy [Her91]) Veronderstel dat n asynchrone processen 1,..., n communiceren via gedeelde objecten, waarbij objecten van type Consensus aanwezig zijn. Dan kan elk type object deterministisch worden geïmplementeerd. Om te bewijzen dat consensus elke class kan implementeren zullen we eerst een model voor willekeurige classes moeten opstellen. In ons model is S de verzameling toestanden van een object (en in S de initiële toestand), M de verzameling methoden (inclusief parameters) die kan worden aangeroepen, en R de verzameling return-waarden. Het gedrag van het object wordt beschreven door twee functies (new state) ns : S M S en (return waarde) rw : S M R. Wanneer op het object in toestand s een methode m wordt toegepast, levert de methode rw(s, m) op en de toestand van het object verandert in ns(s, m). (Dit model is van toepassing op deterministische classes, waarbij een methode-aanroep altijd maar één mogelijke uitkomst heeft en één mogelijke toestandsovergang. Voor non-deterministische classes moet het gedrag met relaties worden gemodelleerd; het algoritme blijft ongeveer gelijk.) Werking van de universele implementatie De universele constructie in deze sectie zorgt ervoor dat het feitelijke evalueren van de objectfuncties vrij van interleaving is. Er wordt namelijk een wachtvrije synchronisatie verzorgd, en het is daarmee een beetje te vergelijken met de mutual exclusion uit hoofdstuk 2. Wel is extra werk nodig om de wachtvrijheid te garanderen: een thread wordt nooit uitgesloten van het object, maar kan zijn eigen werk uitvoeren en eventueel eerst het werk van een andere thread afmaken. De constructie gebruikt consensusobjecten als een soort write-once register waarbij verschillende processen kunnen proberen dit object te pakken en hun waarde te schrijven, maar slechts het eerste proces slaagt hierin. Naast consensus objecten worden allerlei registers gebruikt (aanroep, gezien, etc.). Het object wordt gerepresenteerd als een gelinkte lijst, waarin elke uitgevoerde methode wordt opgenomen, uiteraard in volgorde van toepassing. Een proces dat een methode wil uitvoeren maakt een cel aan waarin de methode wordt beschreven, en probeert die in de gelinkte lijst op te nemen. Op twee manieren wordt de techniek van onderling helpen toegepast. (1) Als een cel van p i in de lijst is opgenomen, mogen andere processen de waarde van ns en rw invullen; dit voorkomt dat de andere processen op p i moeten wachten met volgende operaties. (2) Als p i een cel heeft aangemaakt, kunnen andere processen p i helpen de cel in de lijst te krijgen; dit voorkomt livelock van het soort waar p i willekeurig vaak moet proberen zijn cel in de lijst te krijgen. Elke cel bevat de volgende informatie (zie programma 12.4):

193 12.3 Consensus is universeel 183 class cel { integer seqnr M meth S st R ret Consensus next cel(m:m) // Constructor krijgt een methode mee { seqnr = 0 ; meth = m ; next = new Consensus } } max(a,b: *cel) { if (a.seqnr > b.seqnr) { return a } else { return b } } Programma 12.4: De class cel voor de universele constructie. Integer seqnr: rangnummer in de lijst. Cellen die aangemaakt zijn maar nog niet in de lijst opgenomen hebben serienummer 0. Het serienummer wordt gebruikt om van twee objecten te bepalen welke het meest recent aan de lijst is toegevoegd (functie max in Kader 12.5: Structuur van cel en implementatie Elke cel (rechts) bevat een serienummer, een methode met returnwaarde en de nieuwe objecttoestand, en een verwijzing naar de volgende cel. Het onderstaande voorbeeld toont een queue (van integers) waarop achtereenvolgens de methoden enq(3), dec en dec zijn uitgevoerd. Proces 1 is bezig met een dec en proces 2 met een enq(5) operatie. sn next meth st ret anker enq(3) deq deq < > <3> < > < > - - gezien aanroep deq enq(5)

194 Universaliteit van consensus cel anker : init [1, -, in, -, new Consensus] cel gezien[n] : init anker // hoogste cel die pi gezien heeft cel aanroep[n]: // methode waar pi mee bezig is methode voor proces i: { mijn = new cel( methode ) aanroep[i] = mijn for (j=1 ; j <= n ; j++) { gezien[i] = max(gezien[i], gezien[j]) } while (aanroep[i].seqnr == 0) { // Bepaal welke cel geprobeerd wordt c = gezien[i] help = aanroep[ (c.seqnr mod n) + 1] if (help.seqnr == 0) { poging = help } else { poging = aanroep[i] } // Probeer invoegen en toepassen d = (c.next).submit(poging) d.st = ns(c.st, d.meth) d.ret = rw(c.st, d.meth) d.seqnr = c.seqnr + 1 gezien[i] = d } return aanroep[i].ret } Programma 12.6: De universele methode. programma 12.4). De aangeroepen methode meth M. De toestand st S ontstaan na toepassen van meth. De waarde ret R die is opgeleverd door meth. Een consensus object next dat naar de volgende cel wijst. Initiëel wordt het object gerepresenteerd door één cel, het anker, waarin de initiële toestand van het object is opgeslagen. Elk proces houdt bij (in gezien[i]) wat de hoogstgenummerde cel is die het gezien heeft, en (in aanroep[i]) met welke methode het eventueel bezig is. Het uitvoeren van methode door p i wordt beschreven in programma Terwille van de leesbaarheid worden gedeelde registers als gewone variabelen aangesproken en niet met hun read en write() methoden. Allereerst maakt p i een nieuwe cel mijn en zet daarin de aan te roepen methode (inclusief argumenten) en de pointer aanroep[i] gaat naar deze cel wijzen. Het plaatsen van deze pointer in aanroep[i] noemen we het aankondigen van de operatie door p i ; we zullen zien dat vanaf deze aankondiging ook de uitvoering van de operatie gegarandeerd is, zelfs als p i er

195 12.3 Consensus is universeel 185 niets meer aan doet. Na de aankondiging gaat p i proberen deze cel aan de gelinkte lijst toe te voegen en dan de nieuwe toestand en returnwaarde te berekenen. Om te voorkomen dat p i de hele lijst moet aflopen (wat willekeurig lang kan duren) bekijkt hij de array gezien en schat zo wat de laatste cel ongeveer is. Bedenk wel, dat wanneer p i bij de hoofdlus aankomt, al weer enige cellen kunnen zijn toegevoegd. In de hoofdlus onderneemt p i herhaald pogingen een cel aan de lijst toe te voegen en de betreffende methode uit te voeren. We zien de vormen van onderling helpen hier verschijnen. Ten eerste is p i niet zo egoïstisch om dit onvoorwaardelijk met zijn eigen cel te gaan proberen. Behalve de cel aanroep[i] komt ook de cel van collega (gezien[i].seqnr mod n) + 1 in aanmerking. Als die een onbehandelde cel (seqnr == 0) heeft, krijgt poging de waarde van die cel. Vervolgens probeert p i de cel poging aan de ketting te rijgen, en wel volgend op de cel die p i het laatst gezien heeft. Natuurlijk is het mogelijk, dat er al een cel is die daarna is gelinkt, en een assignatie c.next = poging zou de juiste werking grondig kunnen verstoren. Daarom is de next pointer een consensusobject: mocht c.next al een waarde hebben, dan wordt hij niet veranderd! Als resultaat van de submit(poging) wordt de pointer d opgeleverd, naar de cel die volgt op cel c. Dit kan hetzij poging zijn, danwel een andere cel die eerder door een ander proces was gesubmit als opvolger van c. Dan zien we de tweede vorm van helpen in actie. Zelfs als een ander proces eerder een cel na c heeft toegevoegd dan p i wil dit nog niet zeggen dat de uitkomst van de operaties is ingevuld. Daarom gaat p i die operatie afmaken door d.seqnr, d.st en d.ret in te vullen. Tenslotte wordt gezien[i] aangepast, immers, d heeft een hoger nummer dan c. Het algoritme eindigt wanneer p i ziet dat zijn eigen operatie is uitgevoerd (seqnr!= 0) Bewijs van de universele implementatie De datastructuur die wordt opgebouwd is een gelinkte lijst van genummerde cellen, elk bevatten ze een methode en de resulterende toestand van het object. Telkens wanneer een cel is toegevoegd, wordt (door minstens één, maar misschien door meerdere processen) de objecttoestand en returnwaarde ingevuld. We moeten bewijzen dat een proces hoogstens een begrensd aantal stappen doet in de universele methode. Eerst laten we zien dat een operatie niet willekeurig lang kan worden uitgesteld na aankondiging ervan. Lemma 12.3 Vanaf het moment dat p i zijn cel aangekondigd heeft kunnen hoogstens n + 1 cellen worden toegevoegd voordat p i s cel erbij zit. Bewijs. Door het toegepaste helpmechanisme heeft p i s operatie voorrang bij het toevoegen van elke k e cel waar k n i. Onder de volgende n+1 toe te voegen cellen zitten opeenvolgende nummers k 1 en k waarvoor dit geldt. Het proces (zeg p j ) dat de k e cel toevoegt bekijkt aanroep[i] nadat de (k 1) e cel is toegevoegd, en kan dus om deze cel niet heen als het sequencenummer nog steeds 0 is. Dit betekent dat p i s cel al is opgenomen voordat p j gaat linken, of p j helpt p i. Komt p i door het lezen van de gezien pointers inderdaad bij het eind van de lijst uit? Zij x het nummer van de laatst gethreade cel op het moment van p i s aankondiging. Omdat p i de pointers leest na de aankondiging, ziet p i minstens nummer x. Nu kan er echter tijdens het bekijken van die pointers een willekeurig hoog aantal cellen toegevoegd zijn: maar als het er meer dan n zijn is p i s operatie al gethread! In dat geval doorloopt p i hoogstens éénmaal de hoofdlus (om de waarden en het sequencenummer in te vullen). Op het moment dat p i aan de

196 Universaliteit van consensus hoofdlus begint geldt dus: gezien[i] zit hoogstens n + 1 plaatsen van het eind van de lijst, of p i s operatie is al toegevoegd. In elke slag van de hoofdlus van p i neemt gezien[i] toe: zo is er een garantie dat de lijst telkens langer wordt, al dan niet door het toedoen van p i zelf! Na ten hoogste n+1 slagen zijn gegarandeerd n + 1 cellen toegevoegd sinds de aankondiging, hetgeen met lemma 12.3 impliceert dat de operatie van p i is toegevoegd, waarna p i de hoofdlus verlaat en aanroep[i].ret oplevert Reflectie De universele implementatie is behoorlijk inefficiënt: zo wordt voor elke operatie bijvoorbeeld een nieuwe kopie van de objecttoestand aangemaakt. De nieuwe toestand wordt door meerdere processen berekend en ingevuld. Daarnaast is er de overhead van lijst bijhouden en sequencenummers. (Op de grootste zorg, het onbegrensd groeiende geheugengebruik, komen we straks terug.) Deze inefficiëntie is een gevolg van de universaliteit van de constructie: de implementatie moet voor alle doelclasses werken en daardoor is het onmogelijk, specifieke overeenkomsten tussen gegeven class en doel class te benutten zoals dat in programma s 12.2 en 12.3 gebeurde. Een universele constructie kan ook geen gebruik maken van eigenschappen van de geïmplementeerde methoden, maar moet ze als een black box zien, zoals gemodelleerd in de functies ns en rw. De constructie gebruikt een nieuwe cel voor elke aanroep op het geïmplementeerde object. Afgezien van de omvang van de cellen (elk bevat een kopie van de objecttoestand) doet dit het geheugengebruik onbegrensd groeien. Dit probleem is op te lossen door de lijst van het beginstuk af weer af te breken en de cellen te recyclen. Er moet dan wel vastgesteld kunnen worden, dat een bepaald beginstuk door geen proces meer gelezen hoeft te worden, en die vaststelling kan behoorlijk achterlopen. Herlihy [Her91] heeft laten zien, dat het voldoende is elk proces een pool van n 2 cellen te geven: dan is het mogelijk een cel te vinden waarvan de recyclebaarheid kan worden aangetoond. Het totale geheugengebruik is dan O(n 3 ) cellen. Voor een non-deterministische class kan de implementatie worden aangepast. Probleem is dat verschillende processen die de waarde d.st (en d.ret) willen invullen, verschillende uitkomsten kunnen krijgen ook al roepen ze de functie ns (en rw) aan met dezelfde parameters. Dit wordt opgelost door ook deze component van een cel als een consensusobject uit te voeren, zodat de waarde wordt vastgelegd door het eerste proces dat de toestandsberekening voltooit en de waarde naar het object stuurt (submit). Omdat de implementatie zo inefficiënt is zul je hem meestal niet gebruiken om grote objecten mee te implementeren, zoals grafen of andere ingewikkelde datastructuren. Voor compacte objecten die cruciale rol in procescoördinatie spelen, zoals fetch-and-increment of semaforen kan de aanpak geschikt zijn. Er zijn diverse andere universele implementaties bedacht om de nadelen van de behandelde op te vangen, maar een behandeling ervan valt niet binnen het doel van dit boek Universaliteit van naamgeving In sectie 12.3 is aangetoond (stelling 12.2), dat consensus voldoende is om elk object te implementeren, en in sectie 11.4, dat randomisering voldoende is om consensus (met geen andere objecten dan registers) te implementeren. De conclusie is, dat wanneer randomisering wordt toegevoegd aan Herlihy s model, implementatie van alle objecttypen mogelijk is.

197 12.4 Universaliteit van naamgeving 187 ZwElect Identity[..] name: { id = 0 while ( id==0 ) { i++ ; if Identity[i] { id = i } } return id } Programma 12.7: Naming object met Zwakke Electie. Buhrman et al. [BPSV06] wezen op het belang van een aanname van Herlihy s model, namelijk de beschikbaarheid van unieke identificatienummers voor de processen. (Omdat deze aanname doorgaans gerechtvaardigd is, en dan ook zeer algemeen wordt gemaakt, is het werk van Buhrman meer van theoretisch dan van praktisch belang.) Volgens de gebruikelijke definitie van implementaties (definitie 10.1) zijn processen genummerd, en mag het procesnummer ook in de code voor het proces worden gebruikt. In de universele implementatie van Herlihy (programma 12.6) gebeurt dit ook, doordat er voor elk proces een eigen entry in de arrays aanroep en gezien is. Dat niet alle implementaties het procesnummer essentieel gebruiken is te zien aan het gerandomiseerde consensus-object (programma 11.6). Die implementatie is anoniem in de zin dat processen hun nummer niet nodig hebben, en er geen variabelen zijn die aan bepaalde processen gekoppeld zijn. Om de vraag te beantwoorden, welke anonieme implementaties mogelijk zijn, speelt het naming (naamgeving) object een cruciale rol. Een naming object voor n processen biedt een methode name, die door elk proces eenmaal mag worden aangeroepen, en geeft elk proces een verschillend getal (bv. uit het bereik 1... n). In feite is een finite-fetch-and-increment of counter als naming object te gebruiken, maar in principe hoeft een naming object de getallen niet eens aansluitend en op volgorde uit te delen. Een object A dat universeel is in anonieme systemen, implementeert daar alle objecten, dus ook het naming object. Omgekeerd, als een object A naming implementeert, maakt het daarmee de toepassing van Herlihy s universele implementatie mogelijk en daarmee implementeert het, in combinatie met consensus of randomisering, elk mogelijk object. Het is daarom gerechtvaardigd om naming zelf als universeel object te beschouwen in anonieme systemen met randomisering. Buhrman et al. bewezen, dat naming niet, zoals consensus, kan worden geïmplementeerd met alleen multiple-reader, multiple-writer registers, zelfs niet als Las Vegas algoritme met randomisering. (Wel is er een Monte Carlo oplossing: laat elk proces een random getal kiezen in een voldoende groot bereik, dan is met bepaalde kans elk getal uniek.) Las Vegas naming kan wel worden geïmplementeerd met single-writer registers. Met registers en randomisering is de zwakke electie te implementeren, en daarmee naming (volgens programma 12.7, zie [BPSV06]). Dat de tweede implementatie deterministisch is, toont aan dat zwakke electie niet zonder randomisering te realiseren is in een anoniem systeem, zelfs niet met behulp van consensus objecten. Daarmee is zwakke electie een object dat in een geïdentificeerde, deterministische setting, zwakker is dan consensus (consensus-getal 2), maar in een randomiserende, anonieme setting sterker is dan consensus (implementeert naming).

198 Universaliteit van consensus Samenvatting en conclusies In dit hoofdstuk werd bekeken in hoeverre een hoog consensusgetal een garantie biedt voor het bestaan van implementaties. De centrale stelling van dit hoofdstuk is dat als een object consensus kan implementeren, dan kan het alle objecten implementeren. Hiertoe werd bewezen dat consensus zelf universeel is, dat wil zeggen, elk object kan implementeren. Het materiaal in dit hoofdstuk illustreert het belang van het consensusprobleem. Ook in systemen met berichtuitwisseling gelden soortgelijke resultaten: zodra consensus kan worden opgelost, liggen andere coördinatieproblemen doorgaans binnen bereik. Tenslotte moeten we in het oog houden wat precies bewezen is: een class met consensusgetal n is universeel, dwz., kan elke class implementeren, in een systeem met n processen. Dit betekent niet, dat dezelfde class ook voor een systeem met meer processen kan worden geïmplementeerd. De compare-and-swap class, met zijn consensusgetal oneindig, heeft hiervan natuurlijk geen last: in elk systeem kan het elke class implementeren, en het is hiermee het krachtigste object dat we hebben gezien. Andere voorbeelden van classes en operaties die consensusgetal hebben zijn: memory-to-memory move of swap (zie opgave 12.8), en de front-queue, maar de laatste zal als hardware primitief niet aangetroffen worden. De consensus-hiërarchie is zinvol voor deterministische implementaties; met randomisering is consensus uit registers te implementeren. Opgaven bij hoofdstuk 12 Opgave 12.1 Waarom kun je met een compare-and-swap geen oneindige fetch-and-increment implementeren op de manier van programma 12.2? Opgave 12.2 Geef een implementatie van k-fetch-and-increment vanuit queue, die slechts één queue gebruikt. Opgave 12.3 Bewijs dat compare-and-swap de oneindige fetch-and-increment kan implementeren. Opgave 12.4 Sterke Electie. Gegeven is een Sterk Electie-object, dat bij elke aanroep de identiteit van de leider retourneert. Dat wil zeggen, het resultaat van elect is een threadnummer, dit nummer is bij alle aanroepen (van alle threads) gelijk, en het is het nummer van een thread die een aanroep heeft gedaan. (a) Is de Sterke Electie te implementeren met de zwakke vorm (die alleen leider of nonleider retourneert? (Motiveer!) (b) Wat is het consensus-getal van het Sterke Electie-object? Opgave 12.5 Maak de verzamelingen S, M en R en de functies ns en rw, zoals gedefiniëerd in sectie 12.3, expliciet voor de test-and-set class. Opgave 12.6 Maak de verzamelingen S, M en R en de functies ns en rw, zoals gedefiniëerd in sectie 12.3, expliciet voor de consensus class. Dezelfde vraag voor de electie class. Opgave 12.7 Geef voorbeelden van deterministische en non-deterministische classes.

199 Opgaven bij hoofdstuk Opgave 12.8 De memory-swap class bestaat uit een array van registers, die individueel atomair gelezen en geschreven kunnen worden. Daarnaast is er een swap methode, die twee indices meekrijgt en de twee betreffende array-waarden (atomair) verwisselt. Bewijs dat memory-swap consensusgetal oneindig heeft.

200 Hoofdstuk 13 Multi-core Systemen Door Alexander Melchior In dit hoofdstuk wordt getracht meer inzicht te verschaffen over multiprocessor en multi-core systemen zodat men weet hoe er het beste kan worden omgegaan met deze nieuwe ontwikkeling. Allereerst zal de hardwarematige verschillen tussen meerdere multiprocessor en multicore implementaties worden vergeleken en wat uitleg worden verschaft. Vervolgens wordt er getracht te laten zien met wat voor soort dingen men rekening moet houden bij het parallel programmeren, door middel van het geven van een klein aantal voorbeelden Een stukje geschiedenis In de voorgaande hoofdstukken is behandeld hoe te redeneren en te denken over hoe meerdere threads tegelijk kunnen werken op een enkel virtueel systeem. Tot zo n 2 à 3 jaar geleden was dit voor de gemiddelde onderzoeker ook alles wat hij of zij kon doen in deze tak van sport. Fysieke multi-cpu systemen waren extreem prijzig en schaars. Alleen universiteiten, multinationals en overheden hadden de beschikking over machines met meerdere processoren 1. Voor onderzoekers is multi-threading al jaren een gebied van onderzoek geweest zonder dat het praktische toepasbaar was voor het grote publiek. Voor het MKB en consumenten waren er wel multiprocessor mogelijkheden, maar deze waren meestal vele malen duurder dan een systeem zonder meerdere processoren. Tevens gaven ze niet de gewenste prestatie verbetering van een systeem. Meestal werden ze aangeschaft voor extra redundancy en niet voor extra prestaties, als een processor zou uitvallen dan kon er verder gewerkt worden op de andere. Voor de consument was het dan ook niet interessant; als men een sneller computer wilde dan kocht men gewoon eentje met meer Megabytes geheugen of een processor met meer Megahertzen. Lang is dit goed gegaan, maar omdat het steeds meer moeite kost om een verdubbeling van de snelheid te bewerkstelligen wordt het op ten duur niet meer rendabel. Als gevolg werden processor fabrikanten genoodzaakt om een andere oplossing te bedenken om vooruitgang te boeken. Een van deze oplossingen, en ook de meeste bekende, is het ontwikkelen van multi-core processoren. 1 Zie de supercomputer ranking van 2005: 190

201 13.2 Hardware 191 Tegenwoordig heeft bijna elke nieuwe processor minstens twee cores met als gevolg dat in bijna ieder huishouden wel een multi-core systeem staat. Wat tot voor kort alleen maar kon worden gesimuleerd, kan met deze nieuwe ontwikkeling in de praktijk daadwerkelijk worden uitgeprobeerd en toegepast. Een bekende vraag die je waarschijnlijk om je heen vaak hebt gehoord is: Ik heb nu 2 cores, maar is mijn pc nu ook twee keer zo snel?. Ondertussen weet je al genoeg om uit te kunnen leggen dat er meer bij komt kijken dan simpelweg een tweede core toevoegen. Voor de gemiddelde consument is het eigenlijk meer een raadsel met veel reclame, termen en andere praat. Een goede manier van uitleggen is een vergelijking trekken met het beter bekende kip of het ei verhaal. Immers, tot voor kort is er bijna geen vraag geweest naar programmatuur welke goed kon omgaan met meerdere processoren. Bedrijven en programmeurs hebben parallel programmeren links laten liggen met het gevolg dat er de afgelopen 30 jaar voornamelijke single-threaded software is ontwikkeld Hardware Voordat we verder gaan moeten we ons domein inperken. Een computer is een alles behalve duidelijke omschrijving van waar we mee werken. We zullen ons dus ook beperken tot de standaard huis-tuin-en-keuken computers, tenzij anders aangegeven. Voor de intimi: er wordt uit gegaan van een standaard IBM compatibel/ix86 architectuur UniProcessor De systemen waar iedereen mee bekend is zijn gebaseerd op uniprocessoren, een enkele processor. Hiervoor kan men prima een snel programmeren schrijven zonder echt te begrijpen hoe het systeem in elkaar zit. Met meer kennis van algoritmen en datastructuren kan je nog betere en sneller programma s schrijven, maar alsnog blijf je werken op een redelijk abstract niveau. Echte kennis over de onderliggende architectuur is niet nodig. Denk bijvoorbeeld over een for loop die itererend over een stuk code loopt. Je geeft op waar de loop moet beginnen, wat er elke iteratie moet veranderen en wanneer hij moet stoppen. Over de volgorde nadenken heeft in het algemeen geen zin, je begint vooraan en eindigt achteraan, waarom moeilijk doen als het makkelijk kan? Met meerdere processoren kan je dit ook doen, maar dan hou je maar een enkele processor aan het werk. Het is nu de vraag hoe je er voor kan zorgen dat je al deze losse iteraties parallel kan verwerken. Hier is geen eenduidige antwoord op, tijdens het parallel programmeren moet je als het ware rekening houden met een extra dimensie Multiprocessor Multiprocessor en multi-core is een erg breed en veelomvattende term, eigenlijk alles met meer dan een rekenkern krijgt al het predicaat multi-core of multiprocessor opgeplakt. De meeste mensen associëren het al meteen met multi-core, wat voornamelijk komt door dat Intel zijn nieuwste generaties processoren met deze benaming promoot. In multiprocessorland zijn er meerdere stromingen voor het ontwerpen van Multiprocessor systemen. De meest bekende en grootste hiervan zijn SMP (Symmetric MultiProcessing) en NUMA (Non-Uniform Memory Architecture), minder gebruikte architecturen zijn ASMP (ASymmetric MultiProcessing) en Clustered Multiprocessing.

202 192 Multi-core Systemen Figuur 13.1: SMP Symmetric MultiProcessing. Vooral SMP (Symmetric MultiProcessing) is op dit moment (2009) een veel voorkomende architectuur. Dit komt vooral omdat het al sinds de Intel (1986) toegepast wordt voor het maken van server systemen. De afgelopen 20 jaar heeft dit er voor gezorgd dat SMP vooral geïntegreerd is geraakt binnen de wereld van de servers en tot op zeker hoogte binnen de consumentenmarkt. Toen het snelheidsplafond eenmaal bereikt was en er naar alternatieven gezocht moest worden om de vraag naar snellere processoren te stillen is de keuze dan ook snel gevallen op SMP. Het is in de jaren een proven concept geworden en er werd voorzien dat met de juiste marketing het grote publiek ook enthousiast kon worden gemaakt. De rest is geschiedenis. Opbouw van het SMP systeem. SMP is het beste te vergelijken met de ouderwetse uniprocessor opzet van x86 systemen. De processor, geheugencontroler (welke direct verbonden is met het geheugen), I/O, insteekkaarten (ISA, PCI, AGP, PCI-E, etc etc) en andere randapparatuur zijn allemaal direct dan wel indirect aangesloten op de zelfde bus. In ons geval is dit een FSB, Front Side Bus. Over deze bus gaat alle communicatie tussen de aangesloten onderdelen. Als de processor een stukje geheugen wil hebben dan vraagt de processor aan de geheugencontroler, welke ook aan de bus zit, of dit naar hem toe kan worden gestuurd. Na dit verzoek zal de geheugencontroler de verzochte data naar de processor worden gestuurd zodat deze weer vrolijk verder kan rekenen. De bus heeft alleen wel een paar nare eigenschappen. Slechts één process kan tegelijk gebruik maken van de bus; als er een ander process bezig is zit er niets anders op dan te wachten tot deze klaar is. Ook is de FSB ondertussen verouderd en relatief langzaam geworden. Een processor kan een berekening in een klein aantal cykels (een klok tick of hertz) uitvoeren, maar het duurt met moderne systemen honderden cykels om iets uit het geheugen te halen. Om nog maar te zwijgen over disk I/O of nog langzamere I/O. Het grootste deel van de tijd staat je processor duimen te draaien tot er weer een beetje data binnenkomt om te verwerken. Een van de oplossingen voor dit probleem is het implementeren van een cache. Dit is kleine hoeveelheid heel snel geheugen waar de processor binnen een paar cykels bij kan. Omdat dit

203 13.2 Hardware 193 geheugen vele malen kleiner is dan het normale geheugen moet er goed worden nagedacht over wat er in de cache wordt gezet. Als de benodigde data niet in de cache van de processor staat moet het alsnog uit het normale geheugen worden gehaald (zie kader 13.2). Door de implementatie van cache is er ook meteen een probleem geïntroduceerd voor multiprocessor systemen. Waar staat de meest up-to-date versie van je data? Bij een uniprocessor is dit natuurlijk triviaal, als het niet in je eigen cache staat dan staat het in het normale geheugen. Maar bij multi-processoren is dit wat lastiger; staat dit in het normale geheugen, je eigen cache of in een cache van een andere processor? Er zijn meerdere protocollen om dit in goede banen te leiden, welke allemaal onder de noemer cache coherency vallen. Een van de meest gebruikte protocollen voor het in goede banen leiden van het dataverkeer voor caches is MESI. MESI betekent precies waar het voor staat: Modified, Exclusive, Shared en Invalid. Deze vier statussen geven aan wat de status van een bepaald stuk data is. Modified De data in de cache van de processor is veranderd en moet worden weggeschreven naar het normale geheugen voordat een andere processor het kan lezen. Exclusive Alleen deze processor heeft dit stuk data in de zijn cache, maar deze is nog gelijk aan de versie in het normale geheugen. Een andere processor kan dus gewoon de data uit het normale geheugen lezen. Shared De data staat in de cache van deze processor, maar een andere processor heeft de data ook in zijn cache staan. Beide copiën van de data zijn nog gelijk aan de data in het normale geheugen. Invalid De data in de cache is niet meer geldig, er is op een andere locatie een nieuwere versie aanwezig. In de cache wordt bijgehouden wat de status van een bepaald stuk data is waardoor de processor weet waar hij aan toe is. Een belangrijke observatie is dat cache snel is en dat er voor een efficiënt gebruik moet worden geprobeerd te voorspellen welk geheugen gebruikt gaat worden. Non-Uniform Memory Architecture/Access. In de 90 er jaren werden de problemen met de schaalbaarheid van SMP steeds groter door de beperkte bandbreedte van de FSB. Met NUMA (Non-Uniform Memory Architecture/Access) wordt geprobeerd om een architectuur te maken welke dit nadeel niet heeft en wel goed schaalbaar is. Een NUMA architectuur bestaat uit allemaal nodes welke onderling met elkaar verbonden zijn. Iedere node bestaat uit een processor en een lokaal geheugen, ook is een node verbonden met andere nodes. Alle lokale geheugens samen vormen het gehele geheugen van het systeem. Het systeem is dus eigenlijk een samenhangende graaf waarin elke node elke andere node kan bereiken, al dan niet via andere nodes. Als een node met een stuk data wil gaan werken welke niet in zijn lokale geheugen staat moet deze worden gehaald bij een andere node in de graaf. Hiermee komt ook meteen de non-uniform eigenschap naar voren. De node kan de data uit zijn eigen lokale geheugen veel sneller benaderen dan het data welke hier niet in staat. Een ander voordeel is dat nodes geheel onafhankelijk van elkaar kunnen werken. Een node niet liggende op het pad tussen twee nodes welke onderling data aan het uitwisselen zijn, zal niets direct merken van dit verkeer.

204 194 Multi-core Systemen Kader 13.2: Cachelines Een van de truken om te zorgen dat veel van de benodigde data in de cache zit is om bij een request aan het normale geheugen ook de omliggende stuk geheugen mee te sturen. Over het algemeen gebruikt een programma een vast stuk geheugen en zit het netjes achter elkaar. Hierdoor heb je al het stuk data welke later nodig is al van te voren in de cache gezet en hoeft de processor niet lang te wachten op de data. De vraag is alleen hoeveel van de omliggende data sla je in de cache opslaat? De hoeveelheid data wordt aangeduid met de grote van een cacheline, een lijn cache welke in een keer vol wordt geschreven met de benodigde data en de rest van de zekfde cachline. Maar hoe groot maak je een cacheline? Je cache heeft maar een maximum grootte en kan snel vol raken. Als je je cacheline even groot maakt als de cache zelf (de maximale grote) dan heb je veel van het stuk programma waar je bezig bent, maar je krijgt een cachemiss zodra je weer in een ander stuk van het programma verder gaat. Ook moet alle extra data voor de cache over de bus voordat het in de cache staat. Als je een grote cacheline gebruikt dan wordt er relatief minder gebruik gemaakt van de gecachde data; het is minder rendabel geweest om de data te cachen. Met een kleine cacheline heb je juist kans dat je vaak een cachemiss hebt terwijl je in een lokaal stuk programma bezig bent. Hier zien we dus een duidelijke tradeoff. Kleine cachelines zorgen voor meerdere stukjes programma in je cache en kleinere overdrachtstijden per cacheline maar je moet vaak de cache vullen. Grote cachelines zorgen voor minder cachemisses in een bepaald stuk programma maar zorgt voor grotere overdrachtstijden per cacheline en je hebt kans dat je vaker je cache moet vervangen Multi-core Nu alle basisstukken zijn besproken kunnen we ons gaan richten op een van de moderne toepassingen: Multi-core processoren. Dit zijn eigenlijk kleine multiprocessor configuraties maar dan op een enkele chip. Deze processoren hebben in de korte tijd dat ze bestaan al een flinke vlucht in ontwikkeling genomen. Iedere processorfabrikant wil het zelfde bereiken maar Figuur 13.3: NUMA

205 13.2 Hardware 195 Figuur 13.4: Links een Core Duo en rechts een Core 2 Duo. gaat hierin zijn eigen weg. We zullen hier vooral kijken welke ontwikkelingen de mainstream multi-core processoren zijn doorgegaan, vooral doelende op Intel en AMD processoren. SMP/Intel. De eerste multi-core processoren bestaan uit 2 cores welke gebruik maken van SMP. Iedere core staat geheel op zichzelf, als ze onderling willen communiceren dan moet dit ook via de FSB. Ook staan ze op elkaar te wachten als ze beide data uit het geheugen willen halen. Een voorbeeld hiervan is de Pentium D, dit zijn twee Pentium 4 s welke aan elkaar zijn geplakt om zo samen op een chip te passen. Het is natuurlijk een beetje onzin als de cores fysiek naast elkaar liggen om dan onderling te moeten communiceren via de FSB. Hier zijn meerdere oplossingen voor bedacht: een aparte bus tussen de cores en L2 caches aanleggen (de Core Duo) of een gedeelde L2 cache maken (de Core 2 Duo) (zie Figuur 13.4). Maar ook deze architecturen bleven een probleem hebben, de snelheid van de FSB, of beter gezegd: het ontbreken van de snelheid. NUMA/HT. Aan de andere kant van het spectrum staat de NUMA architectuur. Nog voor de introductie van multi-cores is AMD bezig geweest een NUMA architectuur te ontwikkelen voor zijn single-core processoren met het oog op de toekomst. Men heeft de geheugencontroler op de chip van de processor geplaatst waardoor de data van en naar het geheugen niet langer Kader 13.5: Cache levels Cache is geı ntroduceerd omdat het normale geheugen steeds langzamer werd ten opzichte van de processor. Tijdens de introductie van de eerste caches (ten tijde van de 80386) was dit een aparte upgrade voor het systeem; je kon een stukje cache bijprikken op het moederbord. Met de komst van de volgende generatie werd een klein deel van de cache geı ntegreerd in de processor (on-die) en was de rest van de cache nog te vinden op het moederbord. Om het verschil tussen deze twee cache aan te geven werdt de cache in de processor als level 1 (L1) aangeduid en de cache op het moederbord als level 2 (L2). Tegenwoordig hebben de meeste processoren een L1, L2 en soms ook een L3 cache. Een mooie illustratie van de verschillende soorten kan je zien in figuur Als vuistregel kan je er van uit gaan de een Ln cache kleiner en sneller is dan een Ln + 1 cache.

206 196 Multi-core Systemen Figuur 13.6: AMD Opteron/Athlon X2. maar zonder CPU 1. De single-core variant is het zelfde over de FSB hoeft te lopen. Ook zijn er snelle interconnects toegevoegd (bij AMD bekend onder de naam HyperTransport) welke al het verkeer tussen processor en andere onderdelen regelen. Doordat de interconnects snelle point to point verbindingen zijn, kan de processor data uit het geheugen halen terwijl hij tegelijk ook data naar de videokaart stuurt. Door deze ontwerpkeuze hebben systemen met twee single-core processoren wel ieder een eigen set geheugen nodig. Onderling zijn de processoren verbonden met een interconnect en niet met een bus. Maar de echte verbetering zit hem in de interne bus welke het verkeer tussen de rekenunits, geheugencontroler en interconnects regelt. Hierdoor is het design van de gehele processor modulairder geworden en kan men een oudere core vervangen voor een nieuwe zonder de gehele chip te moeten aanpassen. De gulden middenweg. Beide fabrikanten hebben natuurlijk goed naar elkaar gekeken en een beetje leentjebuur gespeeld. AMD heeft in hun nieuwste processor een gedeelde L3 cache geïmplementeerd en Intel heeft zijn eigen interconnect geïntroduceerd: QuickPath. Je kan de nieuwe opzet het beste zien als een mix van SMP en NUMA. De NUMA architectuur komt naar voren in de snelle onderlinge interconnects van de cores. Verder hebben alle cores een snelle verbinding naar de L3 cache en de geheugencontroler op de processor, wat

207 13.3 Software 197 Figuur 13.7: Core i7, vervang de Intel namen met de AMD equivalenten en je hebt een Phenom X4 (kort door de bocht!) weer duidt op een SMP architectuur. Door op deze manier best of both worlds te krijgen zien we verbeterde prestaties. Dit komt niet alleen doordat er meer cores of snellere cores worden gebruikt, de (veel) snellere interconnects en on-die (op de chip zelf) geheugencontroler zorgen er voor dat de cores minder lang hoeven te wachten op hun data Software Nu we de harde waren hebben behandeld en weten wat de sterke en zwakke plekken zijn van multi-core processoren is het tijd te kijken hoe we met deze eigenschappen moeten omgaan. Dit is meteen het moeilijkste van parallelprocessing, er is geen standaard plan van aanpak om het ideale parallel programma te schrijven. Iedere situatie heeft een eigen aanpak nodig om goed parallel te kunnen worden geprogrammeerd Tegenwerking In de theorie ziet alles er mooi uit, maar in de echte wereld van compilers en processoren gaan dingen niet altijd zoals je wenst. Schrijf maar eens een programma welke met twee threads een enkele teller ophoogt, gebruikmakende van de Peterson lock 2 (algoritme 13.8), ieder keer. Je verwacht dat de eindwaarde van deze teller zal zijn, maar de kans is groot dat de waarde lager zal uitvallen. Compiler optimalisatie. Het is bewijsbaar dat de Peterson lock voldoet aan mutual exclusion, toch zit het probleem hem in de lock. 2 Zie s algorithm.

Gedistribueerd programmeren

Gedistribueerd programmeren Gedistribueerd programmeren Gedistribueerd programmeren Collegedictaat, september 2007 Gerard Tel Email: gerard@cs.uu.nl Instituut voor Informatica en Informatiekunde Universiteit Utrecht Opmaak: Gerard

Nadere informatie

Gelijktijdigheid: Wederzijdse Uitsluiting & Synchronisatie Concurrency: Mutual Exclusion & Synchonization (5e ed: 5.1-5.2, Appendix A.

Gelijktijdigheid: Wederzijdse Uitsluiting & Synchronisatie Concurrency: Mutual Exclusion & Synchonization (5e ed: 5.1-5.2, Appendix A. Gelijktijdigheid: Wederzijdse Uitsluiting & Synchronisatie Concurrency: Mutual Exclusion & Synchonization (5e ed: 51-52, Appendix A1) Processes zijn meestal niet onafhankelijk Bijvoorbeeld: 2 processen

Nadere informatie

Gedistribueerd Programmeren - Samenvatting

Gedistribueerd Programmeren - Samenvatting Gedistribueerd Programmeren - Samenvatting Geertjan van Vliet Disclaimer: Aan deze teksten kunnen geen rechten ontleend worden. Bepaalde passages zijn de visie van de auteur en niet die van de docent.

Nadere informatie

Opgaven Registers Concurrency, 29 nov 2018, Werkgroep.

Opgaven Registers Concurrency, 29 nov 2018, Werkgroep. Opgaven Registers Concurrency, 29 nov 2018, Werkgroep. Gebruik deze opgaven om de stof te oefenen op het werkcollege. Cijfer: Op een toets krijg je meestal zes tot acht opgaven. 1. Safe Integer: Van een

Nadere informatie

Centrale begrippen hoofdstuk 3. Waarom multiprogramming? Vandaag. processen proces state: running, ready, blocked,... Vragen??

Centrale begrippen hoofdstuk 3. Waarom multiprogramming? Vandaag. processen proces state: running, ready, blocked,... Vragen?? Vragen?? Vandaag Hoofdstuk 4: threads (tentamenstof : 4.1 t/m 4.2) Kleine Opgaven 4.1 (niet alleen ja of nee, ook waarom!) en 4.4 inleveren maandag Centrale begrippen hoofdstuk 3 processen proces state:

Nadere informatie

Take-home Tentamen Protocolvericatie. Universiteit van Amsterdam. 27 Maart 1994

Take-home Tentamen Protocolvericatie. Universiteit van Amsterdam. 27 Maart 1994 Take-home Tentamen Protocolvericatie Vakgroep Programmatuur Universiteit van Amsterdam 27 Maart 1994 Algemene Opmerkingen Dit tentamen omvat zes reguliere opgaven plus een bonusopgave. Opgaves 1, 2, 5

Nadere informatie

Hoofdstuk 3: Processen: Beschrijving en Besturing. Wat is een proces? Waarom processen? Wat moet het OS ervoor doen? Is het OS zelf een proces?

Hoofdstuk 3: Processen: Beschrijving en Besturing. Wat is een proces? Waarom processen? Wat moet het OS ervoor doen? Is het OS zelf een proces? Hoofdstuk 3: Processen: Beschrijving en Besturing Wat is een proces? Waarom processen? Wat moet het OS ervoor doen? Is het OS zelf een proces? 1 Wat is een proces? Een proces is een programma in uitvoering

Nadere informatie

Werkcollegebundel Deel 1 Concurrency 8 oktober 2018, Gerard Tel, Niet verspreiden 1!.

Werkcollegebundel Deel 1 Concurrency 8 oktober 2018, Gerard Tel, Niet verspreiden 1!. Werkcollegebundel Deel 1 Concurrency 8 oktober 2018, Gerard Tel, Niet verspreiden 1!. Deze bundel bevat een collectie toetsvragen over het eerste deel van Concurrency. Behalve dat goede antwoorden worden

Nadere informatie

Computerarchitectuur en netwerken Toets 1 4 okt

Computerarchitectuur en netwerken Toets 1 4 okt 11.00 13.00 De open vragen moet je beantwoorden op tentamenpapier. De multiple-choice antwoorden moet je op het vragenblad invullen in de rechtervakjes en dat blad inleveren. Schrijf je naam, studentnummer

Nadere informatie

Semaforen. Semaforen p. 1/2

Semaforen. Semaforen p. 1/2 Semaforen 2008 Semaforen p. 1/2 Vorige Keer mbv mutual exclusion kritieke sectie (cs) 1. software : Dekker s, Peterson s algoritme 2. hardware: uniprocessor machine: disable interrupts 3. hardware: multiprocessor

Nadere informatie

Computerarchitectuur. H&P Ch 5. Thread-Level Parallelism

Computerarchitectuur. H&P Ch 5. Thread-Level Parallelism Computerarchitectuur H&P Ch 5. Thread-Level Parallelism Kristian Rietveld http://ca.liacs.nl/ Thread-Level Parallelism In het geval van thread-level parallelism (TLP) gaan we uit van meerdere threads.

Nadere informatie

Uitgebreide uitwerking Tentamen Complexiteit, juni 2016

Uitgebreide uitwerking Tentamen Complexiteit, juni 2016 Uitgebreide uitwerking Tentamen Complexiteit, juni 016 Opgave 1. (3+10++7+6) a. De hoogte van de beslissingsboom (lengte van het langste pad) stelt het aantal arrayvergelijkingen in de worst case voor.

Nadere informatie

Eerste Toets Concurrency 20 december 2018, , Educ-β.

Eerste Toets Concurrency 20 december 2018, , Educ-β. Eerste Toets Concurrency 20 december 2018, 11.00 13.00, Educ-β. Motiveer je antwoorden kort! Stel geen vragen over deze toets; als je een vraag niet duidelijk vindt, schrijf dan op hoe je de vraag interpreteert

Nadere informatie

Opgaven Snapshots en Consensus Hierarchie Concurrency, 6 en 13 december 2018, Werkgroep.

Opgaven Snapshots en Consensus Hierarchie Concurrency, 6 en 13 december 2018, Werkgroep. Opgaven Snapshots en Consensus Hierarchie Concurrency, 6 en 13 december 2018, Werkgroep. Gebruik deze opgaven om de stof te oefenen op het werkcollege. Cijfer: Op een toets krijg je meestal zes tot acht

Nadere informatie

Multi-core systemen. door Alexander Melchior

Multi-core systemen. door Alexander Melchior Multi-core systemen Multi-cpu & Multi-core Multi cpu & Multi core door Alexander Melchior Toevoeging aan GDP Overdragen Capita Selecta Waarom? Een stukje geschiedenis 2005: Introductie eerste consumenten

Nadere informatie

1 Rekenen in eindige precisie

1 Rekenen in eindige precisie Rekenen in eindige precisie Een computer rekent per definitie met een eindige deelverzameling van getallen. In dit hoofdstuk bekijken we hoe dit binnen een computer is ingericht, en wat daarvan de gevolgen

Nadere informatie

1=2720/2725 Operating System Concepten

1=2720/2725 Operating System Concepten TECHNISCHE UNIVERSITEIT DELFT Faculteit EWI, Afdeling SCT 1 1 1=2720/2725 Operating System Concepten ^ november 2013, 14.00-17.00 uur. docent: H.J. Sips Dit is een tentamen met 9 open vragen Opgave Punten

Nadere informatie

TI-2720 Operating System Concepten. 6 november 2012, uur. docent: H.J. Sips. Dit is een tentamen met 9 open vragen

TI-2720 Operating System Concepten. 6 november 2012, uur. docent: H.J. Sips. Dit is een tentamen met 9 open vragen TECHNISCHE UNIVERSITEIT DELFT Faculteit Elektrotechniek, Wiskunde en Informatica Sectie Parallelle en Gedistribueerde Systemen TUDelft TI-2720 Operating System Concepten 6 november 2012, 14.00-17.00 uur.

Nadere informatie

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

HOOFDSTUK 3. Imperatief programmeren. 3.1 Stapsgewijs programmeren. 3.2 If Then Else. Module 4 Programmeren HOOFDSTUK 3 3.1 Stapsgewijs programmeren De programmeertalen die tot nu toe genoemd zijn, zijn imperatieve of procedurele programmeertalen. is het stapsgewijs in code omschrijven wat een programma moet

Nadere informatie

BEGINNER JAVA Inhoudsopgave

BEGINNER JAVA Inhoudsopgave Inhoudsopgave 6 Configuratie Hallo wereld! Praten met de gebruiker Munt opgooien Voorwaarden Lussen......6 Configuratie Met deze Sushi kaarten ga je een simpel spel maken met één van de meest populaire

Nadere informatie

Tim Mallezie Architectuur van besturingssystemen: Vraag A2.

Tim Mallezie Architectuur van besturingssystemen: Vraag A2. Procesbeheer: kenmerken van moderne besturingssystemen. 1. Bespreek de (drie) meest typische kenmerken van moderne besturingssystemen. 2. In hoeverre beantwoorden UNIX, Linux en Windows NT hieraan? Geef

Nadere informatie

Automaten. Informatica, UvA. Yde Venema

Automaten. Informatica, UvA. Yde Venema Automaten Informatica, UvA Yde Venema i Inhoud Inleiding 1 1 Formele talen en reguliere expressies 2 1.1 Formele talen.................................... 2 1.2 Reguliere expressies................................

Nadere informatie

TECHNISCHE UNIVERSITEIT EINDHOVEN ComputerSystemen Deeltentamen B (weken 6..9) vakcode 2M208 woensdag 19 Maart 2003, 9:00-10:30

TECHNISCHE UNIVERSITEIT EINDHOVEN ComputerSystemen Deeltentamen B (weken 6..9) vakcode 2M208 woensdag 19 Maart 2003, 9:00-10:30 TECHNISCHE UNIVERSITEIT EINDHOVEN ComputerSystemen Deeltentamen B (weken 6..9) vakcode 2M208 woensdag 19 Maart 2003, 9:00-10:30 Algemene opmerkingen (lees dit!): - Dit tentamen duurt ANDERHALF UUR! - Dit

Nadere informatie

Uitwerking tentamen Analyse van Algoritmen, 29 januari

Uitwerking tentamen Analyse van Algoritmen, 29 januari Uitwerking tentamen Analyse van Algoritmen, 29 januari 2007. (a) De buitenste for-lus kent N = 5 iteraties. Na iedere iteratie ziet de rij getallen er als volgt uit: i rij na i e iteratie 2 5 4 6 2 2 4

Nadere informatie

VERZAMELINGEN EN AFBEELDINGEN

VERZAMELINGEN EN AFBEELDINGEN I VERZAMELINGEN EN AFBEELDINGEN Het begrip verzameling kennen we uit het dagelijks leven: een bibliotheek bevat een verzameling van boeken, een museum een verzameling van kunstvoorwerpen. We kennen verzamelingen

Nadere informatie

Gödels theorem An Incomplete Guide to Its Use and Abuse, Hoofdstuk 3

Gödels theorem An Incomplete Guide to Its Use and Abuse, Hoofdstuk 3 Gödels theorem An Incomplete Guide to Its Use and Abuse, Hoofdstuk 3 Koen Rutten, Aris van Dijk 30 mei 2007 Inhoudsopgave 1 Verzamelingen 2 1.1 Definitie................................ 2 1.2 Eigenschappen............................

Nadere informatie

Programmeren in Java 3

Programmeren in Java 3 7 maart 2010 Deze les Zelf componenten maken Concurrency (multithreading): werken met threads levenscyclus van een thread starten tijdelijk onderbreken wachten stoppen Zelf componenten maken Je eigen component:

Nadere informatie

Waarmaken van Leibniz s droom

Waarmaken van Leibniz s droom Waarmaken van Leibniz s droom Artificiële intelligentie Communicatie & internet Operating system Economie Computatietheorie & Software Efficiënt productieproces Hardware architectuur Electronica: relais

Nadere informatie

Examen Datastructuren en Algoritmen II

Examen Datastructuren en Algoritmen II Tweede bachelor Informatica Academiejaar 2012 2013, tweede zittijd Examen Datastructuren en Algoritmen II Naam :.............................................................................. Lees de hele

Nadere informatie

Uitgebreide uitwerking Tentamen Complexiteit, juni 2017

Uitgebreide uitwerking Tentamen Complexiteit, juni 2017 Uitgebreide uitwerking Tentamen Complexiteit, juni 017 Opgave 1. a. Een pad van de wortel naar een blad stelt de serie achtereenvolgende arrayvergelijkingen voor die het algoritme doet op zekere invoer.

Nadere informatie

Tiende college algoritmiek. 14 april Gretige algoritmen

Tiende college algoritmiek. 14 april Gretige algoritmen College 10 Tiende college algoritmiek 1 april 011 Gretige algoritmen 1 Greedy algorithms Greed = hebzucht Voor oplossen van optimalisatieproblemen Oplossing wordt stap voor stap opgebouwd In elke stap

Nadere informatie

Combinatorische Algoritmen: Binary Decision Diagrams, Deel III

Combinatorische Algoritmen: Binary Decision Diagrams, Deel III Combinatorische Algoritmen: Binary Decision Diagrams, Deel III Sjoerd van Egmond LIACS, Leiden University, The Netherlands svegmond@liacs.nl 2 juni 2010 Samenvatting Deze notitie beschrijft een nederlandse

Nadere informatie

Examen Geavanceerde Computerarchitectuur

Examen Geavanceerde Computerarchitectuur Examen Geavanceerde Computerarchitectuur Academiejaar 2006-2007 Dinsdag 16 januari 2007, 14u00 Prof. dr. ir. L. Eeckhout Richting: Enkele opmerkingen vooraf: Vul eerst en vooral op ieder blad Uw naam en

Nadere informatie

Tiende college algoritmiek. 13/21 april Gretige Algoritmen Algoritme van Dijkstra

Tiende college algoritmiek. 13/21 april Gretige Algoritmen Algoritme van Dijkstra Algoritmiek 017/Gretige Algoritmen Tiende college algoritmiek 13/1 april 017 Gretige Algoritmen Algoritme van Dijkstra 1 Algoritmiek 017/Gretige Algoritmen Muntenprobleem Gegeven onbeperkt veel munten

Nadere informatie

TI-2720 Operating System Concepten. 21 januari 2013, uur. docent: H.J. Sips. Dit is een tentamen met 9 open vragen

TI-2720 Operating System Concepten. 21 januari 2013, uur. docent: H.J. Sips. Dit is een tentamen met 9 open vragen TECHNISCHE UNIVERSITEIT DELFT Faculteit EWI, Afdeling SCT Sectie Parallelle en Gedistribueerde Systemen Ty Delft TI-2720 Operating System Concepten 21 januari 2013, 14.00-17.00 uur. docent: H.J. Sips Dit

Nadere informatie

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

Als een PSD selecties bevat, deelt de lijn van het programma zich op met de verschillende antwoorden op het vraagstuk. HOOFDSTUK 3 3.1 Stapsgewijs programmeren In de vorige hoofdstukken zijn programmeertalen beschreven die imperatief zijn. is het stapsgewijs in code omschrijven wat een programma moet doen, net als een

Nadere informatie

ling van die eigenschap binnen het model geldt. In het bijzonder bij het wiskundig modelleren van een programma kan een eigenschap met wiskundige zeke

ling van die eigenschap binnen het model geldt. In het bijzonder bij het wiskundig modelleren van een programma kan een eigenschap met wiskundige zeke De Nederlandse samenvatting van een proefschrift is bij uitstek het onderdeel van het proefschrift dat door familie en vrienden wordt gelezen. Voor hen wil ik deze samenvatting dan ook schrijven als een

Nadere informatie

Logica voor Informatica

Logica voor Informatica Logica voor Informatica 13 Programma verificatie Wouter Swierstra University of Utrecht 1 Programmeertalen en logica Bij logische programmeertalen hebben we gezien dat we rechstreeks met (een fragment

Nadere informatie

Een interrupt is een hardwaremechanisme dat een extern apparaat in staat stelt om de CPU een signaal te sturen wanneer de I/O operatie voltooid is.

Een interrupt is een hardwaremechanisme dat een extern apparaat in staat stelt om de CPU een signaal te sturen wanneer de I/O operatie voltooid is. Procesbeheer: interrupts en uitvoering van besturingssystemen. a) Beschrijf de werking en de bedoeling van interrupts: Hoe deze door de processorhardware en het besturingssysteem worden verwerkt? Waarom

Nadere informatie

Examen Datastructuren en Algoritmen II

Examen Datastructuren en Algoritmen II Tweede bachelor Informatica Academiejaar 2014 2015, eerste zittijd Examen Datastructuren en Algoritmen II Naam :.............................................................................. Lees de hele

Nadere informatie

ICT Infrastructuren: Processen en Threads. 18 november 2013 David N. Jansen

ICT Infrastructuren: Processen en Threads. 18 november 2013 David N. Jansen ICT Infrastructuren: Processen en Threads 18 november 2013 David N. Jansen Datum en Ajd van werkcollege na overleg met de aanwezigen: donderdag 8:45 10:30 Leerdoel voor vandaag. Stallings hoofdst 2 4 Hoofddoelen

Nadere informatie

Tentamen Computersystemen

Tentamen Computersystemen Tentamen Computersystemen baicosy6 2e jaar bachelor AI, 2e semester 21 oktober 213, 9u-11u OMHP D.9 vraag 1 Van een Single Cycle Harvard machine hebben de componenten de volgende propagation delay time:

Nadere informatie

Modelleren en Programmeren

Modelleren en Programmeren Modelleren en Programmeren Jeroen Bransen 11 december 2015 Ingebouwde datastructuren Meer boomstructuren Access specifiers Gebruikersinvoer Codestijl Packages SAT-solver Ingebouwde datastructuren Ingebouwde

Nadere informatie

Voorkennis: C, basiskennis microprocessoren (bij voorkeur ARM7 processor)

Voorkennis: C, basiskennis microprocessoren (bij voorkeur ARM7 processor) Real Time Operating Systems (RTOS) Voorkennis: C, basiskennis microprocessoren (bij voorkeur ARM7 processor) Kennis nodig voor: Operating systems Niveau: inleidend Diplomavoorwaarde: bachelor, schakelprogramma

Nadere informatie

Proeftentamen in1211 Computersystemen I (NB de onderstreepte opgaven zijn geschikt voor de tussentoets)

Proeftentamen in1211 Computersystemen I (NB de onderstreepte opgaven zijn geschikt voor de tussentoets) TECHNISCHE UNIVERSITEIT DELFT Faculteit Informatietechnologie en Systemen Afdeling ISA Basiseenheid PGS Proeftentamen in1211 Computersystemen I (NB de onderstreepte opgaven zijn geschikt voor de tussentoets)

Nadere informatie

n-queens minimale dominantie verzamelingen Chessboard Domination on Programmable Graphics Hardware door Nathan Cournik

n-queens minimale dominantie verzamelingen Chessboard Domination on Programmable Graphics Hardware door Nathan Cournik n-queens minimale dominantie verzamelingen Chessboard Domination on Programmable Graphics Hardware door Nathan Cournik Rick van der Zwet 4 augustus 2010 Samenvatting Dit schrijven zal

Nadere informatie

Een eenvoudig algoritme om permutaties te genereren

Een eenvoudig algoritme om permutaties te genereren Een eenvoudig algoritme om permutaties te genereren Daniel von Asmuth Inleiding Er zijn in de vakliteratuur verschillende manieren beschreven om alle permutaties van een verzameling te generen. De methoden

Nadere informatie

Belangrijkste ideeën/concepten uit OS, incl. proces

Belangrijkste ideeën/concepten uit OS, incl. proces Operating System Overview (Hfst 2) Wat is een OS? Wat was een OS? Evolutie van OS. OS als virtuele machine OS als beheerder van hulpbronnen (resources) Belangrijkste ideeën/concepten uit OS, incl. proces

Nadere informatie

Uitgebreide uitwerking Tentamen Complexiteit, mei 2007

Uitgebreide uitwerking Tentamen Complexiteit, mei 2007 Uitgebreide uitwerking Tentamen Complexiteit, mei 007 Opgave. a. Een beslissingsboom beschrijft de werking van het betreffende algoritme (gebaseerd op arrayvergelijkingen) op elke mogelijke invoer. In

Nadere informatie

Getallensystemen, verzamelingen en relaties

Getallensystemen, verzamelingen en relaties Hoofdstuk 1 Getallensystemen, verzamelingen en relaties 1.1 Getallensystemen 1.1.1 De natuurlijke getallen N = {0, 1, 2, 3,...} N 0 = {1, 2, 3,...} 1.1.2 De gehele getallen Z = {..., 4, 3, 2, 1, 0, 1,

Nadere informatie

Het begrip 'Proces' Proces-toestand

Het begrip 'Proces' Proces-toestand Het begrip 'Proces' Een proces is de uitvoering van een programma Bij een proces hoort een programma (de code) Program Counter (Instructiewijzer) stack data (data sectie) twee of meer processen kunnen

Nadere informatie

3. Structuren in de taal

3. Structuren in de taal 3. Structuren in de taal In dit hoofdstuk behandelen we de belangrijkst econtrolestructuren die in de algoritmiek gebruikt worden. Dit zijn o.a. de opeenvolging, selectie en lussen (herhaling). Vóór we

Nadere informatie

1 Delers 1. 3 Grootste gemene deler en kleinste gemene veelvoud 12

1 Delers 1. 3 Grootste gemene deler en kleinste gemene veelvoud 12 Katern 2 Getaltheorie Inhoudsopgave 1 Delers 1 2 Deelbaarheid door 2, 3, 5, 9 en 11 6 3 Grootste gemene deler en kleinste gemene veelvoud 12 1 Delers In Katern 1 heb je geleerd wat een deler van een getal

Nadere informatie

Cover Page. The handle holds various files of this Leiden University dissertation

Cover Page. The handle  holds various files of this Leiden University dissertation Cover Page The handle http://hdl.handle.net/1887/28464 holds various files of this Leiden University dissertation Author: Jeroen Bédorf Title: The gravitational billion body problem / Het miljard deeltjes

Nadere informatie

Algoritmen abstract bezien

Algoritmen abstract bezien Algoritmen abstract bezien Jaap van Oosten Department Wiskunde, Universiteit Utrecht Gastcollege bij Programmeren in de Wiskunde, 6 april 2017 Een algoritme is een rekenvoorschrift dat op elk moment van

Nadere informatie

Toetsbundel Deel 1 Concurrency 10 december 2015, Gerard Tel, Niet verspreiden 1!.

Toetsbundel Deel 1 Concurrency 10 december 2015, Gerard Tel, Niet verspreiden 1!. Toetsbundel Deel 1 Concurrency 10 december 2015, Gerard Tel, Niet verspreiden 1!. Deze bundel bevat een collectie toetsvragen over het eerste deel van Concurrency. Je kunt deze bundel gebruiken voor je

Nadere informatie

1 Inleiding in Functioneel Programmeren

1 Inleiding in Functioneel Programmeren 1 Inleiding in Functioneel Programmeren door Elroy Jumpertz 1.1 Inleiding Aangezien Informatica een populaire minor is voor wiskundestudenten, leek het mij nuttig om een stukje te schrijven over een onderwerp

Nadere informatie

TECHNISCHE UNIVERSITEIT EINDHOVEN Faculteit Wiskunde en Informatica

TECHNISCHE UNIVERSITEIT EINDHOVEN Faculteit Wiskunde en Informatica TECHNISCHE UNIVERSITEIT EINDHOVEN Faculteit Wiskunde en Informatica Examen Operating Systemen (2R230) op vrijdag 26 augustus 2005, 14.00-17.00 uur. Het tentamen bestaat uit drie delen die apart worden

Nadere informatie

informatica. hardware. overzicht. moederbord CPU RAM GPU architectuur (vwo)

informatica. hardware. overzicht. moederbord CPU RAM GPU architectuur (vwo) informatica hardware overzicht moederbord CPU RAM GPU architectuur (vwo) 1 moederbord basis van de computer componenten & aansluitingen chipset Northbridge (snel) Southbridge ("traag") bussen FSB/HTB moederbord

Nadere informatie

CPU scheduling : introductie

CPU scheduling : introductie CPU scheduling : introductie CPU scheduling nodig bij multiprogrammering doel: een zo hoog mogelijke CPU-bezetting, bij tevreden gebruikers proces bestaat uit afwisselend CPU-bursts en I/O-bursts lengte

Nadere informatie

Het besturingssysteem of operating system, vaak afgekort tot OS is verantwoordelijk voor de communicatie van de software met de hardware.

Het besturingssysteem of operating system, vaak afgekort tot OS is verantwoordelijk voor de communicatie van de software met de hardware. Het besturingssysteem of operating system, vaak afgekort tot OS is verantwoordelijk voor de communicatie van de software met de hardware. Het vormt een schil tussen de applicatiesoftware en de hardware

Nadere informatie

Computerarchitectuur. Terugblik / discussie / oefenopgaven

Computerarchitectuur. Terugblik / discussie / oefenopgaven Computerarchitectuur Terugblik / discussie / oefenopgaven Kristian Rietveld http://ca.liacs.nl/ Trends & Performance Voorkennis We bouwden een 4-bit microprocessor bij Digitale Technieken. Bij computerarchitectuur

Nadere informatie

Conclusies over semaforen

Conclusies over semaforen Conclusies over semaforen gebruik semaforen is subtiel signal & wait operaties, en access van shared data, op allerlei plekken in de code Kan dit niet handiger? Dwz: zijn er geen betere abstracties? Ja:

Nadere informatie

Practicumopgave 3: SAT-solver

Practicumopgave 3: SAT-solver Practicumopgave 3: SAT-solver Modelleren en Programmeren 2015/2016 Deadline: donderdag 7 januari 2016, 23:59 Introductie In het vak Inleiding Logica is onder andere de propositielogica behandeld. Veel

Nadere informatie

Tentamen Object Georiënteerd Programmeren TI1200 30 januari 2013, 9.00-12.00 Afdeling SCT, Faculteit EWI, TU Delft

Tentamen Object Georiënteerd Programmeren TI1200 30 januari 2013, 9.00-12.00 Afdeling SCT, Faculteit EWI, TU Delft Tentamen Object Georiënteerd Programmeren TI1200 30 januari 2013, 9.00-12.00 Afdeling SCT, Faculteit EWI, TU Delft Bij dit tentamen mag je geen gebruik maken van hulpmiddelen zoals boek of slides. Dit

Nadere informatie

Tiende college algoritmiek. 2 mei Gretige algoritmen, Dijkstra

Tiende college algoritmiek. 2 mei Gretige algoritmen, Dijkstra College 10 Tiende college algoritmiek mei 013 Gretige algoritmen, Dijkstra 1 Muntenprobleem Gegeven onbeperkt veel munten van d 1,d,...d m eurocent, en een te betalen bedrag van n (n 0) eurocent. Alle

Nadere informatie

Kennismaking met programmeren

Kennismaking met programmeren Kennismaking met programmeren werkblad wij zijn de computer Project van de Pedagogische Academie, Hanzehogeschool Groningen en Groningen Programmeert in samenwerking met: In deze les gaan we op papier

Nadere informatie

Examen Datastructuren en Algoritmen II

Examen Datastructuren en Algoritmen II Tweede bachelor Informatica Academiejaar 2012 2013, eerste zittijd Examen Datastructuren en Algoritmen II Naam :.............................................................................. Lees de hele

Nadere informatie

recursie Hoofdstuk 5 Studeeraanwijzingen De studielast van deze leereenheid bedraagt circa 6 uur. Terminologie

recursie Hoofdstuk 5 Studeeraanwijzingen De studielast van deze leereenheid bedraagt circa 6 uur. Terminologie Hoofdstuk 5 Recursion I N T R O D U C T I E Veel methoden die we op een datastructuur aan kunnen roepen, zullen op een recursieve wijze geïmplementeerd worden. Recursie is een techniek waarbij een vraagstuk

Nadere informatie

2 n 1. OPGAVEN 1 Hoeveel cijfers heeft het grootste bekende Mersenne-priemgetal? Met dit getal vult men 320 krantenpagina s.

2 n 1. OPGAVEN 1 Hoeveel cijfers heeft het grootste bekende Mersenne-priemgetal? Met dit getal vult men 320 krantenpagina s. Hoofdstuk 1 Getallenleer 1.1 Priemgetallen 1.1.1 Definitie en eigenschappen Een priemgetal is een natuurlijk getal groter dan 1 dat slechts deelbaar is door 1 en door zichzelf. Om technische redenen wordt

Nadere informatie

Disclaimer Het bestand dat voor u ligt, is nog in ontwikkeling. Op verzoek is deze versie digitaal gedeeld. Wij willen de lezer er dan ook op wijzen

Disclaimer Het bestand dat voor u ligt, is nog in ontwikkeling. Op verzoek is deze versie digitaal gedeeld. Wij willen de lezer er dan ook op wijzen Disclaimer Het bestand dat voor u ligt, is nog in ontwikkeling. Op verzoek is deze versie digitaal gedeeld. Wij willen de lezer er dan ook op wijzen dat er zowel typografische als inhoudelijke onvolkomenheden

Nadere informatie

Uitgebreide uitwerking tentamen Algoritmiek Dinsdag 2 juni 2009, uur

Uitgebreide uitwerking tentamen Algoritmiek Dinsdag 2 juni 2009, uur Uitgebreide uitwerking tentamen Algoritmiek Dinsdag 2 juni 2009, 10.00 13.00 uur Opgave 1. a. Een toestand wordt bepaald door: het aantal lucifers op tafel, het aantal lucifers in het bezit van Romeo,

Nadere informatie

Het Eindfeest. Algoritmiek Opgave 6, Voorjaar

Het Eindfeest. Algoritmiek Opgave 6, Voorjaar 1 Achtergrond Het Eindfeest Algoritmiek Opgave 6, Voorjaar 2017 1 Om het (successvol) afsluiten van Algoritmiek te vieren, is er een groot feest georganiseerd. Jij beschikt als enige van je vrienden over

Nadere informatie

Tentamen Objectgeorienteerd Programmeren TI februari Afdeling ST Faculteit EWI TU Delft

Tentamen Objectgeorienteerd Programmeren TI februari Afdeling ST Faculteit EWI TU Delft I ' Tentamen Objectgeorienteerd Programmeren TI 1200 1 februari 2012 9.00-12.00 Afdeling ST Faculteit EWI TU Delft Bij dit tentamen mag je geen gebruik maken van hulpmiddelen zoals boek of slides. Dit

Nadere informatie

computerarchitectuur antwoorden

computerarchitectuur antwoorden 2017 computerarchitectuur antwoorden F. Vonk versie 1 2-8-2017 inhoudsopgave hardware... - 3 - CPU... - 3 - bussen... - 4 - bridges... - 4 - RAM... - 4 - hardware architectuur... - 5 - Dit werk is gelicenseerd

Nadere informatie

Tentamen Computersystemen

Tentamen Computersystemen Tentamen Computersystemen baicosy06 2e jaar bachelor AI, 2e semester 23 september 2013 13u-15u IWO 4.04A (blauw), Academisch Medisch Centrum, Meidreef 29, Amsterdam ZuidOost Het is niet toegestaan communicatieapparatuur

Nadere informatie

Zelftest Informatica-terminologie

Zelftest Informatica-terminologie Zelftest Informatica-terminologie Document: n0947test.fm 01/07/2015 ABIS Training & Consulting P.O. Box 220 B-3000 Leuven Belgium TRAINING & CONSULTING INTRODUCTIE Deze test is een zelf-test, waarmee u

Nadere informatie

Modulewijzer InfPbs00DT

Modulewijzer InfPbs00DT Modulewijzer InfPbs00DT W. Oele 0 juli 008 Inhoudsopgave Inleiding 3 Waarom wiskunde? 3. Efficiëntie van computerprogramma s............... 3. 3D-engines en vectoranalyse................... 3.3 Bewijsvoering

Nadere informatie

Getallenleer Inleiding op codeertheorie. Cursus voor de vrije ruimte

Getallenleer Inleiding op codeertheorie. Cursus voor de vrije ruimte Getallenleer Inleiding op codeertheorie Liliane Van Maldeghem Hendrik Van Maldeghem Cursus voor de vrije ruimte 2 Hoofdstuk 1 Getallenleer 1.1 Priemgetallen 1.1.1 Definitie en eigenschappen Een priemgetal

Nadere informatie

Digitale en analoge technieken

Digitale en analoge technieken Digitale en analoge technieken Peter Slaets February 14, 2006 Peter Slaets () Digitale en analoge technieken February 14, 2006 1 / 33 Computerarchitectuur 1 Processors 2 Primair geheugen 3 Secundair geheugen

Nadere informatie

Java virtuele machine JVM

Java virtuele machine JVM Implementatie Java Java virtuele machine JVM Java programma vertaald naar byte code instructies Byte code instructies uitgevoerd door JVM JVM is processor architectuur kan in principe in hardware worden

Nadere informatie

Opgaven Binair Zoeken en Invarianten Datastructuren, 4 mei 2016, Werkgroep.

Opgaven Binair Zoeken en Invarianten Datastructuren, 4 mei 2016, Werkgroep. Opgaven Binair Zoeken en Invarianten Datastructuren, 4 mei 2016, Werkgroep. Gebruik deze opgaven, naast die uit het boek, om de stof te oefenen op het werkcollege. Cijfer: Op een toets krijg je meestal

Nadere informatie

Vakinhoudelijke uitwerking Keuzevak Applicatieontwikkeling van het profiel MVI vmbo beroepsgericht

Vakinhoudelijke uitwerking Keuzevak Applicatieontwikkeling van het profiel MVI vmbo beroepsgericht Vakinhoudelijke uitwerking Keuzevak Applicatieontwikkeling van het profiel MVI vmbo beroepsgericht Deze vakinhoudelijke uitwerking is ontwikkeld door het Redactieteam van de Schooleamenbank vmbo voor dit

Nadere informatie

Toets In2305-ii Embedded Programming Dinsdag 28 November 2006, 15:45-16:30

Toets In2305-ii Embedded Programming Dinsdag 28 November 2006, 15:45-16:30 Toets In2305-ii Embedded Programming Dinsdag 28 November 2006, 15:45-16:30 Teneinde misverstanden over de syntactische geldigheid van code fragmenten in dit tentamen te voorkomen, zal altijd worden gesproken

Nadere informatie

3. Structuren in de taal

3. Structuren in de taal 3. Structuren in de taal In dit hoofdstuk behandelen we de belangrijkst econtrolestructuren die in de algoritmiek gebruikt worden. Dit zijn o.a. de opeenvolging, selectie en lussen (herhaling). Vóór we

Nadere informatie

Twaalfde college complexiteit. 11 mei 2012. Overzicht, MST

Twaalfde college complexiteit. 11 mei 2012. Overzicht, MST College 12 Twaalfde college complexiteit 11 mei 2012 Overzicht, MST 1 Agenda voor vandaag Minimum Opspannende Boom (minimum spanning tree) als voorbeeld van greedy algoritmen Overzicht: wat voor technieken

Nadere informatie

Datastructuren Uitwerking jan

Datastructuren Uitwerking jan Datastructuren Uitwerking jan 2015 1 1a. Een abstracte datastructuur is een beschrijving van een datastructuur, met de specificatie van wat er opgeslagen wordt (de data en hun structuur) en welke operaties

Nadere informatie

Digitale technieken Deeltoets II

Digitale technieken Deeltoets II Digitale technieken Deeltoets II André Deutz 11 januari, 2008 De opgaven kunnen uiteraard in een willekeurige volgorde gemaakt worden geef heel duidelijk aan op welke opgave een antwoord gegegeven wordt.

Nadere informatie

Netchange. Concurrency Opgave 2, December

Netchange. Concurrency Opgave 2, December Netchange Concurrency Opgave 2, December 2016 1 Opdracht Achtergrond Het internet wordt gevormd door vele computers die met elkaar in verbinding staan en een groot netwerk vormen. Op deze kaart kun je

Nadere informatie

Recursion. Introductie 37. Leerkern 37. Terugkoppeling 40. Uitwerking van de opgaven 40

Recursion. Introductie 37. Leerkern 37. Terugkoppeling 40. Uitwerking van de opgaven 40 Recursion Introductie 37 Leerkern 37 5.1 Foundations of recursion 37 5.2 Recursive analysis 37 5.3 Applications of recursion 38 Terugkoppeling 40 Uitwerking van de opgaven 40 Hoofdstuk 5 Recursion I N

Nadere informatie

DE ASTRO PI PROGRAMMEREN VOOR MISSION ZERO

DE ASTRO PI PROGRAMMEREN VOOR MISSION ZERO DE ASTRO PI PROGRAMMEREN DOCENTENHANDLEIDING 1 Deze handleiding is bedoeld om leerlingen te ondersteunen bij de Astro Pi Challenge, waarbij leerlingen een programma voor de Astro Pi-computer in het ISS

Nadere informatie

Real-Time Systems (RTSYST)

Real-Time Systems (RTSYST) Real-Time Systems (RTSYST) Week 2 Process/Thread states ready running Wait for I/O or I/O or completion blocked / sleeping Scheduler = deel van OS dat de toestanden van processen/threads bepaald. OS gebruikt

Nadere informatie

Nederlandse samenvatting (Dutch summary)

Nederlandse samenvatting (Dutch summary) Nederlandse samenvatting (Dutch summary) Ditproefschriftpresenteerteen raamwerk voorhetontwikkelenvanparallellestreaming applicaties voor heterogene architecturen met meerdere rekeneenheden op een chip.

Nadere informatie

TECHNISCHE UNIVERSITEIT EINDHOVEN Faculteit Wiskunde en Informatica

TECHNISCHE UNIVERSITEIT EINDHOVEN Faculteit Wiskunde en Informatica TECHNISCHE UNIVERSITEIT EINDHOVEN Faculteit Wiskunde en Informatica Deel-tentamen Operating Systemen (2IN05) op vrijdag 5 oktober 2007, 9.00-10.30 uur. Het tentamen bestaat uit twee delen die apart worden

Nadere informatie

start -> id (k (f c s) (g s c)) -> k (f c s) (g s c) -> f c s -> s c

start -> id (k (f c s) (g s c)) -> k (f c s) (g s c) -> f c s -> s c Een Minimaal Formalisme om te Programmeren We hebben gezien dat Turing machines beschouwd kunnen worden als universele computers. D.w.z. dat iedere berekening met natuurlijke getallen die met een computer

Nadere informatie

Vorig college. IN2505-II Berekenbaarheidstheorie College 4. Opsommers versus herkenners (Th. 3.21) Opsommers

Vorig college. IN2505-II Berekenbaarheidstheorie College 4. Opsommers versus herkenners (Th. 3.21) Opsommers Vorig college College 4 Algoritmiekgroep Faculteit EWI TU Delft Vervolg NDTM s Vergelijking rekenkracht TM s en NDTM s Voorbeelden NDTM s 20 april 2009 1 2 Opsommers Opsommers versus herkenners (Th. 3.21)

Nadere informatie

Automaten & Complexiteit (X )

Automaten & Complexiteit (X ) Automaten & Complexiteit (X 401049) Inleiding Jeroen Keiren j.j.a.keiren@vu.nl VU University Amsterdam Materiaal Peter Linz An Introduction to Formal Languages and Automata (5th edition) Jones and Bartlett

Nadere informatie

ALGORITMIEK: antwoorden werkcollege 5

ALGORITMIEK: antwoorden werkcollege 5 ALGORITMIEK: antwoorden werkcollege 5 opgave 1. a. Brute force algoritme, direct afgeleid uit de observatie: loop v.l.n.r. door de tekst; als je een A tegenkomt op plek i (0 i < n 1), loop dan van daaruit

Nadere informatie

Variabelen en statements in ActionScript

Variabelen en statements in ActionScript Ontwikkelen van Apps voor ios en Android Variabelen en statements in ActionScript 6.1 Inleiding Als we het in de informatica over variabelen hebben, bedoelen we een stukje in het geheugen van de computer

Nadere informatie

Computerarchitectuur en netwerken. Memory management Assembler programmering

Computerarchitectuur en netwerken. Memory management Assembler programmering Computerarchitectuur en netwerken 2 Memory management Assembler programmering Lennart Herlaar 10 september 2018 Inhoud 1 Protectie: Hoe het O.S. programma s tegen elkaar kan beschermen modes memory management

Nadere informatie