SOP322/ćw - temat E

Wątki POSIX.

--> literatura :
    * Gray "Komunikacja miedzy procesami ...", rozdział 11
    * krótki spis wszystkich funkcji biblioteki pthread "pthread3.txt" (j. ang)
    * wstęp oraz opis wszystkich funkcji biblioteki pthread "pthr3big.txt" (j. ang)\
    * "POSIX Threads Programming" http://www.llnl.gov/computing/tutorials/pthreads/

--> niektóre aplikacje "z natury rzeczy" powinny składać się z wielu procesów (np serwery usług)
    takie rozwiązanie ma tę wadę, że jest kosztowne z punktu widzenia systemu operacyjnego
    gdyż tworzenie nowych procesów wymaga wielu zabiegów (ze strony s.o.),
    dlatego właśnie wymyślono wątki - ich tworzenie jest mniej kosztowne, a także
    komunikacja między wątkami jest łatwiejsza niż między procesami ...

--> do tworzenia programów wielowątkowych na Linuxie używamy biblioteki "pthread";
    zawiera ona procedury obsługi wątków zgodne z normą POSIX;
    nazwy wszystkich jej funkcji zaczynają się od przedrostka "pthread_";
    dokumentacja każdej funkcji jest dostępna w manualu oraz w "pthr3big.txt";
    (zauważmy że wątki są bardzo podobne do "procesów" języka BACI ...)

--> przypomnijmy najważniejsze cechy wątków:

--> dwa sposoby implementacji wątków w systemie operacyjnym:
       * wątki poziomu użytkownika
              -system operacyjny nie jest świadomy istnienia wątków
              -są zaimplementowane przy pomocy specjalnej biblioteki dołączanej do programów
              -zalety:
                  +szybkie przełączanie kontekstu między wątkami,
                  +planowanie przydziału procesora może zależeć od aplikacji
              -wady:
                  +jeśli fun. sys. blokuje, to wszystkie wątki są zablokowane (można to obejść !)
                  +nie można skorzystać z wielu procesorów
              -przykład tego typu implementacji wątków, to wątki POSIX (niekoniecznie !)
       * wątki poziomu jądra
              -wątki są zdefiniowane w systemie operacyjnym
              -przykład tego typu implementacji wątków to wątki Windows NT

--> funkcje biblioteki pthread zwracają wartość 0, jeśli zakończyły się sukcesem,
    zwracają wartość >0 jeśli zakończyły się błędem (to jest właśnie kod błędu);
    NIE modyfikują zmiennej "errno" !!!
 

Przykłady w plikach o nazwach "watek??.cc" kompiluje się następująco:

   g++ watek01.cc -lpthread -o watek01
(jeśli używamy funkcji matematycznych, np sin, to należy dołączyć dodatkowo bibliotekę matematyczną "m")
   g++ watek01.cc -lpthread -lm -o watek01

Ćwiczenie 70

Wypróbuj opisane niżej przykłady watek01.cc - watek05.cc.

--> "watek01.cc"
    jest to podstawowy przyklad w ktorym sa tworzone watki;
    przy pomocy funkcji pthread_create()
    watki te zwracaja swoj "id" (otrzymywany przez pthread_self());

    glowny watek (wykonujacy funkcje main()) po utworzeniu
    watkow probuje je wcielić przy pomocy pthread_join();
    (jest to operacja analogiczna do wait() w przypadku procesow)
    pobierajac przy okazji ich "status zakonczenia" - w tym wypadku ich "id";
       innymi słowy: przy pomocy pthread_join() można czekać na zakończenie
    innego wątku, różnica w porównaniu z procesami polega na tym że
    dowolny wątek może czekać na zakończenie dowolnego innego wątku
    (a nie tylko macierzysty na zakonczenie potomnego jak w przypadku
    procesów !)

    dopiero po wcieleniu watek oddaje zasoby
    (chyba ze jest to watek "odlaczony" = "detached"
    do odlaczania sluzy funkcja pthread_detach)

    watki wykonuja kod funkcji podanej przez 3 parametr funkcji pthread_create();
    funkcja ta otrzymuje jako 1 argument 4 parametr funkcji pthread_create()
    (można w ten sposób przekazać jakąś informacje do wątku)

    szczegolna role pelni typ (void*) - sluzy jako uniwersalny
    typ danych - np do przekazywania statusu zakonczenia
 

