plik


ÿþ62 WgBb jzyka C IV. Programowanie wspóBbie|ne Jzyk C nie zawiera oczywi[cie |adnych mechanizmów umo|liwiajcych progra- mowanie wspóBbie|ne (np. takich, jak w jzyku Ada). W rozdziale niniejszym przedstawi implementacj moduBu umo|liwiajcego pseudo-wspóBbie|ne wyko- nywanie funkcji w jzyku C. Termin "pseudo-wspóBbie|no[" jest tutaj bardzo wa|ny, gdy| w |adnym wypadku zastosowane rozwizanie nie umo|liwia realiza- cji rzeczywistej wspóBbie|no[ci na maszynach wieloprocesorowych. Pomimo tego, dla zwikszenia czytelno[ci opisu, bd w dalszej jego cz[ci u|ywaB terminów "wspóBbie|ny" oraz "pseudo-wspóBbie|ny" wymiennie. Implementacja wspomnianego moduBu bdzie pretekstem do zastosowania wielu technik opisanych w poprzednich rozdziaBach tej ksi|ki. Dlatego te| przed przy- stpieniem do czytania tego rozdziaBu polecam przeczytanie rozdziaBów po- przednich. Z drugiej strony implementacja ta jest przykBadem niecodziennego stylu programowania w jzyku C, charakteryzujcego si bardzo intensywnym u|yciem preprocesora. WspóBbie|ne wykonywanie funkcji nie bdzie realizowane na poziomie systemu operacyjnego lecz na poziomie programu w C, i bdzie ono wykonane z wykorzystaniem jedynie elementów jzyka standardowego. Oznacza to midzy innymi, |e moduB bdzie przeno[ny zarówno na ró|ne kompilatory (nale|y jed- nak ostro|nie stosowa opcje optymalizacji) jak i ró|ne platformy sprztowe. In- n konsekwencj realizacji przeBczania zadaD caBkowicie na poziomie jzyka C jest "gruboziarnisto[" zrealizowanej pseudo-wspóBbie|no[ci. Je|eli dwa (lub wicej) zadania (funkcje, programy) maj by wykonywane w sposób wspóB- bie|ny na jednym procesorze, to w rzeczywisto[ci na zmian wykonywane s pewne maBe fragmenty tych zadaD. Najmniejsz tak czstk mo|e by instruk- cja procesora, która nie mo|e ju| by podzielona. Takie rozwizanie byBoby pseudo-wspóBbie|no[ci "drobnoziarnist" i gwarantowaBoby maxymalne zBu- dzenie rzeczywistej wspóBbie|no[ci. W przedstawionym poni|ej module naj- mniejsz cz[ci funkcji, która musi by wykonana, zanim sterowanie zostanie przekazane do innej funkcji, jest jedna instrukcja (ang. statement) jzyka C. 1.Dlaczego wspóBbie|no[? Klasyczne programy wspóBbie|ne s wykonywane na maszynach wieloprocesoro- wych. Celem zastosowania równolegBych komputerów i równolegBych programów jest zmniejszenie zBo|ono[ci czasowej rozwizywanego zadania. Jest spraw oczywist, |e w przypadku programu wykonywanego pseudo-wspóBbie|nie na komputerze jednoprocesorowym nie mo|na liczy na zwikszenie prdko[ci obli- czeD. Co wicej, wykonanie w takim przypadku kilku zadaD musi trwa dBu|ej ni| trwaBoby wykonanie tych zadaD sekwencyjnie jedno po drugim. Dzieje si tak dlatego, |e oprócz kodu zadaD procesor musi wykonywa pewien kod zwizany IV Programowanie wspóBbie|ne 63 z ich przeBczaniem. Mo|na by w takim razie powiedzie, |e pseudo- wspóBbie|no[ jest sztuk dla sztuki. Nie jest to prawd, a najlepszym na to dowo- dem jest popularno[ programów typu DESQview czy Windows, umo|liwiajcych pseudowspóBbie|ne wykonywanie programów. Twierdzenie, |e wielozadaniowo[ realizowana na jednym procesorze nie mo|e przynie[ zysków czasowych jest prawd dopóty, dopóki zadania caBy czas wymagaj pracy procesora. W rzeczywisto[ci czste s sytuacje, gdy wikszo[ czasu pracy zadania nie jest zu|ywana na prac procesora. Na przykBad operacje na pamici zewntrznej s zwykle na tyle wolne w porównaniu z szybko[ci procesora, |e mógBby on równo- cze[nie wykonywa inn prac. Jeszcze bardziej skrajnym przypadkiem jest cze- kanie przez zadanie na dane wprowadzane przez u|ytkownika z klawiatury. Je|eli jedno z zadaD utknBo w takim wskim gardle, procesor mo|e po[wici swój czas na wykonanie innych zadaD. Mo|na to zrealizowa wBa[nie poprzez pseudo- wspóBbie|no[. Zyskiwanie czasu w takich sytuacjach nie jest jednak jedynym motywem zasto- sowania wielozadaniowo[ci. Programy wykonujce kilka zadaD na raz mog by bardzo wygodne dla u|ytkownika. PrzykBadem niech bdzie edytor tekstów zapi- sujcy co jaki[ czas redagowany tekst "w tle". Jak ju| wcze[niej wspomniaBem, realizacja wspóBbie|nego wykonywania funkcji bdzie polegaBa na wykonywaniu na zmian kolejnych fragmentów ka|dej z funkcji. Do przekazywania sterowania z jednej funkcji do drugiej posBu| nam funkcje setjmp i longjmp. 2.Funkcje setjmp i longjmp Deklaracje tych funkcji wygldaj nastpujco: int setjmp(jmp_buf); void longjmp(jmp_buf,int); S one funkcjami standardowymi. Ich deklaracje, oraz deklaracja typu jmp_buf znajduj si w pliku nagBówkowym "setjmp.h". SBu| one do zapamitania, a nastpnie odtworzenia stanu programu. W praktyce oznacza to, |e funkcja lon- gjmp umo|liwia wykonanie skoku do jakiego[ miejsca, w którym stan programu zostaB wcze[niej zapamitany przy pomocy funkcji setjmp. Jak wskazuje sama nazwa funkcji, jest to skok daleki, nie ograniczony do wntrza funkcji (jak skok przy pomocy instrukcji goto . jmp_buf definiuje struktur, w której prze- chowywane s informacje o stanie programu. WywoBanie funkcji setjmp powoduje zapamitanie w zmiennej typu jmp_buf, przekazanej do niej jako argument, informacji o stanie programu. Funkcja zwraca warto[ 0. Funkcj longjmp wywoBuje si z dwoma argumentami. Pierwszym jest zmienna, w której wcze[niej zapamitano stan programu przy pomocy funkcji setjmp. Drugi argument jest liczb caBkowit. WywoBanie funkcji longjmp powoduje 64 WgBb jzyka C odtworzenie stanu programu jaki zostaB zapamitany w zmiennej typu jmp_buf przekazanej jako pierwszy argument. W wyniku takiego wywoBania funkcji lon- gjmp program znajduje si w punkcie powrotu z funkcji setjmp (bo w takim momencie zostaB zapamitany stan programu), przy czym warto[ zwracana przez funkcj setjmp jest równa drugiemu argumentowi funkcji longjmp (lub 1 je|eli drugi argument byB równy 0). Na podstawie warto[ci funkcji setjmp, program jest w stanie odró|ni czy zostaBa ona normalnie wywoBana w wyniku zinterpreto- wania kolejnej instrukcji, czy te| nastpiB skok przy pomocy funkcji longjmp. DziaBanie funkcji setjmp i longjmp jest czasami trudne do zrozumienia. Poni|- szy przykBad powinien wyja[ni niejsno[ci. if(setjmp(buf)) { /* cig instrukcji */ } /* ... */ longjmp(buf,3); Po wywoBaniu funkcji setjmp warunek nie bdzie speBniony (setjmp zwróci warto[ 0) i cig instrukcji nie zostanie wykonany. W efekcie wywoBania funkcji longjmp w innej cz[ci programu, sterowanie zostanie przekazane do miejsca powrotu z funkcji setjmp, ale tym razem zostanie zwrócona warto[ równa 3, a wic warunek bdzie speBniony i cig instrukcji zostanie wykonany. Poniewa| po "powrocie" funkcja setjmp zwraca warto[ przekazan jako argu- ment funkcji longjmp, nic nie stoi na przeszkodzie, |eby przy pomocy ró|nych funkcji longjmp przekazywa ró|ne warto[ci i na ich postawie identyfikowa miejsce, z którego nastpiB daleki skok, na przykBad przy pomocy instrukcji swi- tch switch(setjmp(buf)) { case 1 : /* z punktu 1 */ break; case 2 : /* z punktu 2 */ break; case 3 : /* z punktu 3 */ break; } 3.PrzeBczanie zadaD Zastanówmy si na pocztek, w jaki sposób dokona przeBczenia procesora pomidzy dwiema funkcjami. Jak wcze[niej napisaBem, u|yjemy pary funkcji setjmp i longjmp do wykonania dalekich skoków pomidzy procesami (funkcjami). Dla ka|dego procesu bdziemy potrzebowa jednej zmiennej typu jmp_buf sBu|cej do zapamitania stanu programu w momencie przekazania sterowania do drugiego procesu. Obie zmienne mu- sz by globalne, aby obie funkcje mogBy si do nich odwoBa. jmp_buf buf1,buf2; W celu wykonania skoku do funkcji f1 bdziemy u|ywa wywoBania IV Programowanie wspóBbie|ne 65 longjmp(buf1,1); Analogicznie, |eby skoczy do funkcji f2 longjmp(buf2,1); Przed wykonaniem skoku do drugiego procesu, ka|da funkcja musi wywoBa funkcj setjmp(buf). Zapamitane przez t funkcj informacje o stanie programu bd mogBy by w przyszBo[ci wykorzystane do powrotu do miejsca, w którym dziaBanie funkcji zostaBo zawieszone. Tak wic sekwencje przeBczajce zadania bd wygldaBy mniej wicej tak: if(setjmp(buf1)==0)longjmp(buf2,1); /* w funkcji f1 */ if(setjmp(buf2)==0)longjmp(buf1,1); /* w funkcji f2 */ Funkcja longjmp zostanie wywoBana tylko wtedy, gdy setjmp zwróci warto[ ze- ro. Nastpi to wic po wywoBaniu setjmp w celu zapamitania kontekstu programu, a nie nastpi po powrocie w to miejsce przy pomocy dalekiego skoku. Spróbujmy teraz uogólni to rozwizanie na nieznan z góry ilo[ procesów. Trzeba zdefiniowa jak[ struktur danych, która zapewniaBaby istnienie jednego bufora typu jmp_buf dla ka|dej funkcji, a tak|e umo|liwiaBaby okre[lenie jaka jest nastpna funkcja w "BaDcuszku". struct el { jmp_buf buf; struct el *next; }; Ka|d funkcja bdzie posiadaBa wBasny element typu struct el. W polu buf tego elementu bdzie zapamitywany kontekst tej funkcji w chwili przeBczania stero- wania do kolejnego zadania. Pole next struktury bdzie wskazywaBo element typu struct el skojarzony z funkcj, do której ma by przekazane sterowanie. W ten spo- sób powstanie zaptlona lista o wzBach typu struct el. Lista jest jednokierunkowa, gdy| ka|dy jej element zawiera tylko pole wskazujce nastpny element. Do peBnej manipulacji list jednokierunkow (w tym do usuwania elementów z listy) po- trzebne s co najmniej dwie zmienne, wskazujce na dwa kolejne wzBy listy: struct el *cur,*last; Zmienna cur bdzie zawsze wskazywa na wzeB odpowiadajcy aktywnej funkcji, za[ zmienna last - na wzeB odpowiadajcy poprzedniej funkcji. Sekwencja przeB- czania zadaD zapisana przy u|yciu tych zmiennych bdzie wygldaBa nastpujco: if(setjmp(cur->buf)==0) longjmp((last=cur,cur=(cur->next))->buf,1); Argumentem funkcji longjmp jest do[ skomplikowane wyra|enie: (last=cur,cur=(cur->next))->buf Analiza tego wyra|enia rozpocznie si od przypisania zmiennej last warto[ci zmiennej cur. Nastpnie zmiennej cur jest przypisywany wskaznik do nastpnego 66 WgBb jzyka C wzBa listy. Pole buf tego wzBa zostaje argumentem funkcji longjmp (zostanie wykonany skok do nastpnej funkcji). Pola buf w li[cie musz by zainicjowane przed pierwszym wywoBaniem sekwen- cji przeBczajcej zadania. {eby to osign, umie[cimy na pocztku ka|dej funk- cji nastpujcy warunek: if(setjmp(cur->buf)==0)return; Je|eli funkcja zostanie wywoBana, to jej kontekst zostanie zapamitany w polu cur->buf i nastpi od razu powrót do funkcji wywoBujcej. Pozostaje jeszcze zastanowi si, co zrobi po zakoDczeniu dziaBania procesu. Trzeba go oczywi[cie usun z listy, |eby nie byB wicej wykonywany. Doko- nuje si tego instrukcj cur=last->next=cur->next; W tym miejscu przydaje si zadeklarowana wcze[niej na wyrost zmienna last. Na- stpnie trzeba przekaza sterowanie do kolejnego procesu: longjmp(cur->buf,1); 4.Zapis praktyczny Przedstawione powy|ej konstrukcje robi dobry u|ytek z funkcji setjmp i longjmp umo|liwiajc przeBczanie funkcji - zadaD, ale w |adnym wypadku nie nadaj si do praktycznego zastosowania w programowaniu. Stosujc definicje preprocesora mo|na zapisa te sekwencje w sposób du|o czytelniejszy. ZaBó|my, |e zapis ten musi speBnia nastpujce warunki: K zamiana napisanej i uruchomionej wcze[niej funkcji na posta, w której mogBaby ona by wykonywana wspóBbie|nie, musi by prosta, prawie automatyczna, K funkcja przystosowana do wykonywania wspóBbie|nego powinna dalej móc by wywoBywana w normalny sposób, K je|eli funkcja jest ostatnim procesem (wszystkie inne zakoDczyBy ju| dziaBa- nie) to nie powinna by wykonywana sekwencja przeBczania zadaD. Pierwszym krokiem bdzie zastpienie omówionych w poprzednim podrozdziale sekwencji odpowiednimi definicjami preprocesora: #define BEGIN if(setjmp(cur->buf)==0)return; #define END cur=last->next=cur->next; \ longjmp(cur->buf,1); #define _ if(setjmp(cur->buf)==0) \ longjmp((last=cur,cur=(cur->next))->buf,1); IV Programowanie wspóBbie|ne 67 Makrodefinicje BEGIN oraz END bd umieszczane odpowiednio na pocztku i na koDcu funkcji. Ostatnie makro jest wBa[ciw sekwencj przeBczajc zadania. IdeaBem byBaby sytuacja gdyby to makro w ogóle nie byBo widoczne, dlatego te| wybraBem dla niego nazw jak najmniej rzucajc si w oczy: _ (podkre[lenie). Jak ju| wcze[niej ustalili[my, wszystkie makrodefinicje maj by "przezroczy- ste" gdy funkcja zostanie wywoBana w normalny sposób. Do rozró|niania czy funkcja jest wykonywana wspóBbie|nie czy nie, u|yjemy lokalnej zmiennej is_a_process. Zmienna ta bdzie miaBa warto[ 1 tylko wtedy, gdy funkcja zostanie wywoBana przez funkcj inicjujc procesy. Definicje rozbudowane o sprawdzanie, czy funkcja jest wykonywana wspóBbie|- nie wygldaj nastpujco: #define BEGIN { \ static char is_a_process; \ if((is_a_process=be_a_process)!=0) \ if(setjmp(cur->buf)==0)return; #define END if(is_a_process!=0) \ {\ cur=last->next=cur->next; \ longjmp(cur->buf,1); \ }\ } /* zamyka nawias otwarty w BEGIN */ #define _ if(is_a_process!=0) \ if(setjmp(cur->buf)==0) \ longjmp((last=cur,cur=(cur->next))->buf,1); Zmienna be_a_process jest zmienn globaln. Jej warto[ wynosi caBy czas zero i jest ustawiana na jeden przez funkcj inicjujc procesy na czas inicjujcego wywoBania procesu. Funkcja inicjujca przydziela pami na struktur struct el dla procesu i umieszcza j w li[cie. void proces(FUNCTION f) { struct el *tmp; _number++ ; tmp=malloc(sizeof(struct el)); if(cur) {tmp->next=cur->next; cur->next=tmp; } else {cur=tmp; cur->next=tmp;} last=cur; cur=tmp; be_a_process=1; (*f)(); be_a_process=0; } FUNCTION jest typem wskazanie do funkcji typu void: typedef void (*FUNCTION)(void); 68 WgBb jzyka C W funkcji proces wystpuje globalna zmienna _number. Jak wskazuje nazwa, jej warto[ bdzie okre[laBa liczb "|ywych" procesów. Proces bdzie jej u|ywaB do sprawdzenia czy nie jest ju| przypadkiem ostatnim |ywym procesem. Po zainicjowaniu wszystkich procesów mo|na rozpocz ich wspóBbie|ne wy- konywanie. W tym celu zdefiniujemy makro RUN: #define RUN if(setjmp(_the_end)==0) \ longjmp((cur=cur->next)->buf,1); Globalna zmienna _the_end sBu|y do zapamitania punktu, z którego zostaBo wy- woBane wspóBbie|ne wykonywanie procesów, i do którego nale|y wróci gdy wszystkie procesy zakoDcz dziaBanie. Wyja[nienia wymaga chyba jeszcze wyra|enie w funkcji longjmp: (cur=cur->next)->buf Zapewnia ono rozpoczcie pseudo-wspóBbie|nego wykonywania funkcji od tej, która zostaBa jako pierwsza zadeklarowana za pomoc funkcji proces. Po wprowadzeniu dwóch dodatkowych zmiennych globalnych, _number i _the_end, wcze[niejsze definicje bd wygldaBy nastpujco: #define BEGIN { \ static char is_a_process; \ if((is_a_process=be_a_process)!=0) \ if(setjmp(cur->buf)==0)return; #define END if(is_a_process!=0) \ if(-- _number!=0) \ {\ cur=last->next=cur->next; \ longjmp(cur->buf,1); \ }\ else \ longjmp(_the_end,1); \ } /* zamyka nawias otwarty w BEGIN */ #define _ if(is_a_process!=0 && _number>0 ) \ if(setjmp(cur->buf)==0) \ longjmp((last=cur,cur=(cur->next))->buf,1); Poni|ej znajduje si peBna zawarto[ plików proces.h i proces.c zawieraj- cych wszystkie opisane powy|ej definicje, a tak|e definicje i deklaracje wszyst- kich u|ywanych zmiennych. Dodatkowo w pliku proces.h zostaBo zdefiniowane makro ABORT, powodujce zakoDczenie wszystkich procesów. IV Programowanie wspóBbie|ne 69 /* plik proces.c Adam Sapek */ #include "proces.h" struct el *cur=NULL,*last=NULL; /* definicje zmiennych globalnych */ jmp_buf _the_end; unsigned char _number=0,be_a_process=0; void proces(FUNCTION f) /* funkcja inicjujca proces */ { struct el *tmp; _number++ ; tmp=malloc(sizeof(struct el)); /* przydziel pami */ if(cur!=NULL) {tmp->next=cur->next; cur->next=tmp; } /* dopisz do kolejki */ else {cur=tmp; cur->next=tmp;} /* pierwszy proces -> stwórz kolejk */ last=cur; cur=tmp; be_a_process=1; (*f)(); /* inicjuj funkcj jako proces */ be_a_process=0; } /* plik proces.h Adam Sapek */ #ifndef __proces_h #define __proces_h #include <setjmp.h> /* longjmp i setjmp */ #include <stdlib.h> /* malloc */ #include <stdio.h> /* NULL */ /* makro BEGIN jest umieszczane na pocztku funkcji/procesu */ #define BEGIN { static char is_a_process; \ if((is_a_process=be_a_process)!=0) \ if(setjmp(cur->buf)==0)return; /* makro END jest umieszczane na koDcu funkcji/procesu */ #define END if(is_a_process!=0) \ if(-- _number!=0) \ {\ cur=last->next=cur->next; \ longjmp(cur->buf,1); \ }\ else \ longjmp(_the_end,1); \ } /* zamyka nawias otwarty w BEGIN */ /* makro _ powoduje przekazanie sterowania do nastpnego procesu w kolejce */ #define _ if( is_a_process!=0 && _number>0 ) \ if(setjmp(cur->buf)==0) \ 70 WgBb jzyka C longjmp((last=cur,cur=(cur->next))->buf,1); /* makro RUN rozpoczyna wspólbie|ne wykonywanie procesów */ #define RUN if(setjmp(_the_end)==0)longjmp((cur=cur->next)->buf,1); /* makro ABORT powoduje natychmiastowe przerwanie WSZYSTKICH procesów */ #define ABORT if(is_a_process!=0) \ {_number=0; longjmp(_the_end,1);} /* deklaracje obiektów zewntrznych */ typedef void (*FUNCTION)(void); struct el { jmp_buf buf; struct el *next; }; extern unsigned char _number,be_a_process; extern jmp_buf _the_end; extern struct el *cur,*last; extern void proces(FUNCTION); #endif 5.Program wspóBbie|ny Na pocztek wypada poda kilka ogólnych zasad stosowania zdefiniowanych na- rzdzi. Piszc program wspóBbie|ny najlepiej napisa i uruchomi osobno ka|d funkcj, która ma by w programie procesem. Przystosowanie napisanej wcze[niej funkcji do pracy wspóBbie|nej jest bardzo Batwe i ogranicza do zmian "kosmetycz- nych". Po pierwsze na pocztku funkcji, po deklaracjach zmiennych, nale|y umie- [ci makro BEGIN. Drugie makro, END, umieszcza si na koDcu funkcji. Teoretycznie ani makro BEGIN nie musi znajdowa si na samym pocztku funk- cji, ani makro END na samym koDcu, wymagane jest tylko, aby makro BEGIN poprzedzaBo makro END. Umieszczajc wspomniane makra w innym miejscu na- le|y jednak pamita, |e tylko ta cz[ funkcji, która jest zawarta midzy nimi, b- dzie wykonywana w pracy wspóBbie|nej. Kod poprzedzajcy makro BEGIN wykona si tylko podczas inicjowania procesu funkcj proces, a kod nastpujcy za makrem END nie wykona si w ogóle. Makro o nazwie _ (podkre[lenie) umieszcza si w tych punktach, w których pro- ces ma by przerywany, a sterowanie ma by przekazywane do innych proce- sów. {eby uzyska najlepsz wspóBbie|no[, najlepiej dopisa to makro do ka|dego [rednika koDczcego instrukcj. Nie zawsze takie rozwizanie jest naj- lepsze. Sekwencja przekazania sterowania (makro _) ma swoje koszty, zarówno czasowe jak i pamiciowe. Umieszczajc to makro inteligentnie w ka|dym pro- cesie mo|na w du|ym stopniu sterowa dziaBaniem programu. Mo|na w ten spo- sób na przykBad uprzywilejowa niektóre procesy lub ich fragmenty, IV Programowanie wspóBbie|ne 71 uniemo|liwi przerwanie krytycznych sekcji w procesach. Niektóre zasady wy- boru lokalizacji makra _ zostan opisane w dalszej cz[ci tego rozdziaBu, przy opisie przykBadowego programu. Najbardziej zdradzieckim bBdem, jaki mo|na popeBni w programie wspóBbie|- nym, jest u|ycie w procesie zmiennej automatycznej. Pami na zmienne auto- matyczne jest przydzielana po wywoBaniu funkcji i zwalniania po jej zakoDczeniu. Z tego powodu zmienne automatyczne ró|nych procesów mog znalez si w tym samym obszarze pamici (i zwykle tak si wBa[nie dzieje). BBdy powstaBe "dziki temu" mog by bardzo trudne do wykrycia nawet przy pomocy debuggera. Dlatego nale|y bezwzgldnie pamita o zamienieniu wszystkich zmiennych automatycznych w funkcjach/procesach na zmienne sta- tyczne. Jest to wBa[ciwie jedyna zmiana merytoryczna jakiej nale|y dokona w funkcji, |eby mogBa sta si procesem wspóBbie|nym. Programy wspóBbie|ne nale|y konsolidowa z moduBem proces.obj powsta- Bym po skompilowaniu pliku proces.c. Mo|na to zrobi na wiele sposobów, np.: K utworzy w [rodowisku Turbo C projekt o zawarto[ci: program.c proces.c K u|y bezpo[rednio kompilatora wsadowego Microsoft C poprzez wy- woBanie: cl program.c proces.c PrzykBadowy program Program demonstracyjny bdzie skBadaB si z dwóch procesów. Jeden z nich b- dzie wczytywaB kolejne bajty z podanego pliku i zliczaB caBkowit liczb bitów równych jeden w pliku. Drugi proces na czas pracy pierwszego wygasi ekran i bdzie wy[wietlaB na nim chodzcego "w|a". Po przeczytaniu caBego pliku proces pierwszy przerwie dziaBanie obydwu procesów i wróci do programu gBównego, który wy[wietli liczb jedynek w pliku. W wersji klasycznej funkcja zliczajca liczb jedynek w pliku mogBaby wygl- da na przykBad tak: void bits(void) { FILE *in; unsigned c; char name[80]; printf("\nNazwa pliku : "); scanf("%s",name); if(NULL!=(in=fopen(name,"rb"))) while(!feof(in)) { c=getc(in); 72 WgBb jzyka C while(c){ sum+=c&1; c/=2; } } } Funkcja ta pyta o nazw pliku i próbuje otworzy do czytania w trybie binarnym plik o podanej nazwie. Je|eli plik uda si otworzy, funkcja wczytuje kolejno wszystkie bajty i zlicza kolejne jedynki. Gdzie umie[ci poszczególne makra, |eby przystosowa nasz funkcj do pracy wspóBbie|nej ? Makro END znajdzie si w standardowym miejscu, na koDcu funkcji. Makro BEGIN mo|na umie[ci albo bezpo[rednio po definicjach zmiennych, albo dopie- ro po wczytaniu nazwy pliku. Wydaje si, |e lepiej jest umie[ci je w tym drugim miejscu. Pytanie o nazw pliku pojawi si wtedy w czasie inicjowania procesu funkcj proces. Prosz zauwa|y, |e nie mo|na makra BEGIN przesun jeszcze dalej, za warunek if. Gdyby nie udaBo si otworzy pliku, makro BEGIN w ogóle nie zostaBoby wykonane, a funkcja nie zostaBaby poprawnie zainicjowana jako pro- ces. Normalnie, zakoDczenie dziaBania jednego procesu wspóBbie|nego nie powoduje przerwania dziaBania pozostaBych; proces zakoDczony jest po prostu wyBczany z kolejki. W naszym programie proces czytajcy plik jest procesem nadrzdnym. Natychmiast po przeczytaniu caBego pliku obydwa procesy powinny by prze- rwane, a program gBówny powinien wypisa wynik. W takim celu zostaBo w pliku proces.h zdefiniowane makro ABORT. Powoduje ono natychmiastowe przerwanie wszystkich zadaD i skok do miejsca, z którego zostaBy uruchomione makrem RUN. Makro ABORT nale|y umie[ci bezpo[rednio przed makrem END. Kolejn zmian jest nadanie zmiennym lokalnym klasy static. Wystarczy w tym celu przed definicjami umie[ci sBowo kluczowe static. Na koniec zostawiBem spraw najwa|niejsz - makro _ (podkre[lenie). W tej funkcji istniej dwa potencjalne miejsca, w których mo|na je umie[ci: ptla gBówna i ptla testujca kolejne bity wczytanego bajtu. Zale|y nam oczywi[cie, |eby funkcja czytajca plik nie zostaBa zbytnio spowolniona, a wic najlepiej jest umie[ci makro _ tylko w ptli gBównej. Powstaje pytanie, czy wystarczy to, |e- by drugi proces mógB dziaBa pBynnie. Jedyn odpowiedz mo|na uzyska metod do[wiadczaln, uruchamiajc program. StwierdziBem, |e umieszczenie makra tylko w ptli gBównej jest zupeBnie wystarczajce. Funkcja bits wyglda po zmianach nastpujco: void bits(void) { static FILE *in; static unsigned c; static char name[80]; IV Programowanie wspóBbie|ne 73 printf("\nNazwa pliku : "); scanf("%s",name); BEGIN if(NULL!=(in=fopen(name,"rb"))) while(!feof(in)) { c=getc(in); _ while(c){ sum+=c&1; c/=2; } } ABORT END } Zadaniem drugiego procesu, jak wcze[niej napisaBem, bdzie wygaszenie ekranu na czas dziaBania procesu czytajcego plik. Proces ten bdzie przykBadem adap- tacji do pracy wspóBbie|nej funkcji napisanej w zupeBnie innym celu. Zostanie tu u|yta oryginalna funkcja z rezydentnego programu Screen Saver, zaprezentowa- nego w rozdziale pitym tej ksi|ki. W celu zwikszenia czytelno[ci, w funkcji tej dokonaBem kilku uproszczeD, a tak|e zamieniBem bezpo[rednie odwoBania do pamici video na odpowiednie funkcje biblioteczne. Poniewa| funkcje te nie s zestandaryzowane, ich wersje dla ró|nych kompilatorów nieco si ró|ni. Poni- |ej przedstawi wersj dla kompilatora Turbo C firmy Borland, a na doBczonej do ksi|ki dyskietce znajduje si tak|e wersja dla kompilatora Microsoft C 5.1. Proces wygaszajcy ekran jest przerywany przez proces pierwszy, gdy ten za- koDczy swoj prac, dlatego te| jego gBówna ptla jest nieskoDczona. Skoro tak, to wydaje si, |e umieszczanie makra END na koDcu funkcji, w miejscu, w którym nigdy nie zostanie wykonane, mija si z celem. Prosz jednak jeszcze raz spojrze na definicje: makra BEGIN i END stanowi nierozBczn par. W definicji makra END znajduje si nawias zamykajcy instrukcj blokow otwart w definicji BEGIN. W tym miejscu dochodzimy znowu do punktu zasadniczego: gdzie umie[ci ma- kro _. Prosz spojrze na funkcj ekr i spróbowa samemu wybra najodpowied- niejsze miejsce. void scr(void) { static unsigned direct=3,y,x,a; BEGIN; clrscr(); while(1) { do{ if(rand()%11==0)direct=rand()%8; x=snake[0].x+dx[direct]; y=snake[0].y+dy[direct]; } while(x<1||x>=80||y<1||y>25); gotoxy(x,y); printf("ˆ%ˆ%"); gotoxy(snake[0].x,snake[0].y); printf("“%“%"); gotoxy(snake[1].x,snake[1].y); printf("’%’%"); gotoxy(snake[2].x,snake[2].y); printf("‘%‘%"); gotoxy(snake[3].x,snake[3].y); printf(" "); for(a=3;a>=1;a--) { 74 WgBb jzyka C snake[a].x=snake[a-1].x; snake[a].y=snake[a-1].y; } snake[0].x=x; snake[0].y=y; a=*(char far *)0x46c; while(*(char far *)0x46c-a<7); } END; } WBa[ciwie umieszczenie tego makra obok ka|dego [rednika nie szkodzi niczym oprócz niepotrzebnego zwikszenia programu i zmniejszenia czytelno[ci zapisu. Kluczem do poprawnego umieszczenia sekwencji przeBczajcej zadania jest spo- strze|enie, |e funkcja scr praktycznie caBy czas spdza w ptli opózniajcej (pro- sz spróbowa uruchomi t funkcj bez tej ptli !): while(*(char far *)0x46c-a<7); Teraz jest ju| chyba jasne, |e wystarczy umie[ci makro _ (podkre[lenie) w tej ptli, i to nie obok [rednika, tylko zamiast niego: while(*(char far *)0x46c-a<7)_ Poni|ej przedstawiony jest kompletny program. /* plik bity.c */ #include "proces.h" #include <conio.h> unsigned long sum=0; void bits(void) /* funkcja zlicza ilo[ jedynek w bajtach pliku */ { static FILE *in; static unsigned c; static char name[80]; printf("\nNazwa pliku : "); scanf("%s",name); /* wczytaj nazw pliku */ BEGIN /* pocztek czsci wspóBbie|nej */ if(NULL!=(in=fopen(name,"rb"))) while(!feof(in)) { c=getc(in); _ /* wczytaj kolejny bajt z pliku */ while(c){ sum+=c&1; c/=2; } /* policz jedynki w tym bajcie */ } ABORT /* przerwij procesy wspóBbie|ne */ END } struct { /* struktura reprezentuje jednen*/ IV Programowanie wspóBbie|ne 75 unsigned char x,y; /* czBon w|a */ }snake[4]={{4,4},{3,3},{2,2},{1,1}}; /* w| ma 4 czBony */ void scr(void) /* funkcja wygasza ekran i wy[wietla chodzcego w|a */ { static unsigned direct=3,y,x,a; static char dx[8]={0,1,2,1,0,-1,-2,-1}, dy[8]={-1,-1,0,1,1,1,0,-1}; BEGIN; clrscr(); while(1) { do{ if(rand()%11==0) /* [rednio co 11 kroków */ direct=rand()%8; /* losuj nowy kierunek w|a */ x=snake[0].x+dx[direct]; y=snake[0].y+dy[direct]; }while(x<1||x>=80||y<1||y>25); /* czy mo|na w tym kierunku ? */ gotoxy(x,y); printf("ˆ%ˆ%"); /* rysuj w|a*/ gotoxy(snake[0].x,snake[0].y); printf("“%“%"); gotoxy(snake[1].x,snake[1].y); printf("’%’%"); gotoxy(snake[2].x,snake[2].y); printf("‘%‘%"); gotoxy(snake[3].x,snake[3].y); printf(" "); for(a=3;a>=1;a--) /* przesuD w|a*/ { snake[a].x=snake[a-1].x; snake[a].y=snake[a-1].y; } snake[0].x=x; snake[0].y=y; a=*(char far *)0x46c; while(*(char far *)0x46c-a<7) _ /* odczekaj 7 taktów zegara */ } END; } void main() { clrscr(); /* wyczy[ ekran */ proces(scr); /* inicjuj procesy */ proces(bits); RUN; /* uruchom procesy */ clrscr(); printf("W pliku znaleziono %lu jedynek",sum); /* wypisz wynik */ } 6.Komunikacja midzy procesami Najprostszym sposobem komunikacji midzy procesami wspóBbie|nymi jest zasto- sowanie zmiennych globalnych. Metoda ta jest tyle| prosta, co prymitywna. Nie 76 WgBb jzyka C zapewnia ona synchronizacji: proces, który ma czyta jak[ dan ze zmiennej glo- balnej, nie wie, czy zostaBa ona tam ju| zapisana. Podobnie, proces zapisujcy dan nie wie, czy poprzednia warto[ zostaBa ju| pobrana. Dopisanie odpowiednich wskazników synchronizujcych mo|e na tyle zaciemni zapis, |e warto posBu|y si preprocesorem w celu implementacji w miar ogólnej metody komunikacji midzy procesami. Dziki ukryciu szczegóBów w definicjach procesora i zadeklarowaniu funkcji, zmiennych i struktur w osobnym pliku mo|na uzyska bardzo Batwe w u|yciu i elegancko wygldajce narzdzia. Do komunikacji midzy procesami u|yjemy skrytki typu FIFO (ang. First In First Out). Procesy "producenci" bd deponowa w skrytce dane okre[lonego typu, a procesy "konsumenci" bd je z niej pobiera. Je|eli skrytka bdzie pu- sta, konsumenci bd czeka na dane, je|eli za[ bdzie peBna, producenci bd czeka na wolne miejsce. Niezbdne jest, aby oczekiwanie którego[ procesu (czy to konsumenta, czy producenta) nie powodowaBo zawieszenia pozostaBych pro- cesów. W przeciwnym przypadku czekajcy proces nigdy nie doczekaBby si zmiany stanu skrytki. Typ danych przechowywanych w skrytce (w C++ mo|e to by nie tylko typ standardowy, ale tak|e klasa) oraz rozmiar skrytki bdzie mógB by definiowany. W przypadku niezdefiniowania typu i/lub rozmiaru bd przyjmowane warto[ci domy[lne: typ int i rozmiar równy 100. Skrytka bdzie struktur zdefiniowan jak poni|ej: struct { unsigned long pocz,koniec; typ wnetrze[rozmiar]; }_skrytka; Pole pocz bdzie wskazywaBo nastpny element mo|liwy do pobrania, a pole ko- niec, pierwsze wolne miejsce w skrytce. Je|eli skrytka bdzie pusta, warto[ pola pocz bdzie równa warto[ci pola koniec. Je|eli skrytka bdzie peBna, zajdzie waru- nek: _skrytka.koniec-_skrytka.pocz==rozmiar Warunki sprawdzajce, czy skrytka jest peBna czy pusta, mog przyda si w programowaniu, dlatego zdefiniujemy je w postaci identyfikatorów preprocesora: #define PELNA (_skrytka.pocz-_skrytka.koniec==rozmiar) #define PUSTA (_skrytka.pocz==_skrytka.koniec) Funkcje umieszczajca i usuwajca dan ze skrytki s bardzo proste i nie wyma- gaj chyba komentarza: _umiesc(typ x) { if(!PELNA) { IV Programowanie wspóBbie|ne 77 _skrytka.wnetrze[(_skrytka.pocz++)%rozmiar]=x; return 0; } return 1; } _usun(typ *x) { if(!PUSTA) { *x=_skrytka.wnetrze[(_skrytka.koniec++)%rozmiar]; return 0; } return 1; } S to funkcje wewntrzne dla moduBu komunikacji midzy procesami; dla pro- gramisty przygotujemy wygodniejsze narzdzie. Jak wida, funkcje _umiesc i _usun zwracaj warto[ 0 w przypadku sukcesu i warto[ 1 w przypadku nie- powodzenia. Wykorzystujc t wBasno[ zdefiniujemy makrodefinicje czekajce w razie potrzeby na dane lub wolne miejsce w skrytce, ale nie zawieszajce przy tym pozostaBych procesów. #define put(x) while(_umiesc(x))_ #define get(x) while(_usun(&x))_ Makrodefinicje get i put czekaj w ptli while a| odpowiednia funkcja zwróci warto[ zero, co [wiadczy o pomy[lnym zapisie do skrzynki (put) lub odczycie ze skrytki (get). Jednocze[nie w ka|dym obiegu ptli, wywoBywane jest makro _ (podkre[lenie), dziki czemu pozostaBe procesy mog normalnie pracowa. Na koniec dygresja dotyczca u|ycia plików nagBówkowych, wBczanych do programu dyrektyw preprocesora #include. Z zasady nale|y unika umiesz- czania w tych plikach kodu, a wic definicji zmiennych i funkcji, które nale|y definiowa w osobnym, niezale|nie kompilowanym pliku. Natomiast w pliku nagBówkowym powinny znalez tylko ich deklaracje. Wystarczy rzut oka na przytoczony poni|ej fragment, |eby przekona si, |e w pliku nagBówkowym bufor.h umie[ciBem zarówno funkcje jak i zmienne. W tym przypadku sytuacja jest jednak wyjtkowa: typ danych deponowanych w skrytce, a wic typ argumentu funkcji _umiesc i _usun jest znany dopiero w momencie kompilacji programu gBównego. Podobnie jest z typem i rozmiarem samej skrytki. Oczywi[cie, mo|na zaimplementowa skrytk tak, |eby mogBa by skompilowana wcze[niej i akceptowaBa dane dowolnego typu. Kosztem tego b- dzie wiksze skomplikowanie i brak kontroli zgodno[ci typów danych umieszcza- nych i pobieranych ze skrytki. Nie bd przesdzaB, które rozwizanie jest lepsze, zachcam natomiast gorco do zaimplementowania tego drugiego samodzielnie. Poni|ej przytoczono peBn zawarto[ pliku bufor.h. Zawiera on jedn nie opi- san, ale chyba oczywist funkcj _inicjuj. /* plik bufor.h Adam Sapek */ #ifndef __bufor_h 78 WgBb jzyka C #define __bufor_h #ifndef __proces_h #include "proces.h" #endif #ifndef rozmiar #define rozmiar 100 /* domy[lny rozmiar skrytki */ #endif #ifndef typ #define typ int /* domy[lny typ danych w skrytce */ #endif #define put(x) while(_umiesc(x))_ /* wBó| dan do skrytki */ #define get(x) while(_usun(x))_ /* pobierz dan ze skrytki */ struct { /* skrytka FIFO */ unsigned long pocz,koniec; typ wnetrze[rozmiar]; /* dane typu typ */ }_skrytka; #define PELNA (_skrytka.pocz-_skrytka.koniec==rozmiar) /* czy peBna ? */ #define PUSTA (_skrytka.pocz==_skrytka.koniec) /* czy pusta ? */ void _inicjuj(void) /* inicjacja skrytki */ { _skrytka.pocz=_skrytka.koniec=0; } _umiesc(typ x) /* funkcja umieszcza dan w skrytce */ { if(!PELNA) { _skrytka.wnetrze[(_skrytka.pocz++)%rozmiar]=x; return 0; } return 1; } _usun(typ *x) /* funkcja usuwa dan ze skrytki */ { if(!PUSTA) { *x=_skrytka.wnetrze[(_skrytka.koniec++)%rozmiar]; return 0; } return 1; } #endif Zastosowanie skrytki jest bardzo proste. Program, który jej u|ywa powinien za- wiera dyrektyw #include "bufor.h" IV Programowanie wspóBbie|ne 79 poprzedzon ewentualnymi definicjami rozmiaru skrzynki i/lub typu danych, np.: #define rozmiar 10 #define typ char Do umieszczania danych w skrytce nale|y stosowa makro put, a do ich pobiera- nia makro get. Nale|y pamita, |e argumentem makra get jest wskaznik do zmiennej, w której ma by umieszczona pobrana dana. Poni|szy prosty programik ilustruje zastosowanie skrytki. /* plik prod.c */ #define typ char #include "bufor.h" void producent1(void) /* "produkuje" du|e litery i umieszcza w skrytce */ { static char c; BEGIN for(c='A';c<='Z';c++) { put(c); _ } END } void producent2(void) /* "produkuje" maBe litery i umieszcza w skrytce */ { static char c; BEGIN for(c='z';c>='a';c--) { put(c); _ } END } void konsument(void) /* pobiera ze skrytki znak i wypisuje na ekranie */ { static char c,x; BEGIN for(x=0;x<52;x++) { get(&c); _ putchar(c); } END } void main() { proces(producent1); /* inicjuj procesy */ proces(producent2); proces(konsument); RUN; /* uruchom procesy wspóBbie|nie */ } Program jest trywialny, niemniej jednak prze[ledzenie jego dziaBania mo|e by pouczajce. Radz zwBaszcza poeksperymentowa zmieniajc liczb makrodefi- nicji _ umieszczonych w jednym z procesów producentów, a tak|e rozmiar skrytki. 80 WgBb jzyka C 7.WspóBbie|ne wej[cie z klawiatury WywoBanie w procesie wspóBbie|nym funkcji czytajcej dane z klawiatuy powo- duje zawieszenie dziaBania pozostaBych procesów. Poniewa| wprowadzenie in- formacji z klawiatury zajmuje relatywnie du|o czasu, jest to dosy powa|na uci|liwo[. Poza tym wczytywanie z klawiatury w minimalnym stopniu wyko- rzystuje procesor i jest najlepszym momentem do wykonania jakiej[ pracy przez inne procesy. Zacznijmy od funkcji najprostszych. Funkcje getch i getche sBu| do wczyta- nia znaku z klawiatury. Ró|nica midzy nimi polega na tym, |e getche powoduje wypisanie wczytanego znaku na ekran. Obydwie funkcje czekaj na naci[nicie klawisza i dlatego u|yte w procesie wspóBbie|nym powoduj zawieszenie dziaBa- nia wszystkich procesów. Zamiast tych funkcji mo|emy zdefiniowa makrodefini- cje preprocesora zwracajce kod znaku albo warto[ NIC je|eli w buforze klawiatury nie ma |adnego znaku. #define NIC -1 #define getk() (kbhit()?getch():NIC) #define getke() (kbhit()?getche():NIC) Funkcja kbhit zwraca warto[ niezerow je|eli w buforze klawiatury znajduje si znak gotowy do pobrania. Wszystkie trzy wykorzystane funkcje: kbhit, getch i getche nie s wprawdzie funkcjami standardu ANSI, ale s implementowane za- równo w kompilatorach firmy Borland jak i Microsoft. Bardziej skomplikowane operacje wej[cia realizuje si w jzyku C przy pomocy rodziny funkcji pochodnych od scanf. Rozszerzymy t rodzin o makrodefinicj pscanf umo|liwiajc realizacj formatowanego wej[cia z klawiatury w jednym procesie wspóBbie|nym, podczas gdy pozostaBe procesy wykonuj inna prac. Po- niewa| musimy ograniczy si do u|ycia preprocesora, makro pscanf bdzie umo|liwiaBo wczytanie tylko jednej warto[ci. Bdzie to jednak jedyne ogranicze- nie w stosunku do "oryginaBu"; wszystkie sekwencje formatujce wej[cie akcep- towane w rodzinie scanf bd realizowane przez makro pscanf. Idea rozwizania jest nastpujca. Kolejne znaki, a| do naci[nicia klawisza ENTER, s wczytywane do lokalnego bufora tekstowego. W ptli wczytujcej znaki znajduje si wywoBanie makra _ , co zapewnia wykonywanie si wspóBbie|nie po- zostaBych procesów. Po wczytaniu caBego wiersza, jest on interpretowany przy pomocy standardowej funkcji sscanf realizujcej formatowane wej[cie z cigu znaków. Dziki zastosowaniu funkcji sscanf, mo|emy by pewni, |e wszystkie sekwencje sterujce zostan poprawnie zinterpretowane. Definicja makra pscanf wygldaBaby mniej wicej tak: #define pscanf(form,arg) { static char _buf[100],_x; \ IV Programowanie wspóBbie|ne 81 static int _c; \ _x=0; \ do{ \ _c=getke(); \ if(_c!=NIC) \ _buf[_x++]=_c; \ _ \ }while(_c!='\r'); \ sscanf(_buf,form,arg); \ } Wydaje si konieczne uzupeBnienie jej przynajmniej o interpretacj klawisza BACKSPACE. Odczytanie naci[nicia tego klawisza za pomoc funkcji getche po- woduje przesunicie kursora na ekranie o jeden znak w lewo. Wystarczy wic wy- [wietli w tym miejscu spacj i jeszcze raz cofn kursor. Ponadto trzeba usun ostatni znak z bufora (a wic po prostu dekrementowa licznik x). Rozbudowana w opisany powy|ej sposób makrodefinicja pscanf znajduje si w pliku procesi.h, którego peBny tekst zamieszczono poni|ej. /* plik procesi.h Adam Sapek */ #ifndef __procesi_h #define __procesi_h #ifndef __proces_h #include "proces.h" #endif #include <conio.h> #define NIC -1 #define getk() (kbhit()?getch():NIC) /* czytaj znak z klawiatury */ #define getke() (kbhit()?getche():NIC) /* j.w. + pisz czytany znak */ /* Makro pscanf umo|liwia formatowane wej[cie z klawiatury bez zawieszania */ /* procesów wspóBbie|nych. Makro akceptuje sekwencje sterujce rodziny scanf */ #define pscanf(form,arg) {static char _buf[100],_x; \ static int _c; \ _x=0; \ do{ \ _c=getke(); \ if(_c=='\b') \ 82 WgBb jzyka C {\ putch(' '); putch('\b'); \ if(_x>0)_x--; \ }\ else \ if(_c!=NIC) \ _buf[_x++]=_c; \ _\ }while(_c!='\r'); \ sscanf(_buf,form,arg); \ } #endif Makrodefinicji pscanf u|ywa si dokBadnie tak, jak funkcji z rodziny scanf. Je- dyn ró|nic, o której trzeba pamita, jest to, |e ma ona ustalon liczb argu- mentów: cig format i jedna zmienna do wczytania. Poni|ej przedstawiam programik ilustrujcy ile pracy mog wykona inne proce- sy w czasie, gdy jeden czyta z klawiatury. /* plik mult.c */ #include "procesi.h" unsigned long i; void czytaj(void) /* funkcja pyta o imi i wypisuje pozdrowienie */ { static char name[40]; BEGIN printf("Jak sie nazywasz ?\n"); pscanf("%[^\r]",name); /* wczytaj wiersz do tablicy name */ printf("Ahoj %s !\n",name); ABORT /* przerwij wszystkie procesy */ END } void licz(void) /* funkcja mno|y w petli dwie liczby */ { static float x=3.14, y=1.73, z; BEGIN while(1){ z=y*x; i++; _ } /* pomnó| x*y i zwieksz licznik */ END } void main() { proces(licz); /* inicjuj procesy */ proces(czytaj); RUN /* uruchom procesy wspóBbie|nie */ printf("W miedzyczasie wykonalem %ld mnozen",i); }

Wyszukiwarka

Podobne podstrony:
IMG93
ReadMe (93)
93 dtxt zal1
092 93
93 1SH~1
III CZP 72 93
93 Majkowska
SSHT1(1998)93 104
Dz U 2005 93 775 Konwencja (nr 102) dotycząca minimalnych norm zabezpieczenia społecznego Genewa

więcej podobnych podstron