You are on page 1of 21

62

IV.

Wgb jzyka C

Programowanie wspbiene
Jzyk C nie zawiera oczywicie adnych mechanizmw umoliwiajcych programowanie wspbiene (np. takich, jak w jzyku Ada). W rozdziale niniejszym
przedstawi implementacj moduu umoliwiajcego pseudo-wspbiene wykonywanie funkcji w jzyku C. Termin "pseudo-wspbieno" jest tutaj bardzo
wany, gdy w adnym wypadku zastosowane rozwizanie nie umoliwia realizacji rzeczywistej wspbienoci na maszynach wieloprocesorowych. Pomimo tego,
dla zwikszenia czytelnoci opisu, bd w dalszej jego czci uywa terminw
"wspbieny" oraz "pseudo-wspbieny" wymiennie.
Implementacja wspomnianego moduu bdzie pretekstem do zastosowania wielu
technik opisanych w poprzednich rozdziaach tej ksiki. Dlatego te przed przystpieniem do czytania tego rozdziau polecam przeczytanie rozdziaw poprzednich. Z drugiej strony implementacja ta jest przykadem niecodziennego
stylu programowania w jzyku C, charakteryzujcego si bardzo intensywnym
uyciem preprocesora.
Wspbiene wykonywanie funkcji nie bdzie realizowane na poziomie systemu
operacyjnego lecz na poziomie programu w C, i bdzie ono wykonane
z wykorzystaniem jedynie elementw jzyka standardowego. Oznacza to midzy
innymi, e modu bdzie przenony zarwno na rne kompilatory (naley jednak ostronie stosowa opcje optymalizacji) jak i rne platformy sprztowe. Inn konsekwencj realizacji przeczania zada cakowicie na poziomie jzyka C
jest "gruboziarnisto" zrealizowanej pseudo-wspbienoci. Jeeli dwa (lub
wicej) zadania (funkcje, programy) maj by wykonywane w sposb wspbieny na jednym procesorze, to w rzeczywistoci na zmian wykonywane s
pewne mae fragmenty tych zada. Najmniejsz tak czstk moe by instrukcja procesora, ktra nie moe ju by podzielona. Takie rozwizanie byoby
pseudo-wspbienoci "drobnoziarnist" i gwarantowaoby maxymalne zudzenie rzeczywistej wspbienoci. W przedstawionym poniej module najmniejsz czci funkcji, ktra musi by wykonana, zanim sterowanie zostanie
przekazane do innej funkcji, jest jedna instrukcja (ang. statement) jzyka C.

1. Dlaczego wspbieno?
Klasyczne programy wspbiene s wykonywane na maszynach wieloprocesorowych. Celem zastosowania rwnolegych komputerw i rwnolegych programw
jest zmniejszenie zoonoci czasowej rozwizywanego zadania. Jest spraw
oczywist, e w przypadku programu wykonywanego pseudo-wspbienie na
komputerze jednoprocesorowym nie mona liczy na zwikszenie prdkoci oblicze. Co wicej, wykonanie w takim przypadku kilku zada musi trwa duej ni
trwaoby wykonanie tych zada sekwencyjnie jedno po drugim. Dzieje si tak
dlatego, e oprcz kodu zada procesor musi wykonywa pewien kod zwizany

IV Programowanie wspbiene

63

z ich przeczaniem. Mona by w takim razie powiedzie, e pseudowspbieno jest sztuk dla sztuki. Nie jest to prawd, a najlepszym na to dowodem jest popularno programw typu DESQview czy Windows, umoliwiajcych
pseudowspbiene wykonywanie programw. Twierdzenie, e wielozadaniowo
realizowana na jednym procesorze nie moe przynie zyskw czasowych jest
prawd dopty, dopki zadania cay czas wymagaj pracy procesora.
W rzeczywistoci czste s sytuacje, gdy wikszo czasu pracy zadania nie jest
zuywana na prac procesora. Na przykad operacje na pamici zewntrznej s
zwykle na tyle wolne w porwnaniu z szybkoci procesora, e mgby on rwnoczenie wykonywa inn prac. Jeszcze bardziej skrajnym przypadkiem jest czekanie przez zadanie na dane wprowadzane przez uytkownika z klawiatury. Jeeli
jedno z zada utkno w takim wskim gardle, procesor moe powici swj czas
na wykonanie innych zada. Mona to zrealizowa wanie poprzez pseudowspbieno.
Zyskiwanie czasu w takich sytuacjach nie jest jednak jedynym motywem zastosowania wielozadaniowoci. Programy wykonujce kilka zada na raz mog by
bardzo wygodne dla uytkownika. Przykadem niech bdzie edytor tekstw zapisujcy co jaki czas redagowany tekst "w tle".
Jak ju wczeniej wspomniaem, realizacja wspbienego wykonywania funkcji
bdzie polegaa na wykonywaniu na zmian kolejnych fragmentw kadej
z funkcji. Do przekazywania sterowania z jednej funkcji do drugiej posu 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 nagwkowym "setjmp.h". Su one do zapamitania,
a nastpnie odtworzenia stanu programu. W praktyce oznacza to, e funkcja longjmp umoliwia wykonanie skoku do jakiego miejsca, w ktrym stan programu
zosta wczeniej 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 ktrej przechowywane s informacje o stanie programu.
Wywoanie funkcji setjmp powoduje zapamitanie w zmiennej typu jmp_buf,
przekazanej do niej jako argument, informacji o stanie programu. Funkcja zwraca
warto 0.
Funkcj longjmp wywouje si z dwoma argumentami. Pierwszym jest zmienna,
w ktrej wczeniej zapamitano stan programu przy pomocy funkcji setjmp.
Drugi argument jest liczb cakowit. Wywoanie funkcji longjmp powoduje

