V sérii linuxových vláken jsme diskutovali o způsobech, jak může vlákno ukončit a jak je návratový stav předán z ukončujícího vlákna jeho nadřazenému vláknu. V tomto článku vrhneme trochu světla na důležitý aspekt známý jako synchronizace vláken.
Linux Threads Series:část 1, část 2, část 3, část 4 (tento článek).
Problémy se synchronizací vláken
Vezměme si příklad kódu ke studiu problémů synchronizace:
#include<stdio.h> #include<string.h> #include<pthread.h> #include<stdlib.h> #include<unistd.h> pthread_t tid[2]; int counter; void* doSomeThing(void *arg) { unsigned long i = 0; counter += 1; printf("\n Job %d started\n", counter); for(i=0; i<(0xFFFFFFFF);i++); printf("\n Job %d finished\n", counter); return NULL; } int main(void) { int i = 0; int err; while(i < 2) { err = pthread_create(&(tid[i]), NULL, &doSomeThing, NULL); if (err != 0) printf("\ncan't create thread :[%s]", strerror(err)); i++; } pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); return 0; }
Výše uvedený kód je jednoduchý, ve kterém jsou vytvořena dvě vlákna (úlohy) a ve funkci start těchto vláken je udržován čítač, přes který uživatel získává protokoly o čísle úlohy, která byla spuštěna a kdy byla dokončena. Kód a tok vypadají dobře, ale když vidíme výstup:
$ ./tgsthreads Job 1 started Job 2 started Job 2 finished Job 2 finished
Pokud se zaměříte na poslední dva protokoly, uvidíte, že protokol „Úloha 2 dokončena“ se opakuje dvakrát, zatímco pro „Úloha 1 dokončena“ není vidět žádný protokol.
Pokud se nyní vrátíte ke kódu a pokusíte se najít nějakou logickou chybu, pravděpodobně žádnou chybu nenajdete snadno. Ale pokud se blíže podíváte a představíte si provádění kódu, zjistíte, že:
- Protokol „Úloha 2 zahájena“ se vytiskne hned po „Zahájení úlohy 1“, takže lze snadno usoudit, že zatímco vlákno 1 zpracovávalo, plánovač naplánoval vlákno 2.
- Pokud byl výše uvedený předpoklad pravdivý, pak se hodnota proměnné „counter“ znovu zvýšila před dokončením úlohy 1.
- Když tedy byla úloha 1 skutečně dokončena, nesprávná hodnota čítače vytvořila protokol 'Úloha 2 dokončena' následovaná 'Úloha 2 dokončena' pro skutečnou úlohu 2 nebo naopak, protože to závisí na plánovači.
- Takže vidíme, že problém není v opakujícím se protokolu, ale v nesprávné hodnotě proměnné „counter“.
Skutečným problémem bylo použití proměnné „counter“ druhým vláknem, když ji první vlákno používalo nebo se chystalo použít. Jinými slovy můžeme říci, že problémy způsobila nedostatečná synchronizace mezi vlákny při používání sdíleného prostředku ‚counter‘ nebo jedním slovem můžeme říci, že k tomuto problému došlo kvůli ‚problému synchronizace‘ mezi dvěma vlákny.
Mutexy
Nyní, když jsme pochopili základní problém, pojďme diskutovat o jeho řešení. Nejoblíbenějším způsobem, jak dosáhnout synchronizace vláken, je použití mutexů.
Mutex je zámek, který nastavujeme před použitím sdíleného zdroje a uvolňujeme po jeho použití. Když je zámek nastaven, žádné jiné vlákno nemá přístup k uzamčené oblasti kódu. Vidíme tedy, že i když je vlákno 2 naplánováno, zatímco vlákno 1 neprovedlo přístup ke sdílenému prostředku a kód je uzamčen vláknem 1 pomocí mutexů, vlákno 2 nemůže ani přistupovat k této oblasti kódu. To zajišťuje synchronizovaný přístup ke sdíleným zdrojům v kódu.
Interně to funguje následovně:
- Předpokládejme, že jedno vlákno uzamklo oblast kódu pomocí mutex a spouští tuto část kódu.
- Pokud se nyní plánovač rozhodne přepnout kontext, všechna ostatní vlákna, která jsou připravena spustit stejnou oblast, budou odblokována.
- Pouze jedno ze všech vláken se dostane ke spuštění, ale pokud se toto vlákno pokusí spustit stejnou oblast kódu, která je již zamčená, znovu přejde do režimu spánku.
- Přepínání kontextu bude probíhat znovu a znovu, ale žádné vlákno nebude schopno spustit uzamčenou oblast kódu, dokud nebude uvolněn zámek mutexu nad ní.
- Zámek Mutex uvolní pouze vlákno, které jej uzamklo.
- Takže to zajistí, že jakmile vlákno uzamkne část kódu, žádné jiné vlákno nemůže spustit stejnou oblast, dokud nebude odemčeno vláknem, které jej uzamklo.
- Tento systém tedy zajišťuje synchronizaci mezi vlákny při práci na sdílených zdrojích.
Mutex je inicializován a poté je dosaženo uzamčení voláním následujících dvou funkcí:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_lock(pthread_mutex_t *mutex);
První funkce inicializuje mutex a prostřednictvím druhé funkce lze uzamknout jakoukoli kritickou oblast v kódu.
Mutex lze odemknout a zničit voláním následujících funkcí:
int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_destroy(pthread_mutex_t *mutex);
První výše uvedená funkce uvolní zámek a druhá funkce zámek zničí, takže jej nelze v budoucnu nikde použít.
Praktický příklad
Podívejme se na část kódu, kde se pro synchronizaci vláken používají mutexy
#include<stdio.h> #include<string.h> #include<pthread.h> #include<stdlib.h> #include<unistd.h> pthread_t tid[2]; int counter; pthread_mutex_t lock; void* doSomeThing(void *arg) { pthread_mutex_lock(&lock); unsigned long i = 0; counter += 1; printf("\n Job %d started\n", counter); for(i=0; i<(0xFFFFFFFF);i++); printf("\n Job %d finished\n", counter); pthread_mutex_unlock(&lock); return NULL; } int main(void) { int i = 0; int err; if (pthread_mutex_init(&lock, NULL) != 0) { printf("\n mutex init failed\n"); return 1; } while(i < 2) { err = pthread_create(&(tid[i]), NULL, &doSomeThing, NULL); if (err != 0) printf("\ncan't create thread :[%s]", strerror(err)); i++; } pthread_join(tid[0], NULL); pthread_join(tid[1], NULL); pthread_mutex_destroy(&lock); return 0; }
Ve výše uvedeném kódu:
- Na začátku hlavní funkce je inicializován mutex.
- Stejný mutex je uzamčen ve funkci ‘doSomeThing()’ při používání sdíleného prostředku ‘counter’
- Na konci funkce ‘doSomeThing()’ se odemkne stejný mutex.
- Na konci hlavní funkce, když jsou obě vlákna hotová, je mutex zničen.
Nyní, když se podíváme na výstup, najdeme :
$ ./threads Job 1 started Job 1 finished Job 2 started Job 2 finished
Vidíme tedy, že tentokrát byly přítomny protokoly zahájení a ukončení obou úloh. Synchronizace vláken tedy probíhala pomocí Mutexu.