--> "watek02.cc"
    (kompilować TYLKO pod Digital Unix-em !)

    w tym przykladzie kazdy watek zawiera kod wykonujacy
    pętle z bardzo wieloma obrotami

    watki uzywaja "polityki przelaczania kontekstu" SCHED_FIFO
    (ktora podobno jest uzalezniona od priorytetow)

    jeden z watkow ma nizszy priorytet niz pozostale
    oznacza to, ze powinien pozostac nieco w tyle
    w porownaniu z innymi watkami (jest to wątek z "i=3")

    mozemy sie przekonac ze rzeczywiscie tak jest
    uruchamiajac ten program i przeadresowujac do pliku

      watek02 > watek02.txt
    aby zmienic "polityke przelaczania" i "priorytety"
    nalezy odpowiednie informacje umiescic w strukturze
    atrybutow, a nastepnie podac ją jako drugi parametr funkcji pthread_create()
    funkcje z rodziny "pthread_attr_*" służą wlasnie
    do manipulowania strukturą atrybutów ...
 

--> "watek03.cc"
    w tym przykladzie mamy zastosowanie MUTEX-ow

    mutex <=> mutual exclusion <=> wzajemne wykluczanie

    mutex-y są podobne do semaforów binarnych ...

    ------------- własności muteksów -------------

    ---------------------------------------------------

    istnieja mutexy "wewnątrzprocesowe" i "miedzyprocesowe"
    (miedzyprocesowe musza byc umieszczane we wspolnej pamieci)

    deklaracja mutexu (wewnątrzprocesowego):

      pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
    do zamykania i otwierania muteksow slużą następujące funkcje:
      pthread_mutex_lock(&m); // zamkniecie;
                // jesli nie jest mozliwe to funkcja blokuje

      pthread_mutex_unlock(&m); // otwarcie

      pthread_mutex_trylock(&m); // zamkniecie bez blokowania
                // jesli niemozliwe to zwraca EBUSY
    w przykladzie "watek03.cc" istnieja dwie zmienne globalne
    (dostepne dla wszystkich tworzonych watkow):  konto1, konto2
    kazdy watek probuje dokonywać "przelewów" tj operacji:
     konto1-=kwota; konto2+=kwota;
    gdzie kwota jest losowo wybraną liczbą; oczywiscie na koncu suma konto1+konto2
    powinna byc taka sama jak na poczatku !
 
Zadanie 71

Dodaj wątek kontrolny do przykładu "watek03.cc" i sprawdź co się dzieje przy włączonych i wyłączonych muteksach.

 

--> zmienne warunkowe CONDITION

     Przy pomocy zmiennych condition, muteksów oraz klasy C++ można skonstruować coś
     co przypomina monitor ...

W przykładzie "watek04.cc" mamy klasę/monitor, posiadającą jedną procedurę o nazwie "Procedura" (wykorzystuje się tutaj muteks, aby zapewnić że tylko jeden wątek może przebywać w monitorze); zwróć uwagę na sposób inicjowania muteksu w klasie (jedyny możliwy !); wszystkie wątki próbują wywoływać tę procedurę w pętli, każde wywołanie procedury zwiększa wartość zmiennej liczCalosciowy, jest także zapamiętywane ile razy dany wątek "przebywał" w monitorze. Wypróbuj ten przykład; sprawdź co się stanie po "wzięciu w komentarz"  operacji zamykania/otwierania muteksu w procedurze Procedura.

     Podobnie jak to było w monitorach, zmienne condition pozwalają zorganizować
     "kolejkę uśpionych procesów, oczekujących na jakieś zdarzenie" ...

     Naszkicujemy teraz ogólnie, jak rozwiązać
     problem producenta/konsumenta (nieskończony bufor) ...

      pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
          // muteks, który "ochrania" dostep do bufora

      pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
          // deklaracja zmiennej "condition"
      //
      // ------------ konsument ------------
      //
      while(1)
      {
        pthread_mutex_lock(&mutex);
          // zamykamy muteks

        while( 'bufor jest pusty' )
          pthread_cond_wait(&cond, &mutex);
            /* powoduje ze mutex zostanie otworzony a "konsument" uśpiony 
               i umieszczony w kolejce uśpionych procesów zmiennej "cond"

               gdy "producent" obudzi "konsumenta" wtedy 
               muteks zostanie automatycznie zamknięty a konsument 
               będzie kontynuował działanie ...

               [pętla "while(warunek)" jest podobno niezbędna 
               nie wystarczy "if(warunek)" ...]
            */

        //
        // teraz czytamy z bufora ...
        //

        pthread_mutex_unlock(&mutex);            
          // otwieramy muteks
      
        //
        // konsument robi coś innego ...
        //
      }

      //
      // ------------ producent ------------
      //
      while(1)
      {
        pthread_mutex_lock(&mutex);
        
        //
        // piszemy do bufora ...
        //
        
        pthread_cond_signal(&cond);
          // budzimy "konsumenta"

        pthread_mutex_unlock(&mutex);
          // "konsument" zacznie działać dopiero, gdy otworzymy mutex (???)

        //
        // producent robi coś innego ...
        //

      }