64

Wgb jzyka C
odtworzenie stanu programu jaki zosta zapamitany w zmiennej typu jmp_buf
przekazanej jako pierwszy argument. W wyniku takiego wywoania funkcji longjmp program znajduje si w punkcie powrotu z funkcji setjmp (bo w takim
momencie zosta zapamitany stan programu), przy czym warto zwracana przez
funkcj setjmp jest rwna drugiemu argumentowi funkcji longjmp (lub 1 jeeli
drugi argument by rwny 0). Na podstawie wartoci funkcji setjmp, program
jest w stanie odrni czy zostaa ona normalnie wywoana w wyniku zinterpretowania kolejnej instrukcji, czy te nastpi skok przy pomocy funkcji longjmp.
Dziaanie funkcji setjmp i longjmp jest czasami trudne do zrozumienia. Poniszy przykad powinien wyjani niejsnoci.
if(setjmp(buf))
{
/* cig instrukcji */
}
/* ... */
longjmp(buf,3);

Po wywoaniu funkcji setjmp warunek nie bdzie speniony (setjmp zwrci


warto 0) i cig instrukcji nie zostanie wykonany. W efekcie wywoania funkcji
longjmp w innej czci programu, sterowanie zostanie przekazane do miejsca
powrotu z funkcji setjmp, ale tym razem zostanie zwrcona warto rwna 3,
a wic warunek bdzie speniony i cig instrukcji zostanie wykonany.
Poniewa po "powrocie" funkcja setjmp zwraca warto przekazan jako argument funkcji longjmp, nic nie stoi na przeszkodzie, eby przy pomocy rnych
funkcji longjmp przekazywa rne wartoci i na ich postawie identyfikowa
miejsce, z ktrego nastpi daleki skok, na przykad przy pomocy instrukcji switch
switch(setjmp(buf))
{
case 1 : /* z punktu 1 */ break;
case 2 : /* z punktu 2 */ break;
case 3 : /* z punktu 3 */ break;
}

3. Przeczanie zada
Zastanwmy si na pocztek, w jaki sposb dokona przeczenia procesora pomidzy
dwiema funkcjami. Jak wczeniej napisaem, uyjemy pary funkcji setjmp i longjmp
do wykonania dalekich skokw pomidzy procesami (funkcjami). Dla kadego procesu
bdziemy potrzebowa jednej zmiennej typu jmp_buf sucej do zapamitania stanu
programu w momencie przekazania sterowania do drugiego procesu. Obie zmienne musz by globalne, aby obie funkcje mogy si do nich odwoa.
jmp_buf buf1,buf2;

W celu wykonania skoku do funkcji f1 bdziemy uywa wywoania

IV Programowanie wspbiene

65

longjmp(buf1,1);

Analogicznie, eby skoczy do funkcji f2


longjmp(buf2,1);

Przed wykonaniem skoku do drugiego procesu, kada funkcja musi wywoa


funkcj setjmp(buf). Zapamitane przez t funkcj informacje o stanie programu
bd mogy by w przyszoci wykorzystane do powrotu do miejsca, w ktrym
dziaanie funkcji zostao zawieszone. Tak wic sekwencje przeczajce zadania
bd wyglday 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 wywoana tylko wtedy, gdy setjmp zwrci warto zero. Nastpi to wic po wywoaniu setjmp w celu zapamitania kontekstu programu,
a nie nastpi po powrocie w to miejsce przy pomocy dalekiego skoku.
Sprbujmy teraz uoglni to rozwizanie na nieznan z gry ilo procesw.
Trzeba zdefiniowa jak struktur danych, ktra zapewniaaby istnienie jednego
bufora typu jmp_buf dla kadej funkcji, a take umoliwiaaby okrelenie jaka jest
nastpna funkcja w "acuszku".
struct el {
jmp_buf buf;
struct el *next;
};

