Synchronizacia v Linuxe - mutexy

Tomáš Plch  /  07. 03. 2007, 00:00

Mutual exclusion - vzajomné vylúčenie - je jedena z najpoužívanejších metód synchronizácie. V dnešnom článku si povieme o tom ako a prečo používať toto synchronizačné primitívum v operačnom systéme Linux.

Vzájomné vylúčenie - mutual exclusion - je návrhová technika, ktorá nám umožní zabezpečiť výhradný prístup do kritických častí kódu len jedným vláknom. Pre zopakovanie, kritická časť kódu je blok kódu, ktorý by mohol potenciálne viesť k rôznym výsledkom, ak by ho vykonávali viaceré vlákna súbežne.

Inými slovami, potrebujeme vlákna pri vykonávaní "zoradiť sekvenčne". Toto tvrdenie nieje úplne pravdivé, ale to si ukážeme až neskôr v článku.

Začneme tým, že si povieme, čo potrebujeme na dosiahnutie vzájomného vylúčenia. Prostriedok na vzájomné vylúčenie sa volá mutex. V princípe sa jedná o binárny semafor.
Vnútorná implementácia má často priamo podporu v jadre operačného systému.

Z všeobecného pohľadu poznáme štyri operácie nad mutexom.
Máme mutex, ktorý si nazveme M.

init(M) - inicializácia mutexu do odomknutého stavu
lock(M) - zamkne mutex, ak je zamknutý, čakáme až sa odomkne a potom ho zamkneme (čakanie je realizované napríklad uspaním vláken)
unlock(M) - odomkne mutex a prípadne upozorní na to čakateľov na zamknutie
destroy(M) - zničí mutex, ten je už ďalej nepoužitelný

k týmto operáciám by sme ešte mohli pridať operáciu trylock(M), ktorá sa pokúsi zamknúť mutex a ak sa to nepodarí (mutex je zamknutý) vráti chybovú hodnotu. Použitelnou operáciou by mohla byť operácia trylock_timeout(M,t), ktorá sa pokúsi zamknúť mutex a bude čakať len po dobu t.

Teória okolo mutexov je trošku zložitejšia, ale my si zatiaľ vystačíme so zjednodušenou predstavou.

Použitie mutexov je celkom jednoduché, stačí mutex vytvoriť a potom kritické oblasti kódu obaliť operáciami lock a unlock.
Treba dať pozor aby ku každej operácii lock bola operácia unlock. V opačnom prípade môže dôjsť k nedefinovanému chovaniu, dokonca deadlocku (situácia keď všetci čakajú a nikto nikdy neodomkne mutex, lebo aj on čaká).

Z praktického pohľadu vieme rozlíšiť viaceré druhy mutexov, hlavne čo sa do chovania voči operáciam týka - štandardné mutexy, rekurzívne mutexy, debugovacie mutexy. Štandardný mutex sa správa presne ako sme popísali o niečo vyššie. V prípade rekurzívneho mutexu môže jedno vlákno zamknúť jeden mutex aj viackrát bez toho aby sa mutex zablokoval (deadlock). Avšak musí urobiť rovnaký počet unlock operácií aby sa mutex dostal do stavu odomknutý.
Debugovací mutex je pre určený na ladenie, ak by bolo potrebné sledovať kto vlastní mutex, prípadne v akom je stave.

Neodporúča sa aby jedno vlákno druhému odomykalo mutex. Inými slovami, každé vlákno zamyká a odomyká veci ktoré mu "patria".

Ďalšou dôležitou skutočnosťou je, že mutexy nemusia zaručiť serializáciu prístupu. Čo to v realite znamená?
Ilustrujeme si to na príklade (v pseudoC kóde)

Vlákno A:x = 0;

while(true){
   lock(M);
   if ( x == 2 ){
      unlock(M);
      break;
   }
   unlock(M);
}
Vlákno B:
lock(M);
x++;
unlock(M);
lock(M);
x++;
unlock(M);

Ak spustíme prv vlákno A a potom vlákno B, môže sa stať, ze dôjde k vyhladoveniu vlákna B. Pri unlock operácii sa uvoľnia všetci (v tomto prípade vlákno B) a znova súťažia o operáciu lock. No pokiaľ vláknu A ostane čas(kvantum) a nieje medzi operáciou unlock a lock preplánované, získa znova mutex a vlákno B sa znova pri snahe zamknúť mutex zablokuje.
POSIX štandard v prípade vláken nezaručuje serializáciu prístupu, a ilustrovaná situácia môže bez problémov nastať.

Dosť bolo teórie, prejdeme do praxe.
Knižnice implementujúce štandard POSIX pre vlákna sa obvykle nazývajú pthread.

Pri kompilácii by ste mali uviesť parameter -lpthread. Tento parameter povie linkeru, že má prilinkovať knižnicu s názvom pthread.

