Alexander Pil
17 November 2006

Embedded systemen reageren op (externe) events. Het moment en de frequentie hiervan zijn deels onvoorspelbaar. Vaak wordt een besturingssysteem gebruikt om de softwaredelen die verantwoordelijk zijn voor afhandeling van deze events op het juiste moment te activeren. In de praktijk blijkt dat designers van dergelijke concurrentsystemen vaak onvoldoende kennis hebben waardoor ze ontwerpbeslissingen in de verkeerde fase en op oneigenlijke gronden nemen. Dit leidt tot overcomplexe, moeilijk te testen systemen waarbij veel fouten niet (tijdig) boven water komen. In dit artikel gaat Leon van Snippenberg in op de achtergronden van concurrentiesoftwareontwerp.

Embedded software maakt steeds vaker gebruik van een besturingssysteem. Dit is verantwoordelijk voor het op het juiste moment activeren van processen en voor communicatie en synchronisatie tussen deze processen. Het biedt slechts de primitieve functionaliteit. De ontwerper moet zorgen voor een correct werkend geheel. In de praktijk is zijn kennis vaak onvoldoende of gebruikt hij het verkeerd. Ook bij opleidingsinstituten is steeds minder aandacht voor ontwerpen van concurrentiesystemen ten faveure van meer fancy onderwerpen als webdesign en GUI-ontwerp. De afstand tussen benodigde en aanwezige kennis wordt steeds groter. Door gebruik te maken van een aantal uitgangspunten kunnen we dit probleem verkleinen.

Waarom concurrency

Systeemontwerpers moeten zich bewust zijn van de redenen waarom ze concurrency überhaupt moeten toepassen. Concurrency biedt een oplossing voor een probleem maar is geen doel op zich. In de praktijk zie ik dat ze deze redenen niet of onvoldoende hanteren. Vaak is het waarom een intuïtieve aangelegenheid waarbij ontwerpers veelal performance als reden opvoeren zonder dit nader te kwantificeren. Voor het introduceren van concurrency in een systeemontwerp zijn vijf mogelijke redenen.

Ten eerste omdat we urgente processen met voorrang moeten afhandelen. Afhankelijk van het optredende event moet de afhandeling hiervan binnen een gedefinieerde tijdspanne beginnen. Dit kan er toe leiden deze afhandeling te modelleren als een apart proces met een zodanige prioriteit dat het besturingssysteem hem met voorrang activeert.

Ten tweede kun je zo de beschikbare processorcapaciteit optimaal benutten. Een softwaremodule reageert op een event of verwerkt gegevens van een andere module. Door de ontvangende module uit te voeren als een autonoom proces zal het besturingssysteem hem activeren als aan de gewenste ingangsconditie is voldaan. Zodoende hoeft dit proces niet steeds actief te worden om zelf te checken op zijn trigger. Zo verbruikt het systeem geen onnodige processorcycli.

Verder is veiligheid een reden. Sommige softwaredelen zijn kritischer voor het functioneren van totale systeem dan andere. Door deze componenten uit te voeren als apart processen blijven ze actief terwijl andere processen zijn gecrasht.

Ook de eenvoud van programmeren is een motief om concurrency in te voeren. Door een softwarestuk uit te voeren als apart proces kan het implementeren en testen hiervan eenvoudiger zijn dan wanneer het onderdeel zou zijn van een groter geheel.

Ten slotte speelt responsiveness een rol. Het splitsen van de software in meerdere processen kan leiden tot een reactiever systeem. Door de gebruikersinteractie uit te voeren als apart proces kan het besturingssysteem dit autonoom activeren. De gebruiker hoeft dan niet te wachten op bijvoorbeeld een langdurige berekening omdat deze laatste in een ander proces is opgenomen.

In welke ontwerpfase

Een systeemontwerp bestaat uit de fasen analyse, globaal ontwerp, detailontwerp en implementatie. In elke fase moeten we anders met concurrency omgaan. Tijdens de analyse leggen we het functionele systeemgedrag vast zonder rekening te houden met implementatiedetails. Een van deze bijzonderheden is het gebruikte besturingssysteem. Meestal is die al bekend voordat we aan de analyse beginnen. Dit hoeft geen probleem te zijn maar het leidt er vaak wel toe dat we OS-eigenschappen meenemen in de analyse. Dit vergroot echter de complexiteit. In het analysemodel wordt bijvoorbeeld gesproken over de tijd die het besturingssysteem nodig heeft om te schakelen tussen twee processen. Met het functionele systeemgedrag heeft dat echter niets van doen. Deze complexiteit maakt het model minder herbruikbaar en moeilijker te communiceren naar collega‘s uit andere disciplines. Mijn advies is dan ook om de aanwezige kennis over implementatiedetails niet te gebruiken in de analyse. Figuur 1 toont een zeer gestileerd analysemodel bestaande uit een aantal klassen waar nog geen sprake is van concurrency.

Het analysemodel vormt de input voor het globale ontwerp. Hier identificeren we een aantal hoogniveaumodules die gezamenlijk het gewenste systeemgedrag implementeren. Ook hier zie ik in de praktijk dat processen en hun eigenschappen een prominente rol spelen, terwijl dit te vroeg is. De modules die in deze fase ontstaan, communiceren uiteraard met elkaar. Het is voldoende om te identificeren of de communicatie tussen deze modules synchroon of asynchroon is. Als een module functionaliteit asynchroon aanbiedt, impliceert dit dat binnen die module een of meerdere processen actief zullen zijn. Dit komt pas naar voren in de volgende fase. Omdat ontwerpers in de praktijk vaak te veel detail in hun globale design aanbrengen, leidt ook dit tot een te complex en daardoor slechter te gebruiken model. Figuur 2 toont het model waarbij een opdeling is gemaakt in een aantal modules. Met de pijl tussen de modules geef ik aan dat hier asynchrone communicatie plaatsvindt zonder me hierbij druk te maken over details.