Kad funkcja bdzie posiadaa wasny element typu struct el. W polu buf tego
elementu bdzie zapamitywany kontekst tej funkcji w chwili przeczania sterowania do kolejnego zadania. Pole next struktury bdzie wskazywao element typu
struct el skojarzony z funkcj, do ktrej ma by przekazane sterowanie. W ten sposb powstanie zaptlona lista o wzach typu struct el. Lista jest jednokierunkowa,
gdy kady jej element zawiera tylko pole wskazujce nastpny element. Do penej
manipulacji list jednokierunkow (w tym do usuwania elementw z listy) potrzebne s co najmniej dwie zmienne, wskazujce na dwa kolejne wzy listy:
struct el *cur,*last;

Zmienna cur bdzie zawsze wskazywa na wze odpowiadajcy aktywnej funkcji,


za zmienna last - na wze odpowiadajcy poprzedniej funkcji. Sekwencja przeczania zada zapisana przy uyciu tych zmiennych bdzie wygldaa nastpujco:
if(setjmp(cur->buf)==0)
longjmp((last=cur,cur=(cur->next))->buf,1);

Argumentem funkcji longjmp jest do skomplikowane wyraenie:


(last=cur,cur=(cur->next))->buf

Analiza tego wyraenia rozpocznie si od przypisania zmiennej last wartoci


zmiennej cur. Nastpnie zmiennej cur jest przypisywany wskanik do nastpnego

66

Wgb jzyka C
wza listy. Pole buf tego wza zostaje argumentem funkcji longjmp (zostanie
wykonany skok do nastpnej funkcji).
Pola buf w licie musz by zainicjowane przed pierwszym wywoaniem sekwencji przeczajcej zadania. eby to osign, umiecimy na pocztku kadej funkcji nastpujcy warunek:
if(setjmp(cur->buf)==0)return;

Jeeli funkcja zostanie wywoana, to jej kontekst zostanie zapamitany w polu


cur->buf i nastpi od razu powrt do funkcji wywoujcej.
Pozostaje jeszcze zastanowi si, co zrobi po zakoczeniu dziaania procesu.
Trzeba go oczywicie usun z listy, eby nie by wicej wykonywany. Dokonuje si tego instrukcj
cur=last->next=cur->next;

W tym miejscu przydaje si zadeklarowana wczeniej na wyrost zmienna last. Nastpnie trzeba przekaza sterowanie do kolejnego procesu:
longjmp(cur->buf,1);

4. Zapis praktyczny
Przedstawione powyej konstrukcje robi dobry uytek z funkcji setjmp
i longjmp umoliwiajc przeczanie funkcji - zada, ale w adnym wypadku nie
nadaj si do praktycznego zastosowania w programowaniu.
Stosujc definicje preprocesora mona zapisa te sekwencje w sposb duo
czytelniejszy. Zamy, e zapis ten musi spenia nastpujce warunki:
K zamiana napisanej i uruchomionej wczeniej funkcji na posta, w ktrej

mogaby ona by wykonywana wspbienie, musi by prosta, prawie


automatyczna,

K funkcja przystosowana do wykonywania wspbienego powinna dalej

mc by wywoywana w normalny sposb,

K jeeli funkcja jest ostatnim procesem (wszystkie inne zakoczyy ju dziaa-

nie) to nie powinna by wykonywana sekwencja przeczania zada.

Pierwszym krokiem bdzie zastpienie omwionych w poprzednim podrozdziale


sekwencji odpowiednimi definicjami preprocesora:
#define BEGIN
#define END
#define _

if(setjmp(cur->buf)==0)return;
cur=last->next=cur->next;
\
longjmp(cur->buf,1);
if(setjmp(cur->buf)==0)
\
longjmp((last=cur,cur=(cur->next))->buf,1);

IV Programowanie wspbiene

67

Makrodefinicje BEGIN oraz END bd umieszczane odpowiednio na pocztku


i na kocu funkcji. Ostatnie makro jest waciw sekwencj przeczajc zadania.
Ideaem byaby sytuacja gdyby to makro w ogle nie byo widoczne, dlatego te
wybraem dla niego nazw jak najmniej rzucajc si w oczy: _ (podkrelenie).
Jak ju wczeniej ustalilimy, wszystkie makrodefinicje maj by "przeroczyste" gdy funkcja zostanie wywoana w normalny sposb. Do rozrniania czy
funkcja jest wykonywana wspbienie czy nie, uyjemy lokalnej zmiennej
is_a_process. Zmienna ta bdzie miaa warto 1 tylko wtedy, gdy funkcja zostanie
wywoana przez funkcj inicjujc procesy.
Definicje rozbudowane o sprawdzanie, czy funkcja jest wykonywana wspbienie wygldaj nastpujco:
#define BEGIN

\
static char is_a_process;
\
if((is_a_process=be_a_process)!=0)
\
#define END

if(setjmp(cur->buf)==0)return;
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 cay czas zero
i jest ustawiana na jeden przez funkcj inicjujc procesy na czas inicjujcego
wywoania procesu. Funkcja inicjujca przydziela pami na struktur struct el dla
procesu i umieszcza j w licie.
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