W przykładzie "watek05.cc" mamy rozwiązanie problemu producenta/konsumenta z nieskończonym buforem za pomocą monitora (czyli klasy C++ z muteksami i zmiennymi conditon) o nazwie "Bufor". Przetestuj ten przykład.
Uwaga: zauważ że procesor może się przełączyć na inny wątek gdy dany konsument odczytał element ale jeszcze go nie wypisał na terminalu - dlatego ten przykład niekoniecznie wyświetla kolejne liczby!; aby to wymusić wystarczy operacje "odczytywanie elem+wypisywanie elem" potraktować jako sekcje krytyczna.
 
Zadanie 72

Sprawdź, czy w przykładzie "watek05.cc" rzeczywiście konsument zostaje wznowiony dopiero, gdy producent otworzy muteks (a nie zaraz po pthread_cond_signal()).

Zadanie 73 (*)


Rozwiąż problem producenta/konsumenta, w wersji ze skończonym buforem.
  • Użyj muteksów i zmiennych warunkowych, zgodnie z powyższym szkicem i przykładem "watek05.cc" (uwzględniając, że w tym zadaniu ma być skończony bufor).
  • Rozwiązanie powinno przypominać rozwiązanie przy pomocy monitora: dlatego użyj klasy języka C++; każdą funkcję składową zaczynaj od zamknięcia, a kończ otwarciem muteksu, który jest zmienną składową klasy; oczywiście zmienne condition także są zmiennymi składowymi klasy.
  • W klasie/monitorze powinny znaleźć się dwie funkcje składowe: WstawElement(), PobierzElement(). Oczywiście producent i konsument wywołują swoje funkcje w pętli.  Dodaj kontrolę poprawności działania (niech producent produkuje kolejne liczby 0..999, a konsument wyświetla je na ekranie).
  • Sprawdź czy Twoje rozwiązanie jest poprawne gdy jest 1 producent i 10 konsumentów. Zauważ ze tylko jeden z konsumentów otrzyma 999 i będzie wiedział że powinien się skończyć; dodaj mechanizm aby także inni konsumenci się wtedy skończyli (użyj funkcji pthread_cancel(); aby pthread_cancel() dzialalo nalezy na poczatku funkcji watkowej wykonać:
        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
  •     pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
    ).

    Zadanie 74


    Napisz program tworzący dwa procesy PM i PP, które komunikuja sie za pomoca polaczenia "gniazdkowego" (można użyć socketpair(); pamiętaj ze polaczenia "gniazdkowe" są dwukierunkowe !)

    Porównaj wyniki działania programu na różnych systemach operacyjnych.
    Dodatkowe wskazówki:
    wątek 1 tworzy napisy przy pomocy sprintf() i wysyła go używając długiego bufora (np 50-bajtowego);
    wątki 2,3,4 czytają dane także używając długiego bufora;
    chodzi o zaobserwowanie że w niektórych os komunikaty wracają pocięte przypadkowo, w innych przychodzą w postaci niezmienionej ...