Wel vind ik dat we in deze fase moeten nadenken over hoe we in de vervolgfasen moeten omgaan met concurrency. Het gebruikte besturingssysteem is vaak zo rijk aan features dat een inperking van het gebruik hiervan nuttig is, juist op het gebied van concurrency. Het loont om richtlijnen op te stellen waaraan de ontwerpers zich in de vervolgfasen dienen te houden.

In de detailontwerpfase werken we de geïdentificeerde modules en hun interfaces verder uit. Hier worden processen voor het eerst geïdentificeerd. Asynchrone functies hebben een autonoom proces nodig. De moduleontwerper geeft aan hoe dit wordt opgelost, liefst op basis van de eerder genoemde richtlijnen. Omdat deze vaak niet zijn vastgelegd, ontstaan in deze fase verschillende manieren voor het oplossen van het concurrencyprobleem. Figuur 3 toont het soort modellen dat in deze fase ontstaat. De cirkelsymbolen geven hier de processen aan.

Tijdens de implementatie zetten we de modules uit het detailontwerp om in code. De processen zijn nu bekend. Een programmeur mag nooit zelfstandig besluiten om processen in de implementatie te introduceren of hun vastgelegde eigenschappen aan te passen. In vrijwel alle projecten waaraan ik heb meegewerkt gebeurde dit echter wel. Het is hier dat de grootste problemen ontstaan. Vaak maken ontwikkelaars met ontoereikende kennis over zowel de systeemeisen als de werkwijze van het OS een implementatie waarbij ze op eigen houtje processen introduceren. Figuur 4 toont de code die de modules uit figuur 3 implementeert. Hierbij is het essentieel dat er een een-op-eenvertaling wordt gemaakt van het voorgaande model.

Hoe concurrency toe te passen

De eigenschappen van het besturingssysteem bepalen hoe concurrency op implementatieniveau wordt toegepast. In de praktijk kunnen we hier op heel verschillende manieren mee omgaan. Niet alle strategieën gebruiken de OS-eigenschappen echter correct.

Moderne besturingssystemen bieden een veelvoud aan functionaliteit, met name voor het beheer van en de communicatie tussen processen. Vaak zie ik dat eigen implementaties worden gemaakt voor features die het OS biedt. Hiervoor zijn twee redenen. De ontwikkelaars kennen het besturingssysteem onvoldoende of ze vertrouwen het niet. Dit laatste leidt soms tot draconische ontwerpen. De basisfunctionaliteit van het OS is schedulen. Het bepaalt aan de hand van een aantal criteria en het optreden van events welk van de processen actief moet zijn. In de praktijk zie ik dat ontwikkelaars toch een eigen schedule-mechanisme implementeren. De argumentatie is dan dat ze niet zeker waren of het besturingssysteem het wel goed zou doen.

Een tweede voorbeeld is de angst voor te veel processen. Een formeel doorlopen systeemontwerp leidt tot een aantal processen. In de praktijk voegen we vrijwel altijd functionaliteit samen in een proces terwijl we die logischerwijs over meerdere processen zouden moeten verdelen. Het doel is dan om het aantal processen te verkleinen. Dit maakt de code complexer omdat de ontwerper er zelf voor moet zorgen dat elke subfunctionaliteit binnen deze processen op het juiste moment actief wordt. Dat is typisch een taak die hoort bij het besturingssysteem. Feitelijk moeten ze dan een eigen scheduler bouwen.

De argumenten die ontwikkelaars ter verdediging aanvoeren, zijn tijd en geheugen. Voor schakelen tussen processen heeft het besturingssysteem tijd nodig. Vaak ligt deze switchtijd in de orde van een aantal microseconden. Voor vrijwel elk OS is deze tijd constant, ongeacht hoeveel processen het beheert. Hij wordt dus niet korter door het aantal processen kunstmatig te verkleinen. Scheduletijd gebruiken als argument voor het verkleinen van het aantal processen is een drogreden.

Geheugengebruik is wel een geldig argument. Elk proces heeft een hoeveelheid geheugen nodig en toename van het aantal processen zal hier een groter beslag op leggen. Een analyse wordt echter meestal niet uitgevoerd en ontwerpbeslissingen worden genomen op basis van termen als ’te veel‘, ’te langzaam‘ of ’te duur‘. Dit verhoogt de systeemcomplexiteit en daarmee de ontwikkeltijd en de kans op fouten.

Ik heb aangegeven hoe ik vind dat we met concurrency moeten omgaan. Door het toepassen van deze regels is het gehele proces deterministischer en daardoor beter beheersbaar. Om dit te bereiken is het nodig dat het kennisniveau van de materie groeit en dat meer inzicht bestaat over de interne werking van het bouwwerk van parallelle processen. Het belang hiervan wordt op termijn alleen maar groter daar de groeiende complexiteit van embedded systemen en het toenemende gebruik van een besturingssysteem voor het beheer van de verschillende softwareprocessen.

Leon van Snippenberg is softwarearchitect bij Atos Origin Technical Automation en binnen dit bedrijf lid van de competence Technical Software Engineering (TSE). Hij heeft meer dan twintig jaar ervaring in ontwerp en implementatie van embedded systemen met als specialisatie concurrency.