Wgb jzyka C
W funkcji proces wystpuje globalna zmienna _number. Jak wskazuje nazwa, jej
warto bdzie okrelaa liczb "ywych" procesw. Proces bdzie jej uywa do
sprawdzenia czy nie jest ju przypadkiem ostatnim ywym procesem.
Po zainicjowaniu wszystkich procesw mona rozpocz ich wspbiene wykonywanie. W tym celu zdefiniujemy makro RUN:
#define RUN

if(setjmp(_the_end)==0)
\
longjmp((cur=cur->next)->buf,1);

Globalna zmienna _the_end suy do zapamitania punktu, z ktrego zostao wywoane wspbiene wykonywanie procesw, i do ktrego naley wrci gdy
wszystkie procesy zakocz dziaanie.
Wyjanienia wymaga chyba jeszcze wyraenie w funkcji longjmp:
(cur=cur->next)->buf

Zapewnia ono rozpoczcie pseudo-wspbienego wykonywania funkcji od tej,


ktra zostaa jako pierwsza zadeklarowana za pomoc funkcji proces.
Po wprowadzeniu dwch dodatkowych zmiennych globalnych, _number
i _the_end, wczeniejsze definicje bd wyglday nastpujco:
#define BEGIN

\
static char is_a_process;

\
if((is_a_process=be_a_process)!=0)
\
if(setjmp(cur->buf)==0)return;
#define END

#define _

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 */
if(is_a_process!=0 && _number>0 )

\
if(setjmp(cur->buf)==0)
\
longjmp((last=cur,cur=(cur->next))->buf,1);

Poniej znajduje si pena zawarto plikw proces.h i proces.c zawierajcych wszystkie opisane powyej definicje, a take definicje i deklaracje wszystkich uywanych zmiennych. Dodatkowo w pliku proces.h zostao
zdefiniowane makro ABORT, powodujce zakoczenie wszystkich procesw.

IV Programowanie wspbiene
/* plik proces.c
#include "proces.h"

69

Adam Sapek

*/

struct el *cur=NULL,*last=NULL;
zmiennych globalnych */

/* definicje

jmp_buf _the_end;
unsigned char _number=0,be_a_process=0;
void proces(FUNCTION f)
inicjujca proces */
{
struct el *tmp;

/* funkcja

_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 ->
stwrz 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 kocu 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

Wgb jzyka C
longjmp((last=cur,cur=(cur->next))->buf,1);
/* makro RUN rozpoczyna wsplbiene wykonywanie procesw */
#define RUN
if(setjmp(_the_end)==0)longjmp((cur=cur->next)->buf,1);
/* makro ABORT powoduje natychmiastowe przerwanie WSZYSTKICH
procesw */
#define ABORT

if(is_a_process!=0)
{_number=0; longjmp(_the_end,1);}

/* deklaracje obiektw zewntrznych */


typedef void (*FUNCTION)(void);
struct el {
jmp_buf
struct el
};

buf;
*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 wspbieny
Na pocztek wypada poda kilka oglnych zasad stosowania zdefiniowanych narzdzi. Piszc program wspbieny najlepiej napisa i uruchomi osobno kad
funkcj, ktra ma by w programie procesem. Przystosowanie napisanej wczeniej
funkcji do pracy wspbienej jest bardzo atwe i ogranicza do zmian "kosmetycznych". Po pierwsze na pocztku funkcji, po deklaracjach zmiennych, naley umieci makro BEGIN. Drugie makro, END, umieszcza si na kocu funkcji.
Teoretycznie ani makro BEGIN nie musi znajdowa si na samym pocztku funkcji, ani makro END na samym kocu, wymagane jest tylko, aby makro BEGIN
poprzedzao makro END. Umieszczajc wspomniane makra w innym miejscu naley jednak pamita, e tylko ta cz funkcji, ktra jest zawarta midzy nimi, bdzie wykonywana w pracy wspbienej. Kod poprzedzajcy makro BEGIN
wykona si tylko podczas inicjowania procesu funkcj proces, a kod nastpujcy
za makrem END nie wykona si w ogle.
Makro o nazwie _ (podkrelenie) umieszcza si w tych punktach, w ktrych proces ma by przerywany, a sterowanie ma by przekazywane do innych procesw. eby uzyska najlepsz wspbieno, najlepiej dopisa to makro do
kadego rednika koczcego instrukcj. Nie zawsze takie rozwizanie jest najlepsze. Sekwencja przekazania sterowania (makro _) ma swoje koszty, zarwno
czasowe jak i pamiciowe. Umieszczajc to makro inteligentnie w kadym procesie mona w duym stopniu sterowa dziaaniem programu. Mona w ten sposb na przykad uprzywilejowa niektre procesy lub ich fragmenty,

IV Programowanie wspbiene

71

uniemoliwi przerwanie krytycznych sekcji w procesach. Niektre zasady wyboru lokalizacji makra _ zostan opisane w dalszej czci tego rozdziau, przy
opisie przykadowego programu.
Najbardziej zdradzieckim bdem, jaki mona popeni w programie wspbienym, jest uycie w procesie zmiennej automatycznej. Pami na zmienne automatyczne jest przydzielana po wywoaniu funkcji i zwalniania po jej
zakoczeniu. Z tego powodu zmienne automatyczne rnych procesw mog
znale si w tym samym obszarze pamici (i zwykle tak si wanie dzieje).
Bdy powstae "dziki temu" mog by bardzo trudne do wykrycia nawet przy
pomocy debuggera. Dlatego naley bezwzgldnie pamita o zamienieniu
wszystkich zmiennych automatycznych w funkcjach/procesach na zmienne statyczne. Jest to waciwie jedyna zmiana merytoryczna jakiej naley dokona
w funkcji, eby moga sta si procesem wspbienym.
Programy wspbiene naley konsolidowa z moduem proces.obj powstaym po skompilowaniu pliku proces.c. Mona to zrobi na wiele sposobw,
np.:
K

utworzy w rodowisku Turbo C projekt o zawartoci:


program.c
proces.c

uy bezporednio kompilatora wsadowego Microsoft C poprzez wywoanie:


cl program.c proces.c

Przykadowy program
Program demonstracyjny bdzie skada si z dwch procesw. Jeden z nich bdzie wczytywa kolejne bajty z podanego pliku i zlicza cakowit liczb bitw
rwnych jeden w pliku. Drugi proces na czas pracy pierwszego wygasi ekran
i bdzie wywietla na nim chodzcego "wa". Po przeczytaniu caego pliku
proces pierwszy przerwie dziaanie obydwu procesw i wrci do programu
gwnego, ktry wywietli liczb jedynek w pliku.
W wersji klasycznej funkcja zliczajca liczb jedynek w pliku mogaby wyglda na przykad 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

Wgb jzyka C
while(c){ sum+=c&1; c/=2; }
}
}