Pravdaže ešte potrebujeme hlavičkový súbor. Ten sa skrýva pod názvom pthread.h.
Takže do kódu stačí vložiť
#include <pthread.h>

Funkcie a typy na prácu s mutexami sú nasledovné.

pthread_mutex_t je typ reprezentujúci mutex. Tento typ je možné nainicializovať buď pomocou funkcie alebo staticky pomocou inicializačných makier PTHREAD_MUTEX_INITIALIZER pre takzvané rýchle mutexy, ktoré využívajú futexy (fast mutex). Futexy presahujú tému tohoto článku a sú principiálne nezaujímavé.
Ďalšie makrá sú PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP pre rekurzívne mutexy a PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP pre mutexy umožňujúce jednoduché debugovanie. Debugovacími mutexami sa zaoberať nebudeme.
V princípe sa len mení chovanie jednotlivých funkcií, ktoré okrem samotného zamykania a odomykania kontrolujú aktuálneho vlastníka mutexu a prípadne vracajú chybové kódy.
 
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

Je funkcia, ktorá nainicializuje mutex. Pokiaľ je v druhom parametre uvedené NULL, mutex je uvedený do default stavu. V opačnom prípade treba použiť funkcie pre manipuláciu s atribútami. Najzaujímavejšie sú funkcie

pthread_mutexattr_init(), pthread_mutexattr_destroy(), pthread_mutexattr_settype()

ostatné sú nepodporované.

Funkcie pthread_mutexattr_init() a pthread_mutexattr_destroy() inicializujú a likvidujú poskytnutú premennú typu pthread_mutexattr_t.
Jediná zatiaľ podporovaná funkcia je pthread_mutexattr_settype(), inicializuje typ mutexu z možných druhov - PTHREAD_MUTEX_NORMAL, PTHREAD_MUTEX_ERRORCHECK, PTHREAD_MUTEX_RECURSIVE, PTHREAD_MUTEX_DEFAULT.

Jedná sa o rovnaké typy ako v prípade statických inicializátorov, s rozdielom medzi NORMAL a DEFAULT mutexom. DEFAULT mutex sa bude nachádzať v nedefinovanom stave, ak sa ho pokúsite znova zamknúť (rekurzívne), normal mutex zablokuje volanie uzamknutia a tak spôsobí deadlock.

int pthread_mutex_lock(pthread_mutex_t *mutex);
Zamkne mutex. Vráti 0 ak je všetko v poriadku, inak došlo k nejakej chybe. Najzaujímavejšia chyba je EINVAL, ktorá nastane ak sa snažíte zamknúť neplatný mutex. Neplatný mutex nastane napríklad po zničení mutexu. Sami si už určite domyslíte použitie.
Ak mutex má zamknuté iné vlákno, táto funkcia uspí volajúce vlákno a čaká na jeho uvoľnenie. Vlákno je zobudené buď v prípade chyby alebo úspešného zamknutia mutexu.

int pthread_mutex_trylock(pthread_mutex_t *mutex);
Správa sa identicky s pthread_mutex_lock() okrem toho ak je mutex zamknutý, vráti hodnotu EBUSY a neblokuje vlákno.

int pthread_mutex_unlock(pthread_mutex_t *mutex);
Odomkne mutex. V prípade nejakej chyby vráti nenulovú hodnotu podobne ako pthread_mutex_lock().

int pthread_mutex_destroy(pthread_mutex_t *mutex);
Zničí mutex. Ak niekto na danom mutexe spí, zobudí ho to a vráti chybovú hodnotu EINVAL.

Na koniec si ukážeme krátku ukážku kódu. Tento príklad je veľmi zjednodušenou ukážkou toho ako sa dá s mutexami narábať. Vo výstupe by nemali byť popremiešané výpisy čísel a písmen. Malo by to vyzerať takto
123123abc123abcabc a nie 12ab3cabc.
#include <pthread.h>

int i;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_t th1, th2;

void * f1( void * param ){
    for ( i = 0; i < 10; i++ ){
        pthread_mutex_lock(&m);
        printf("%d",1);
        printf("%d",2);
        printf("%d",3);
        pthread_mutex_unlock(&m);
    }
}

void * f2( void * param ){

    for ( i = 0; i < 10; i++ ){
        pthread_mutex_lock(&m);
        printf("%c",'a');
        printf("%c",'b');
        printf("%c",'c');
        pthread_mutex_unlock(&m);
    }
}

int main ( int argc, char ** argv ){

    printf("n");

    pthread_create( &th1, NULL, f1, NULL);
    pthread_create( &th2, NULL, f2, NULL);

    pthread_mutex_join(th1,NULL);
    pthread_mutex_join(th2,NULL);

    pthread_mutex_destroy(&m);
}

<= predchádzajúci článok     nasledujúci článok =>

Neprehliadnite: