KURSSIKUULUSTELUN 13.5.2004 ESIMERKKIRATKAISUT JA ARVOSTELUPERUSTEET ====================================================================== Jos esimerkkiratkaisuissa on jotain epäselvyyksiä tai kummallisuuksia, niin ilmoitelkaa asiasta Liisa Marttiselle liisa.marttinen@cs.helsinki.fi. Eri tehtävien keskiarvot: ------------------------- Tehtävä 1: 7.3 pistettä eli noin 81 % maksimipisteitä Tehtävä 2: 6.5 pistettä eli noin 54 % maksimipisteitä. Tehtävä 3: 9.6 pistettä eli noin 64 % maksimipisteistä. Tehtävä 4: 4.5 pistettä eli noin 30 % maksimipisteistä. Yhteensä 27.9 pistettä eli noin 55 % maksimipistesitä Koepisteiden jakauma: ---------------------- 0 xx 1 2 3 4 5 6 7 x 8 x 9 10 x 11 12 x 13 xx 14 xx 15 x 16 xx 17 18 xxx 19 xxx 20 21 x 22 xxx 23 24 xx 25 xx 26 x 27 x 28 xx 29 x 30 xx 31 x 32 xxxx 33 x 34 xxx 35 xx 36 xxx 37 x 38 x 39 x 40 xxxxx 41 42 xx 43 44 x 45 x 46 47 xx 48 xx 49 50 51 ================================================================ Tehtävä 1 (Toni Ruokolainen) ================================================================ 1.Vastaa lyhyesti seuraaviin kysymyksiin (9p) a) Mitä tarkoitetaan lukkiutumisella (dead lock) ja nälkiintymisellä (starvation)? Anna aterioiviin filosofeihin perustuvat esimerkit lukkiutumisesta ja nälkiintymisestä. (4p) Pisteytys: Lukkiutumisen ja nälkiintymisen määritelmät 1 piste kummastakin. Esimerkeistä myös 1 piste per osio (lukkiutuminen & nälkiintyminen). Mallivastaus: * Lukkiutuminen: prosessi(t) odottaa jotakin tapahtumaa (BLOCKED-tilassa), jota ei voi kos­kaan tapahtua.Lukkiutumattomuus on yksi tärkeimmistä hajautetun järjestelmän turvallisuusominaisuuksista (safety). * Nälkiintyminen: prosessilta evätään sen tarvitsemat resurssit kerta toisensa jälkeen (prosessi READY-tilassa). Nälkiintyminen estetään vaatimalla järjestelmältä reiluutta (fairness). Nälkiintymisen olemattomuus on järjestelmän elävyysominaisuus (liveness). * Lukkiutuminen: Kaikki filosofit ottavat vasemman puolen haarukan ja odottavat oikean puolei­sen haarukan vapautumista. Nälkiintyminen: välissä olevaa filosofia sorsitaan muiden toimesta (kun filosofit antavat haarukkansa pois, jos eivät saa molempia) tai filosofit saavat haarukkansa tarjoilijalta, joka ei takaa reilua jakoa. b) Miksi tuottajan ja kuluttajan väliin kannattaa laittaa puskuri? Miten puskurin koko vaikuttaa järjestelmän suorituskykyyn? (2p) Pisteytys: 1 piste kummastakin osakysymyksestä. Mallivastaus: i) Mahdollisuus asynkroniseen toimintaan. Purskeisuuden tasaaminen. ii) Tuottajan ja kuluttajan väliin kannattaa laittaa puskuri, jos tuottajan toiminta on luonteeltaan purskeista, ts. se tuottaa yhtäkkiä paljon dataa, jonka jälkeen se suorittaa muuta toiminnallisuut­taan. Puskurin avulla saadaan järjestelmän tehokkuutta parannettua, koska tuottajan ei tarvitse enää odottaa, että kuluttaja on ehtinyt lukemaan yksipaikkaisen puskurin tyhjäksi. Periaatteessa suurempi puskuri on aina parempi, koska tällöin tuottajan tarvitsee (periaatteessa) harvemmin odottaa puskuritilan vapautumista. Toisaalta tietyn kriittisen rajan jälkeen puskuritilan koon kasvattaminen ei enää lisää suorituskykyä, jos oletetaan että kuluttaja tyhjentää puskuria keskimäärin nopeammin kuin tuottaja sitä täyttää. c) Miksi semaforien käyttö synkronoinnissa ja tahdistuksessa on suositeltavampaa kuin lukkomuuttujan käyttö? Mitä lisähyötyä on monitorista? (3p) Pisteytys: semaforin ja lukkomuuttujan toimintaperiaatteet 1p, perustelu semaforin suositeltavuudelle 1p, monitorin lisähyöty 1p Mallivastaus: * semafori on KJ:n ylläpitämä abstrakti muuttuja, johon liittyy kaksi operaatiota: semaforin varaus WAIT() / P() / DOWN() ja semaforin vapautus SIGNAL() / V() / UP(). Kun semafori varataan, niin semaforimuuttujaa vähennetään yhdellä. Jos semaforimuuttujan arvo oli nolla, niin prosessi pistetään BLOCKED-jonoon odottamaan resurssin vapautumista. Kun prosessi vapaut­taa semaforin, niin semaforimuuttujaa lisätään yhdellä ja tätä tapahtumaa odottavat prosessit herätetään / herätetään ensimmäinen jonossa. Lukkomuuttuuja on myös KJ:n ylläpitämä palvelu, mutta siinä tiettyä muuttujaa luetaan busy-silmukassa atomisella test-and-set -tyyppisellä operaatiolla kunnes muuttuja on halutussa arvossa (esim. 1). * Semafori on suositeltavampi, koska se kuormittaa vähemmän järjestelmän laskentaresursseja sekä muistiväylää (erityisesti moniprosessorijärjestelmät!) * Monitorit toteuttavat semaforeja korkeamman abstraktiotason poissulkemis- ja synkronointipalvelun. Monitorit ovat jonkin abstraktin objektin ilmentymä, johon liittyy sekä dataa että operaatioita. Monitori takaa poissulkemisominaisuuden implisiittisesti, koska vain yksi monitori-instanssin operaatioista on suorituksessa kerrallaan. Pistejakauma: 0 xx 1 2 3 x 4 x 5 xxxx 6 xxxxx 7 xxxxxxxxxxxxxxxxx 8 xxxxxxxxxxxxxx 9 xxxxxxxxxxxxxxxxxxxx Keskiarvo 7.3 pistettä eli noin 81 % maksimipisteitä ============================================================ Tehtävä 2 (Liisa Marttinen) ============================================================= Ratkaistaan tehtävä vähitellen tarkentamalla: Kuka odottaa ja mitä eli mitä cond-jonoja tarvitaan: kuormuri odottaa kuoppa vapaa => cond saa_ajaa boolean varattu (koska aina ei tarvitse jonottaa, vaaan pääsee suoraan jatkamaan) kuorma valmistunut => cond saa_poistua (tässä aina odotus) kaivuri odottaa kuopalla lastattava auto => cond saa_kauhoa => boolean autovalmis (tässä auto voi olla ensin tai sitten kaivuri odottaa) Miten kuormuri toimii: jos kuoppa varattu jää odottamaan, muuten aja suoraan kuoppalle ilmoita tulostasi kaivurille odota, että kuorma valmis aja pois kuopalta ja päästä seuraava kuormuri (condition passing eli tulojärjestyksessä) Miten kaivuri toimii: odottele, kunnes kuopalle tulee lastattava auto kauho kuormuri täyteen anna kuormurille poistumislupa Kuormurin tarkemmin koodattuna: ------------------------------- if (varattu) wait (saa_ajaa) # jos kuoppa varattu jää odottamaan, else varattu = true; # muuten varaa kuoppa aja kuopalle; # aja suoraan kuoppalle autovalmis = true; # ilmoita tulostasi kaivurille: 'jätä merkki' signal (saa_kauhoa); # herätä jo odotteleva kaivuri wait (saa_poistua); # odota, että kuorma valmis aja pois kuopasta; # aja pois kuopalta if (!empty (saa_ajaa)) signal (saa_ajaa) # ja päästä seraava: ensin odottava kuormuri else varattu = false; # muuten uusi tulija kaivurin koodi tarkennettuna: ----------------------------- if (!autovalmis) wait (saa_kauhoa); # odottele, kunnes kuopalle tulee lastattava auto autovalmis = false; # ettei täytä useaan kertaan! kauho kuormuri täyteen; # kauho kuormuri täyteen signal (saa_poistua); # anna kuormurille poistumislupa Monitori + prosessit =================== monitor Kuoppa { cond saa_ajaa; cond saa_poistua; cond saa_kauhoa; boolean varattu = false, autovalmis = false; procedure Haelasti( ) { # kuormurin koodi if (varattu) wait(saa_ajaa) else varattu = true; # vain yksi pääsee kuopalle # huom. condition passing: jonosta otettaessa 'puomi on suljettu' (varattu = true) # uusilta tulijoilta; niiden on mentävä odotusjonoon aja kuopalle autovalmis = true; # jos kaivuri ei ole vielä odottamassa signal(saa_kauhoa); # jos kaivuri on jo odottamassa wait(saa_poistua); # jää odottamaan poistumismerkkiä aja pois kuopalta if (!empty(saa_ajaa) signal(saa_ajaa) # päästetään seuraaava jonottaja jo on else varattu =false; # tai avataan pääsy uusille tulijoille (condition passing) } procedure Teekuorma( ) { # kaivurin koodi if (!autovalmis) wait(saa_kauhoa); # odotetaan uutta autoa autovalmis= false; # jotta ei täytetä samaa autoa useaan kertaan kauho kuormuri täyteen signal(saa_poistua); # anna poistumissignaali } } process Kuormuri( )[i=1 to n)] { while (true) { ajele missä ajlet call Kuoppa. Haelasti ( ); tyhjennä kuorma } } process Kaivuri( ) { while(true) call Kuoppa.Teekuorma ( ); } Ratkaisussa kuopalle ajo ja sieltä poistuminen sekä kuorman täyttö ovat monitorin sisällä, vaikka monitorin sisällä tulee olla aivan välttämättömän ajan. Tässä tehtävässä siitä ei tosin ole paljoakaan haittaa, koska varsinaine homma etenee vain sen yhden kaivurin tahdissa ja kaivurin on ensin saatava auto kuopalle, täytetävä se ja auton on poistuttava ennenkuin seuraava auto voi tulla. Kuitenkin monitorin varattuna pitäminen voi aiheuttaa jonoa monitoriin ja vapautuneen kaivurin on odotettava monitoriin jonottavien kuormurien uuteen jonoon menoa ennenkuin se puolestaan pääsee monitoriin ja voi ruveta täyttämään seuraavaa kuormuria. Ratkaisu, jossa mnitorissa ei turhaan viivytellä ================================================ Ratkaisu saadaan yksinkertaisesti jakamalla kuormurin ja kaivurin proseduurit pienempiin osiin. monitor Kuoppa { cond saa_ajaa; cond saa_poistua; cond saa_kauhoa; boolean varattu = false, # kuopalla on jo auto => tieto muille kuormureille utovalmis = false; # tyhjä auto valmiina kuopalla => tieto kaivurille procedure Tuleekuopalle( ) { # kuormaa hakeva kuormuri if (varattu) wait (saa_ajaa) else varattu = true; # vain yksi pääsee kuopalle # huom. tääs käytössä condition passing: # jonosta otettaessa 'puomi on suljettu' } procedure Onkuopalla( ) { # kuoppaan saapunut kuormuri # ilmoittaa kaivurille autovalmis = true; # jos kaivuri ei ole vielä odottamassa 'jättää merkin' signal(saa_kauhoa); # jos kaivuri on jo odottamassa, vapauttaa jonosta wait(saa_poistua); # jää odottamaan poistumislupaa } procedure Poiskuopalta ( ) { # kuopasta poistunut kuormuri # päästää seuraavan kuormurin; huom. condition passing if (!empty(saa_ajaa) signal(saa_ajaa) # jonosta jos on else varattu =false; # muuten seuraavan saapuvan ('puomi auki') } procedure Odottaakuormuria( ) { # kuormauksen aloittava kaivuri if(!autovalmis) wait(saa_kauhoa); autovalmis= false; # ei täytetä samaa autoa useaan kertaan } procedure Kuormurihoidettu ( ) { # kuormaus valmis signal (saa_poistua); } } process Kuormuri ( ) [i=1 to n] { while (true) { ajele missä ajelet call Kuoppa.Tuleekuopalle( ); aja kuopalle call Kuoppa.Onkuopalla( ); aja pois kuopalta call Kuoppa.Poiskuopalta( ); tyhjennä kuorma } } process Kaivuri ( ) { while (true) { call Kuoppa.Odottaakuormuria ( ); kauho kuormuri täyteen call Kuoppa.Kuormurihoidettu ( ); } } Arvostelusta: ============ Kaikkiaan 12 pistettää: monitorista 9 p ja prosesseista 3 p toimintavirheitä: autoja pääsee kuopalle kuinka paljon tahansa -3 p kuopalle voi päästä kaksi autoa kerrallaan -2 p (if ja ei condition passing) lukkiutumistilanne eli kaikki odottavat -2 p kaivuri täyttää jo täyden kuormurin uudestaan -1 p Kuormuri-prosessissa odotellaan silmukassa kunnes kuorma on täynnä -2 p cond ? jonomuuttajat puuttuvat kokonaan -2p call M.proseduuri puuttuu -1 p, call puuttuu -1/2 p tai monitorin nimi puuttuu -1/2 p vain yksi kuormuri prosessi -1 signal ja wait tunnettu +2 p cond muuttujia määritelty +1 p näiden järkevä käyttö +1 p Ei edellytetty FCFS-järjestystä Pistejakauma: 0 xxxxxxxxx 1 xx 2 xxxxx 3 x 4 xxxx 5 xxx 6 xx 7 xxxx 8 xxxxxxxx 9 xxxx 10 xxxxxxxxxxx 11 xxxxxxxxx 12 xx Tehtävän keskiarvo 6.5 pistettä eli noin 54 % maksimipisteitä. =========================================== Tehtävä 3 ============================================ /* Example answer / Sini Ruohomaa * * Points where we must wait: * Customers: * - wait outside until the guide starts the tour * (- wait for the tour to finish) * The guide: * - wait for at least 10 people to show up for the group * (- wait for the customers to notice that a tour has started) * (- after the tour, wait for the customers to leave) * * The parts in parentheses are not necessary since they were not mentioned * in the task formulation, so we leave them out. * * This is an example of barrier synchronization. You might compare it * with the bees, the trapped bear and the honeypot, or the carousel. */ int waiting_customers = 0; /* Synchronization: ready_group_prepared -- there are enough people in the group. tour_has_begun -- the guide has noted this and is ready to go. The first semaphor allows the guide to continue, the second allows the customers to continue. */ sem ready_group_prepared = 0; sem tour_has_begun = 0; /* Mutual exclusion: * mutex -- protect the waiting_customers variable in two ways: * - only 1 process at a time is allowed to change it, and * - when the tour is just about to begin ("the guide is counting the * customers"), new customers should not enter the yard. See the * guide code for details. */ sem mutex = 1; process customer[i = 1 to N] { /* Processes do not repeat by themselves. The customers do not need to come to the museum several times, but since there is only one guide, he will need to give many tours; hence in process guide, while(true) looping is necessary. */ while(true) { /* A new customer comes up and increases the counter accordingly. To * avoid having two customers modify the counter simultaneously, we * wait for 'mutex' first. */ P(mutex); waiting_customers++; if(waiting_customers == 10) V(ready_group_prepared); /* Inform the guide: there's a group ready. */ V(mutex); /* We allow for more than 10 customers to show up, hence the * V(mutex) is not inside an 'else' clause. The 'if' checks for * equality, because for a check like "waiting_customers >= 10", * 13 customers would cause V(ready_group_prepared) to be called 4 * times, which would make the guide start 4 tours in a row even * if no one showed up afterwards, since the semaphore's value go * up to 4. */ /* We need to wait for the tour to actually start, as described in * the task. */ P(tour_has_begun); /* Once we hear from someone that the tour has begun, we must * inform the guy next to us about it too. Let's pass the baton: */ waiting_customers--; if(waiting_customers > 0) V(tour_has_begun); else /* Everyone's heard, so we're off; let in new customers again. */ V(mutex); /* (Enjoy the tour, leave when you feel like it. Rinse and repeat.) */ } } process guide { while(true) { /* We drink coffee while waiting for enough customers to arrive. Note that we do not necessarily wake up immediately after someone calls V(ready_group_prepared), so the group size can grow. */ P(ready_group_prepared); /* While this can be done in many ways, we choose to * stop new customers from arriving before we start the tour. * This 1) enables us to skip requesting and releasing 'mutex' * for each customer separately when they decrease waiting_customers * and check whether it is still above 0, and 2) it makes sure that * the tour starts eventually, since even a flood of new potential * customers will not make us end up adding one new customer, taking * one new along, adding one new etc. * * The mutex will be opened once the customers have decreased * the waiting_customers counter back to 0 by removing themselves. */ P(mutex); /* Then we inform the first tourer. As implied before, we wake the * customers up by baton-passing. If we just called * V(tour_has_begun) here as many times as we have * waiting_customers and then released 'mutex', new customers could * come add themselves to the waiting_customers counter (which was * already >= 10) and cause V(ready_group_prepared) to be called * more times than necessary. */ V(tour_has_begun); /* No need to wait for the customers to notice you're ready, * they'll figure it out eventually, right? Just run along, give * the tour, and head back once you're done... Rinse and repeat. */ } } /************************************************************************* * Common errors and their effect on scoring (total 15 p): * * - unnecessary code which does not work as expected, but "otherwise" * the program fulfills requirements - not considered an error -0p * - group size always exactly 10 - not considered an error -0p * - looping errors, other typo-like minor errors -1/2p (rounded down) * - while(true) missing from guide (not necessary in customer) OR * [i = 1 to N] missing from customer OR also included in guide without * really considering it in code OR processes called procedures -1p * - semaphores not initialized (and given values) -2p * - customer counter not brought to 0 (or similar) when tour starts -2p * - guide does something similar to * if(waiting_customers > 10) P(ready_group_prepared); which makes him * get to "keep" his permission to give a tour for the next round if * the group assembled before he arrived -2p * - "await", "wait" or other syntax not available here used instead * of synchronization with semaphores when it is needed OR otherwise * using a spinlock (while(something_not_done) do_nothing();) -3p * - a check similar to waiting_customers >= 10 instead of == 10 in the * example is done, which means that if 11 customers show up, the * guide gets 2 permissions to start a tour -3p * - only one V(tour_has_begun) or similar is ever given to visitors, * which means that 9 (or more) are left still waiting -3p * - mutual exclusion for protecting common variables is missing -6p * - like above, but only missing in some places -3p * - mutual exclusion semaphor is kept "closed" while going to wait for * a synchronization semaphor (P(mutex); .. P(tour_has_begun); V(mutex)), * causing a deadlock -5p * - other, single deadlocks (or repeated but very subtle, as in harder to * locate), "non-obvious" -3p * * If points go below 7, scoring is changed to a less algorithmic * "what level of understanding of a) mutual exclusion and b) * synchronization does this answer demonstrate", so these do not apply * anymore. * */ 0 xx 1 x 2 xxxxxxx 3 4 5 xxx 6 xxxx 7 xxxxxxxx 8 x 9 xx 10 x 11 xx 12 xxxxxxxxxxxx 13 xxxxx 14 x 15 xxxxxxxxxxxxxxx Keskiarvo 9.6 pistettä eli noin 64 % maksimipisteistä. =================================== Tehtävä 4 (Teemu Sideroff) ===================================== module BoundedBuffer op produce(typeT data); op consume() returns typeT; body sem mutex=1; sem empty = n, full = 0; typeT[M] buffer; int max=M; int rear=0, front=0; proc produce(data) { P(empty); P(mutex); buffer[rear] = data; rear = (rear+1) % M V(mutex); V(full); } proc consume() returns result { typeT result; P(full); P(mutex); result = buffer[front]; front = (front+1) % M; V(mutex); V(empty); } end BoundedBuffer; module Producer[i] # multiple producers, module consists only # of a single process now body process produce { while(..) { #... typeT d = ... ; # create data BoundedBuffer.produce(d); # and store it to the buffer #... } } end Producer module Consumer[i] # .. and consumers body process consume { while(...) { #... typeT data = call BoundedBuffer.consume(); # fetch data from buffer use_data(data); #... } } end Consumer Arvostelusta: Koska kokeen yhteydessä oli käytössä muistilappu, joka esittää RPC:n muodon, odotettiin vastausten myös noudattavan syntaksiltaan kurssilla, ja lapulla, käytettyä merkintätapaa. Ainakin siinä määrin, ettei ollut sekoittamisen vaaraa sanomanvälitykseen tai rendezvous:hun. Monien ratkaisussa ei oltu otettu minkäänlaista kantaa siihen, mitä tehdään jos puskuri täyttyy (tai on luettaessa tyhjä). Jos ratkaisussa tämä oli mahdollista, eikä asiaa edes kommentoitu millään lailla, rokotettiin luonnollisesti enemmän kuin niitä, jotka asiasta edes mainitsivat. Toinen yleinen virhe oli poissulkemisen huomiotta jättäminen. Arvostelu polarisoitui kovasti, joko asia osattiin, tai sitten yritettiin jotain ihan muuta. Tai jätettiin kokonaan yrittämättä, tehtävä ei ollut kovin suosittu. b) Synkronisointi on hankalampaa. Kommunikointi toimii nyt käyttäen kanavia. Palvelinprosessille voidaan lähettää viestit vaikkapa käyttäen yhtä kanavaa, jossa kerrotaan kenttien arvoina, onko kyseessä viesti tuottajalta vai kuluttajalta. Kuluttajalle palvelimen on lisäksi lähetettävä oikeaa kanavaa pitkin paluuarvo (jokaista kuluttajaa kohden oltava oma kanava). Synkronointi on kuitenkin ongelmallisempaa; mitä tehdä kun kuluttaja pyytää dataa, kun puskuri on tyhjä? Samaten, mitä tehdä kun tuottaja haluaa kirjoittaa, kun puskuri on täynnä? Tällöin puskuri voisi lähettää pyynnön lähettäjälle viestin, jossa ilmoitetaan yrittämään uudelleen. Nyt vaaditaan siis kanavat myös tuottajalle lähetettäviä paluuviestejä varten. Lisäksi tuottaja- ja kuluttajaprosessiin vaaditaan lisää toimintalogiikkaa RPC:tä käytettäessä riittävien kahden koodirivin sijasta. Toinen vaihtoehto olisi käyttää puskurinpalvelimessa kahta prosessia, toinen kuuntelemassa tuottajia ja toinen kuluttajia. Tässäkin ratkaisussa tarvitaan jokaiselle kuluttajalle oma kanava, johon palvelin voi lähettää kuluttajan pyytämää dataa. Lisäksi palvelimen prosessien on jollain tavalla synkronoitava puskurinkäyttö. Tuottajien ja kuluttajien koodi ei sitä vastoin vaadi tässä sen suurempia lisäyksiä. Arvostelusta: + esitetty oikein, että sanomanvälityksen käyttö tekee synkronoinnista hankalampaa + kanavien idea selitetty, selitetty mitä kanavia tarvitaan + jokin toimiva ratkaisuehdotelma, mitään valmista koodia ei vaadittu ------ Yksi ratkaisutapa (huom. mitään koodia ei tarvinnut ratkaisussa olla): chan req(string type, int id, typeT data); chan conf_c[n] (string status, typeT data); #channels for consumers chan conf_p[m] (string status); #for producers process palvelin { int front = 0, rear = 0, max = M, count = 0; dataT buffer[M]; while(true){ receive req(type,id,data) #producer wants to store, buffer not full if(type==produce && count < M) buffer[rear] = data; rear = (rear+1)%M; send conf_p[id]("ok"); c++; #producer wants to store, buffer full else if(type==produce && c == M) send conf_p[id]("retry later"); #consumer wants to read, buffer not empty else if(type==consume && c != 0) dataT d = buffer[front]; front = (front+1)%M; send conf_c[id]("ok",d); c--; #consumer wants to read, buffer empty else if(type==consume && c == 0) send conf_c[id]("retry later",null); } } process consumer[i=1 to M] { ### ok=false; while(!ok) { send req("consume",id,null); receive conf[i](msg, data); if(msg == "ok"); ok = true; else sleep(); # wait and try again } ### consume(data); ... } Tuottaja samaan tyyllin. Toinen tapa: sem mutex=1; sem full=n; sem empty=0; process intoBuffer { while(true) { receive produced(data); P(full); P(mutex); "buffer.insert(data);" #ks. tarkka kirjanpito edeltä V(mutex); V(empty); } } process fromBuffer { while(true) { receive consume(id); P(empty); P(mutex); data = "buffer.getdata();" #ks. tarkka kirjanpito edeltä V(mutex); V(full); send consumer[id](data); } } process consumer[i=0 to N] { ... send consume(i); receive[i](data); ... } process producer[i=0 to M] { ... data = cruchnumbers(); send produced(data); ... } ------------- Arvosanajakauma: 0 ***************** 1 ****** 2 ****** 3 ***** 4 **** 5 *** 6 **** 7 **** 8 *** 9 ** 11 *** 12 ** 14 *** 15 ** Keskiarvo 4.5 pistettä eli noin 30 % maksimipisteistä.