Funkcja ta pyta o nazw pliku i prbuje otworzy do czytania w trybie binarnym


plik o podanej nazwie. Jeeli plik uda si otworzy, funkcja wczytuje kolejno
wszystkie bajty i zlicza kolejne jedynki.
Gdzie umieci poszczeglne makra, eby przystosowa nasz funkcj do pracy
wspbienej ?
Makro END znajdzie si w standardowym miejscu, na kocu funkcji. Makro
BEGIN mona umieci albo bezporednio po definicjach zmiennych, albo dopiero po wczytaniu nazwy pliku. Wydaje si, e lepiej jest umieci je w tym drugim
miejscu. Pytanie o nazw pliku pojawi si wtedy w czasie inicjowania procesu
funkcj proces. Prosz zauway, e nie mona makra BEGIN przesun jeszcze
dalej, za warunek if Gdyby nie udao si otworzy pliku, makro BEGIN w ogle
nie zostaoby wykonane, a funkcja nie zostaaby poprawnie zainicjowana jako proces.
Normalnie, zakoczenie dziaania jednego procesu wspbienego nie powoduje
przerwania dziaania pozostaych; proces zakoczony jest po prostu wyczany
z kolejki. W naszym programie proces czytajcy plik jest procesem nadrzdnym.
Natychmiast po przeczytaniu caego pliku obydwa procesy powinny by przerwane, a program gwny powinien wypisa wynik. W takim celu zostao
w pliku proces.h zdefiniowane makro ABORT. Powoduje ono natychmiastowe
przerwanie wszystkich zada i skok do miejsca, z ktrego zostay uruchomione
makrem RUN. Makro ABORT naley umieci bezporednio przed makrem END.
Kolejn zmian jest nadanie zmiennym lokalnym klasy static Wystarczy
w tym celu przed definicjami umieci sowo kluczowe static
Na koniec zostawiem spraw najwaniejsz - makro _ (podkrelenie). W tej
funkcji istniej dwa potencjalne miejsca, w ktrych mona je umieci: ptla
gwna i ptla testujca kolejne bity wczytanego bajtu. Zaley nam oczywicie,
eby funkcja czytajca plik nie zostaa zbytnio spowolniona, a wic najlepiej jest
umieci makro _ tylko w ptli gwnej. Powstaje pytanie, czy wystarczy to, eby drugi proces mg dziaa pynnie. Jedyn odpowied mona uzyska metod
dowiadczaln, uruchamiajc program. Stwierdziem, e umieszczenie makra
tylko w ptli gwnej jest zupenie wystarczajce.
Funkcja bits wyglda po zmianach nastpujco:
void bits(void)
{
static FILE *in;
static unsigned c;
static char name[80];

IV Programowanie wspbiene

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 wczeniej napisaem, bdzie wygaszenie ekranu


na czas dziaania procesu czytajcego plik. Proces ten bdzie przykadem adaptacji do pracy wspbienej funkcji napisanej w zupenie innym celu. Zostanie tu
uyta oryginalna funkcja z rezydentnego programu Screen Saver, zaprezentowanego w rozdziale pitym tej ksiki. W celu zwikszenia czytelnoci, w funkcji
tej dokonaem kilku uproszcze, a take zamieniem bezporednie odwoania do
pamici video na odpowiednie funkcje biblioteczne. Poniewa funkcje te nie s
zestandaryzowane, ich wersje dla rnych kompilatorw nieco si rni. Poniej przedstawi wersj dla kompilatora Turbo C firmy Borland, a na doczonej
do ksiki dyskietce znajduje si take wersja dla kompilatora Microsoft C 5.1.
Proces wygaszajcy ekran jest przerywany przez proces pierwszy, gdy ten zakoczy swoj prac, dlatego te jego gwna ptla jest nieskoczona. Skoro tak,
to wydaje si, e umieszczanie makra END na kocu funkcji, w miejscu,
w ktrym nigdy nie zostanie wykonane, mija si z celem. Prosz jednak jeszcze raz
spojrze na definicje: makra BEGIN i END stanowi nierozczn par.
W definicji makra END znajduje si nawias zamykajcy instrukcj blokow
otwart w definicji BEGIN.
W tym miejscu dochodzimy znowu do punktu zasadniczego: gdzie umieci makro _. Prosz spojrze na funkcj ekr i sprbowa samemu wybra najodpowiedniejsze 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

Wgb 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;
}

