LEKCJA 21: KILKA PROCESÓW JEDNOCZEŚNIE. ________________________________________________________________ W trakcie tej lekcji dowiesz się, jak to zrobić, by Twój PC mógł wykonywać kilka rzeczy jednocześnie. ________________________________________________________________ Procesy współbieżne. Sprzęt, czyli PC ma możliwości zdecydowanie pozwalające na techniczną realizację pracy wielozadaniowej. Nie ma też żadnych przeciwskazań, by zamiast koprocesora umożliwić w PC instalację drugiego (trzeciego) równoległego procesora i uprawiać na PC poważne programowanie współbieżne. Po co? To proste. Wyobraź sobie Czytelniku, że masz procesor pracujący z częstotliwością 25 MHz (to 25 MILIONÓW elementarnych operacji na sekundę!). Nawet, jeśli wziąć pod uwagę, że niektóre operacje (dodawanie, mnożenie, itp.) wymagają wielu cykli - i tak można w uproszczeniu przyjąć, że Twój procesor mógłby wykonać od kilkuset tysięcy do kilku milionów operacji w ciągu sekundy. Jeśli pracujesz np. z edytorem tekstu i piszesz jakiś tekst - znacznie ponad 99% czasu Twój procesor czeka KOMPLETNIE BEZCZYNNIE (!) na naciśnięcie klawisza. Przecież Twój komputer mogłby w tym samym czasie np. i formatować dyskietkę (dyskietka też jest powolna), i przeprowadzać kompilację programu, i drukować dokumenty, i przeprowadzić defragmentację drugiego dysku logicznego, itp. itd.. Nawet taka pseudowspółbieżność realizowana przez DOS, Windows, czy sieć jest ofertą dostatecznie atrakcyjną, by warto było przyjrzeć się mechanizmom PSEUDO-współbieżności w C i C++. Współbieżność procesów, może być realizowana na poziomie * sprzętowym (architektura wieloprocesorowa), * systemowym (np. Unix, OS/2), * nakładki (np. sieciowej - time sharing, token passing) * aplikacji (podział czasu procesora pomiędzy różne funkcje/moduły tego samego pojedynczego programu). My zajmiemy się tu współbieżnością widzianą z poziomu aplikacji. Funkcje setjmp() (ang. SET JuMP buffer - ustaw bufor umożliwiający skok do innego procesu) i longjmp() (ang. LONG JuMP - długi skok - poza moduł) wchodzą w skład standardu C i w związku z tym zostały "przeniesine" do wszystkich kompilatorów C++ (nie tylko Borlanada). Porozmawiajmy o narzędziach. Zaczniemy od klasycznego zestawu narzędzi oferowanego przez Borlanda. Aby zapamiętać stan przerwanego procesu stosowana jest w C/C++ struktura PSS (ang. Program Status Structure) o nazwie jmp_buf (JuMP BUFfer - bufor skoku). W przypadku współbieżności wielu procesów (więcej niż dwa) stosuje się tablicę złożoną ze struktur typu struct jmp_buf TablicaBuforow[n]; Struktura służy do przechowywania informacji o stanie procesu (rejestrach procesora w danym momencie) i jest predefiniowana w pliku SETJMP.H: typedef struct { unsigned j_sp, j_ss, j_flag, j_cs; unsigned j_ip, j_bp, j_di, j_es; unsigned j_si, j_ds; } jmb_buf[1]; Prototypy funkcji: int setjmp(jmp_buf bufor); void longjmp(jmp_buf bufor, int liczba); W obu przypadkach jmp_buf bufor oznacza ten sam typ bufora (niekoniecznie ten sam bufor - może ich być wiele), natomiast int liczba oznacza tzw. return value - wartość zwracaną po powrocie z danego procesu. Liczba ta może zawierać informację, z którego procesu nastąpił powrót (lub inną przydatną w programie), ale nie może być ZEREM. Jeśli funkcja longjmp() otrzyma argument int liczba == 0 - zwróci do programu wartość 1. Wartość całkowita zwracana przez funkcję setjmp() przy pierwszym wywołaniu jest zawsze ZERO a przy następnych wywołaniach (po powrocie z procesu) jest równa parametrowi "int liczba" przekazanemu do ostatnio wywołanej funkcji longjmp(). Przyjrzyjmy się temu mechanizmowi w praktyce. Wyobraźmy sobie, że chcemy realizować współbieżnie dwa procesy - proces1 i proces2. Proces pierwszy będzie naśladował w uproszczeniu wymieniony wyżej edytor tekstu - pozwoli na wprowadzanie tekstu, który będzie powtarzany na ekranie. Proces drugi będzie przesuwał w dolnej części ekranu swój numerek - cyferkę 2 (tylko po to, by było widać, że działa). Program główny wywołujący oba procesy powinien wyglądać tak: ... void proces1(void); void proces2(void); int main(void) { clrscr(); proces1(); proces2(); return 0; } Ależ tu nie ma żadnej współbieżności! Oczywiście. Aby zrealizować współbieżność musimy zadeklarować bufor na bieżący stan rejestrów i zastosować funkcje setjmp(): #include void proces1(void); void proces2(void); jmp_buf bufor1; int main(void) { clrscr(); if(setjmp(bufor1) != 0) proces1(); //Powrót z procesu2 był? proces2(); return 0; } Po wywołaniu funkcji setjmp() zostanie utworzony bufor1, w którym zostanie zapamiętany stan programu. Funkcja, jak zawsze przy pierwszym wywołaniu zwróci wartość ZERO, więc warunek if(setjmp(bufor1) != 0) ... nie będzie spełniony i proces1() nie zostanie wywołany. Program pójdzie sobie dalej i uruchomi proces2(): void proces2(void) { for(;;) { gotoxy(10,20); printf("PROCES 2: "); for(int i = 1; i<40; i++) { printf(".2\b"); delay(5); //UWAGA: delay() tylko dla DOS! } longjmp(bufor1, 1); <--- wróć } ____________ tę jedynkę zwróci setjmp() } Proces 2 będzie drukował "biegającą dwójkę" (zwolnioną przez opóźnienie delay(5); o pięć milisekund), poczym funkcja longjmp() każe wrócić z procesu do programu głównego w to miejsce: int main(void) { clrscr(); if(setjmp(bufor1)) proces1(); <--- tu powrót proces2(); return 0; } Zmieni się tylko tyle, że powtórnie wywołana funkcja setjmp() zwróci tym razem wartość 1, zatem warunek będzie spełniony i rozpocznie się proces1(): void proces1(void) { while(kbhit()) { gotoxy(1,1); printf("PROCES1, Pisz tekst: [Kropka - Koniec]"); gotoxy(pozycja,2); znak = getch(); printf("%c", znak); pozycja++; } if(znak == '.') exit (0); } Proces 1 sprawdzi przy pomocy funkcji kbhit() czy w buforze klawiatury oczekuje znak (czy coś napisałeś). Jeśli tak - wydrukuje znak, jeśli nie - zakończy się i program przejdzie do procesu drugiego. A oto program w całości: [P075.CPP] #include #include #include #include #include void proces1(void); void proces2(void); jmp_buf bufor1, bufor2; char znak; int pozycja = 1; int main(void) { clrscr(); if(setjmp(bufor1)) proces1(); proces2(); return 0; } void proces1(void) { while(kbhit()) { gotoxy(1,1); printf("PROCES1, Pisz tekst: [Kropka - Koniec]"); gotoxy(pozycja,2); znak = getch(); printf("%c", znak); pozycja++; } if(znak == '.') exit (0); } void proces2(void) { for(;;) { gotoxy(10,20); printf("PROCES 2: "); for(int i = 1; i<40; i++) { printf(".1\b"); delay(5); } longjmp(bufor1,1); } } [!!!] UWAGA ________________________________________________________________ Funkcja delay() użyta dla opóżnienia i zwolnienia procesów będzie funkcjonować tylko w środowisku DOS. Przy uruchamianiu prykładowego programu pod Windows przy pomocy BCW należy tę funkcję poprzedzić znakiem komentzrza // . ________________________________________________________________ Wyobrażmy sobie, że mamy trzy procesy. Przykład współbieżności trzech procesów oparty na tej samej zasadzie zawiera program poniżej [P076.CPP] #include #include #include #include #include void proces1(void); void proces2(void); void proces3(void); jmp_buf bufor1, bufor2; char znak; int pozycja = 1; int main(void) { clrscr(); if(setjmp(bufor1)) proces1(); if(setjmp(bufor2)) proces2(); proces3(); return 0; } void proces1(void) { while(kbhit()) { gotoxy(1,1); printf("PROCES1, Pisz tekst: [Kropka - Koniec]"); gotoxy(pozycja,2); znak = getch(); printf("%c", znak); pozycja++; } if(znak == '.') exit (0); } void proces2(void) { for(;;) { gotoxy(10,20); printf("PROCES 2: "); for(int i = 1; i<40; i++) { printf(".2\b"); delay(5); } longjmp(bufor1, 1); } } void proces3(void) { for(;;) { gotoxy(10,23); printf("PROCES 3: "); for(int i = 1; i<40; i++) { printf(".3\b"); delay(2); } longjmp(bufor2,2); } } Procesy odbywają się z różną prędkością. Kolejność uruchamiania procesów będzie: - proces3() - proces2() - proces1() Po uruchomieniu programu zauważysz, że proces pierwszy (pisania) został spowolniony. Można jednak temu zaradzić przez ustawienie flag i priorytetów. Jeśli dla przykładu uważamy, że pisanie jest ważniejsze, możemy wykrywać zdarzenie - naciśnięcie klawisza w każdym z mniej ważnych procesów i przerywać wtedy procesy mniej ważne. Wprowadzanie tekstu w przykładzie poniżej nie będzie spowolnione przez pozostałe procesy. [P077.CPP] #include #include #include #include #include void proces1(void); void proces2(void); void proces3(void); jmp_buf BuforStanu_1, BuforStanu_2; char znak; int pozycja = 1; int main(void) { clrscr(); if(setjmp(BuforStanu_1)) proces1(); if(setjmp(BuforStanu_2)) proces2(); proces3(); return 0; } void proces1(void) { while(kbhit()) { gotoxy(1,1); printf("PROCES1, Pisz tekst: [Kropka - Koniec]"); gotoxy(pozycja,2); znak = getch(); printf("%c", znak); pozycja++; } if(znak == '.') exit (0); } void proces2(void) { for(;;) { gotoxy(10,20); printf("PROCES 2: "); for(int i = 1; i<40; i++) { if(kbhit()) break; printf(".2\b"); delay(5); } longjmp(BuforStanu_1, 1); } } void proces3(void) { for(;;) { gotoxy(10,23); printf("PROCES 3: "); for(int i = 1; i<40; i++) { if(kbhit()) break; printf(".3\b"); delay(2); } longjmp(BuforStanu_2,2); } } [!!!]UWAGA ________________________________________________________________ W pierwszych dwu przykładach trzymanie stale wciśniętego klawisza spowoduje tylko automatyczną repetycję wprowadzanego znaku. W przykładzie trzecim spowoduje to przerwanie procesów 2 i 3, co będzie wyraźnie widoczne na monitorze (DOS). Zwróć uwagę, że kbhit() nie zmienia stanu bufora klawiatury. ________________________________________________________________ W bardziej rozbudowanych programach można w oparciu o drugi parametr funkcji longjmp() zwracany przez funkcję setjmp(buf) po powrocie z procesu identyfikować - z którego procesu nastąpił powrót i podejmować stosowną decyzję np. przy pomocy instrukcji switch: switch(setjmp(bufor)) { case 1 : proces2(); case 2 : proces3(); ..... default : proces0(); } [!!!]UWAGA ________________________________________________________________ * Zmienne sterujące przełączaniem procesów powinny być zmiennymi globalnymi, bądź statycznymi. Także dane, które nie mogą ulec nadpisaniu bezpieczniej potraktować jako globalne. ________________________________________________________________ W przypadku wielu procesów celowe jest utworzenie listy, bądź kolejki procesów. Przydatny do tego celu bywa mechanizm tzw. "łańcuchowej referencji". W obiektach klasy PozycjaListy należy umieścić pole danych - strukturę i pointer do następnego procesu, któremu (zgodnie z ustalonym priorytetem) należy przekazać sterowanie: static jmp_buf Bufor[m]; <-- m - ilość procesów ... class PozycjaListy { public: jmp_buf Bufor[n]; <-- n - Nr procesu PozycjaListy *nastepna; } Wyobrażmy sobie sytuację odrobinę różną od powyższych przykładów (w której zresztą para setjmp() - longjmp() równie często występuje. #include jmp_buf BuforStanu; int Nr_Bledu; int main(void) { Nr_Bledu = setjmp(BuforStanu) <-- tu nastąpi powrót if(Nr_Bledu == 0) <-- za pierwszym razem ZERO { /* PRZED powrotem z procesu (ów) */ .... Proces(); <-- Wywołanie procesu } else { /* PO powrocie z procesu (ów) */ ErrorHandler(); <-- obsługa błędów } .... return 0; } Taka struktura zapewnia działanie następujące: - Był powrót z procesu? NIE: Wywołujemy proces! TAK: Obsługa błędów, które wystąpiły w trakcie procesu. Jeśli teraz proces zaprojektujemy tak: void Proces() { int Flaga_Error = 0; ... /* Jeśli nastąpiły błędy, flaga w trakcie pracy procesu jest ustawiana na wartość różną do zera */ if(Error) Flaga_Error++; ... if(Fllaga_Error != 0) longjmp(BuforStanu, Flaga_Error); ... } proces przekaże sterowanie do programu w przypadku wystąpienia błędów (jednocześnie z informacją o ilości/rodzaju błędów). [Z] ________________________________________________________________ 1. Napisz samodzielnie program realizujący 2, 3, 4 procesy współbieżne. Jeśli chcesz, by jednym z procesów stał się całkowivie odrębny program - skorzystaj z funkcji grupy spawn...() umożliwiających w C++ uruchamianie procesów potomnych. ________________________________________________________________