Waciwie umieszczenie tego makra obok kadego rednika nie szkodzi niczym
oprcz niepotrzebnego zwikszenia programu i zmniejszenia czytelnoci zapisu.
Kluczem do poprawnego umieszczenia sekwencji przeczajcej zadania jest spostrzeenie, e funkcja scr praktycznie cay czas spdza w ptli opniajcej (prosz sprbowa uruchomi t funkcj bez tej ptli !):
while(*(char far *)0x46c-a<7);

Teraz jest ju chyba jasne, e wystarczy umieci makro _ (podkrelenie) w tej


ptli, i to nie obok rednika, tylko zamiast niego:
while(*(char far *)0x46c-a<7)_

Poniej 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
wspbienej */
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
wspbiene */
END
}
struct {
reprezentuje jednen*/

/* struktura

IV Programowanie wspbiene

75

unsigned char x,y;


/* czon wa */
}snake[4]={{4,4},{3,3},{2,2},{1,1}};

/* w ma 4 czony */

void scr(void)
/* funkcja wygasza ekran i wywietla
chodzcego wa */
{
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 krokw
*/
direct=rand()%8;
/* losuj nowy
kierunek wa
*/
x=snake[0].x+dx[direct];
y=snake[0].y+dy[direct];
}while(x<1||x>=80||y<1||y>25);
/* czy mona w tym
kierunku ? */
wa

wa

gotoxy(x,y);
*/
gotoxy(snake[0].x,snake[0].y);
gotoxy(snake[1].x,snake[1].y);
gotoxy(snake[2].x,snake[2].y);
gotoxy(snake[3].x,snake[3].y);
for(a=3;a>=1;a--)

printf("");

/* rysuj

printf("");
printf("");
printf("");
printf(" ");
/* przesu

*/

{
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) _
taktw zegara */
}
END;
}
void main()
{
clrscr();
*/
proces(scr);
procesy
*/
proces(bits);
RUN;
procesy
*/
clrscr();
printf("W pliku znaleziono %lu jedynek",sum);
*/
}

/* odczekaj 7

/* wyczy ekran
/* inicjuj

/* uruchom

/* wypisz wynik

6. Komunikacja midzy procesami


Najprostszym sposobem komunikacji midzy procesami wspbienymi jest zastosowanie zmiennych globalnych. Metoda ta jest tyle prosta, co prymitywna. Nie

76

Wgb jzyka C
zapewnia ona synchronizacji: proces, ktry ma czyta jak dan ze zmiennej globalnej, nie wie, czy zostaa ona tam ju zapisana. Podobnie, proces zapisujcy dan
nie wie, czy poprzednia warto zostaa ju pobrana. Dopisanie odpowiednich
wskanikw synchronizujcych moe na tyle zaciemni zapis, e warto posuy
si preprocesorem w celu implementacji w miar oglnej metody komunikacji
midzy procesami. Dziki ukryciu szczegw w definicjach procesora
i zadeklarowaniu funkcji, zmiennych i struktur w osobnym pliku mona uzyska
bardzo atwe w uyciu i elegancko wygldajce narzdzia.
Do komunikacji midzy procesami uyjemy skrytki typu FIFO (ang. First In
First Out). Procesy "producenci" bd deponowa w skrytce dane okrelonego
typu, a procesy "konsumenci" bd je z niej pobiera. Jeeli skrytka bdzie pusta, konsumenci bd czeka na dane, jeeli za bdzie pena, producenci bd
czeka na wolne miejsce. Niezbdne jest, aby oczekiwanie ktrego procesu (czy
to konsumenta, czy producenta) nie powodowao zawieszenia pozostaych procesw. W przeciwnym przypadku czekajcy proces nigdy nie doczekaby si
zmiany stanu skrytki.
Typ danych przechowywanych w skrytce (w C++ moe to by nie tylko typ
standardowy, ale take klasa) oraz rozmiar skrytki bdzie mg by definiowany.
W przypadku niezdefiniowania typu i/lub rozmiaru bd przyjmowane wartoci
domylne: typ inti rozmiar rwny 100
Skrytka bdzie struktur zdefiniowan jak poniej:
struct {
unsigned long
typ
}_skrytka;

pocz,koniec;
wnetrze[rozmiar];

Pole pocz bdzie wskazywao nastpny element moliwy do pobrania, a pole koniec, pierwsze wolne miejsce w skrytce. Jeeli skrytka bdzie pusta, warto pola
pocz bdzie rwna wartoci pola koniec. Jeeli skrytka bdzie pena, zajdzie warunek:
_skrytka.koniec-_skrytka.pocz==rozmiar

Warunki sprawdzajce, czy skrytka jest pena czy pusta, mog przyda si
w programowaniu, dlatego zdefiniujemy je w postaci identyfikatorw 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 wymagaj chyba komentarza:
_umiesc(typ x)
{
if(!PELNA)
{

IV Programowanie wspbiene

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 moduu komunikacji midzy procesami; dla programisty przygotujemy wygodniejsze narzdzie. Jak wida, funkcje _umiesc
i _usun zwracaj warto 0 w przypadku sukcesu i warto 1 w przypadku niepowodzenia. Wykorzystujc t wasno zdefiniujemy makrodefinicje czekajce
w razie potrzeby na dane lub wolne miejsce w skrytce, ale nie zawieszajce przy
tym pozostaych procesw.
#define
#define

put(x)
get(x)

while(_umiesc(x))_
while(_usun(&x))_

Makrodefinicje get i put czekaj w ptli while a odpowiednia funkcja zwrci


warto zero, co wiadczy o pomylnym zapisie do skrzynki (put) lub odczycie ze
skrytki (get). Jednoczenie w kadym obiegu ptli, wywoywane jest makro _
(podkrelenie), dziki czemu pozostae procesy mog normalnie pracowa.
Na koniec dygresja dotyczca uycia plikw nagwkowych, wczanych do
programu dyrektyw preprocesora #include. Z zasady naley unika umieszczania w tych plikach kodu, a wic definicji zmiennych i funkcji, ktre naley
definiowa w osobnym, niezalenie kompilowanym pliku. Natomiast w pliku
nagwkowym powinny znale tylko ich deklaracje.
Wystarczy rzut oka na przytoczony poniej fragment, eby przekona si, e
w pliku nagwkowym bufor.h umieciem zarwno 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 gwnego. Podobnie jest z typem i rozmiarem
samej skrytki. Oczywicie, mona zaimplementowa skrytk tak, eby moga by
skompilowana wczeniej i akceptowaa dane dowolnego typu. Kosztem tego bdzie wiksze skomplikowanie i brak kontroli zgodnoci typw danych umieszczanych i pobieranych ze skrytki. Nie bd przesdza, ktre rozwizanie jest lepsze,
zachcam natomiast gorco do zaimplementowania tego drugiego samodzielnie.
Poniej przytoczono pen zawarto pliku bufor.h. Zawiera on jedn nie opisan, ale chyba oczywist funkcj _inicjuj.
/* plik bufor.h
Adam Sapek
*/
#ifndef __bufor_h

78

Wgb jzyka C
#define __bufor_h
#ifndef __proces_h
#include "proces.h"
#endif
#ifndef rozmiar
#define rozmiar 100
skrytki */
#endif
#ifndef typ
#define typ int
skrytce */
#endif

/* domylny rozmiar

/* domylny typ danych w

#define put(x)
skrytki */

while(_umiesc(x))_

#define get(x)
skrytki */

while(_usun(x))_

/* w dan do
/* pobierz dan ze

struct {
FIFO */

/* skrytka

unsigned long pocz,koniec;


typ
wnetrze[rozmiar];
typu typ */
}_skrytka;
#define PELNA (_skrytka.pocz-_skrytka.koniec==rozmiar)
pena ? */
#define PUSTA (_skrytka.pocz==_skrytka.koniec)
pusta ? */

/* dane

/* czy
/* czy

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, ktry jej uywa powinien zawiera dyrektyw
#include "bufor.h"

IV Programowanie wspbiene

79

poprzedzon ewentualnymi definicjami rozmiaru skrzynki i/lub typu danych, np.:


#define rozmiar 10
#define typ
char

Do umieszczania danych w skrytce naley stosowa makro put, a do ich pobierania makro get. Naley pamita, e argumentem makra get jest wskanik do
zmiennej, w ktrej ma by umieszczona pobrana dana. Poniszy prosty programik
ilustruje zastosowanie skrytki.
/* plik prod.c */
#define typ char
#include "bufor.h"
void producent1(void)
umieszcza w skrytce */
{
static char c;
BEGIN
for(c='A';c<='Z';c++)
{ put(c); _ }
END
}
void producent2(void)
umieszcza w skrytce */
{
static char c;
BEGIN
for(c='z';c>='a';c--)
{ put(c); _ }
END
}
void konsument(void)
na ekranie */
{
static char c,x;
BEGIN
for(x=0;x<52;x++)
{
get(&c); _
putchar(c);
}
END
}
void main()
{
proces(producent1);
proces(producent2);
proces(konsument);
RUN;
*/
}

/* "produkuje" due litery i

/* "produkuje" mae litery i

/* pobiera ze skrytki znak i wypisuje

/* inicjuj procesy */

/* uruchom procesy wspbienie

Program jest trywialny, niemniej jednak przeledzenie jego dziaania moe by


pouczajce. Radz zwaszcza poeksperymentowa zmieniajc liczb makrodefinicji _ umieszczonych w jednym z procesw producentw, a take rozmiar
skrytki.

80

Wgb jzyka C

7. Wspbiene wejcie z klawiatury


Wywoanie w procesie wspbienym funkcji czytajcej dane z klawiatuy powoduje zawieszenie dziaania pozostaych procesw. Poniewa wprowadzenie informacji z klawiatury zajmuje relatywnie duo czasu, jest to dosy powana
uciliwo. Poza tym wczytywanie z klawiatury w minimalnym stopniu wykorzystuje procesor i jest najlepszym momentem do wykonania jakiej pracy przez
inne procesy.
Zacznijmy od funkcji najprostszych. Funkcje getch i getche su do wczytania znaku z klawiatury. Rnica midzy nimi polega na tym, e getche powoduje
wypisanie wczytanego znaku na ekran. Obydwie funkcje czekaj na nacinicie
klawisza i dlatego uyte w procesie wspbienym powoduj zawieszenie dziaania wszystkich procesw. Zamiast tych funkcji moemy zdefiniowa makrodefinicje preprocesora zwracajce kod znaku albo warto NIC jeeli w buforze
klawiatury nie ma adnego znaku.
#define NIC -1
#define getk()
#define getke()

(kbhit()?getch():NIC)
(kbhit()?getche():NIC)

Funkcja kbhit zwraca warto niezerow jeeli 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 zarwno w kompilatorach firmy Borland jak i Microsoft.
Bardziej skomplikowane operacje wejcia realizuje si w jzyku C przy pomocy
rodziny funkcji pochodnych od scanf. Rozszerzymy t rodzin o makrodefinicj
pscanf umoliwiajc realizacj formatowanego wejcia z klawiatury w jednym
procesie wspbienym, podczas gdy pozostae procesy wykonuj inna prac. Poniewa musimy ograniczy si do uycia preprocesora, makro pscanf bdzie
umoliwiao wczytanie tylko jednej wartoci. Bdzie to jednak jedyne ograniczenie w stosunku do "oryginau"; wszystkie sekwencje formatujce wejcie akceptowane w rodzinie scanf bd realizowane przez makro pscanf.
Idea rozwizania jest nastpujca. Kolejne znaki, a do nacinicia klawisza
ENTER, s wczytywane do lokalnego bufora tekstowego. W ptli wczytujcej znaki
znajduje si wywoanie makra _ , co zapewnia wykonywanie si wspbienie pozostaych procesw. Po wczytaniu caego wiersza, jest on interpretowany przy
pomocy standardowej funkcji sscanf realizujcej formatowane wejcie z cigu
znakw. Dziki zastosowaniu funkcji sscanf, moemy by pewni, e wszystkie
sekwencje sterujce zostan poprawnie zinterpretowane.
Definicja makra pscanf wygldaaby mniej wicej tak:
#define pscanf(form,arg)
\

{ static char _buf[100],_x;

IV Programowanie wspbiene

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 uzupenienie jej przynajmniej o interpretacj klawisza


BACKSPACE. Odczytanie nacinicia tego klawisza za pomoc funkcji getche powoduje przesunicie kursora na ekranie o jeden znak w lewo. Wystarczy wic wywietli 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 powyej sposb makrodefinicja pscanf znajduje si
w pliku procesi.h, ktrego peny tekst zamieszczono poniej.
/* 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()
klawiatury */
#define getke()
czytany znak */

(kbhit()?getch():NIC)

/* czytaj znak z

(kbhit()?getche():NIC)

/* j.w. + pisz

/* Makro pscanf umoliwia formatowane wejcie z klawiatury bez


zawieszania
*/
/* procesw wspbienych. 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

Wgb 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 uywa si dokadnie tak, jak funkcji z rodziny scanf. Jedyn rnic, o ktrej trzeba pamita, jest to, e ma ona ustalon liczb argumentw: cig format i jedna zmienna do wczytania.
Poniej przedstawiam programik ilustrujcy ile pracy mog wykona inne procesy 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 mnoy 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
wspbienie */
printf("W miedzyczasie wykonalem %ld mnozen",i);
}

You might also like