Linux - Vlákna (Threads)

Vykonávání programu může být rozděleno do více "proudů", vláken, které běží paralelně. Vlákna tedy, stejně jako procesy, umožňují aplikaci dělat více věcí najednou. Vlákno existuje uvnitř procesu. Při spuštění programu se vytvoří proces a v něm jedno vlákno, které může spustit další. Všechna tato vlákna běží paralelně ve stejném procesu, ale mohou vykonávat různé části (kód) programu.
Dvě různá vlákna téhož procesu sdílejí stejnou paměť, deskriptory souborů a jiné systémové zdroje přidělené celému procesu. Protože proces a všechna jeho vlákna vykonávají jeden program, když jedno vlákno zavolá exec, všechna ostatní vlákna jsou ukončena.
GNU/Linux implementuje API vláken podle standardu POSIX (pthreads). Všechny funkce a datové typy jsou deklarovány v <pthread.h>. Funkce nejsou ve standardní C knihovně, ale v knihovně libpthread, takže je potřeba linkovat s parametrem -lpthread.

Vytvoření vlákna

#include <bits/pthreadtypes.h>
typedef unsigned long int pthread_t;

pthread_self(3)
#include <pthread.h>
pthread_t pthread_self(void);
  • vrací id vlákna

Př. Napište program, který vypíše ID svého hlavního vlákna. Program nemažte, budete ho rozšiřovat o další probírané funkce!

pthread_equal(3)
#include <pthread.h>
int pthread_equal(pthread_t thread1, pthread_t thread2);
  • vrací nenulové číslo, když thread1 a thread2 jsou id téhož vlákna, jinak 0

Po spuštění každé vlákno vykonává funkci vlákna. Když tato funkce skončí, skončí i vlákno. Má jediný parametr void*, vrací také void*.

pthread_create(3)
#include <pthread.h>
int pthread_create(pthread_t * thread, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg);
  • vytvoří nové vlákno, které vykoná funkci start_routine s parametrem arg
  • attr specifikuje atributy vlákna (viz pthread_attr_init), pokud je NULL, jsou defaultní: joinable, defaultní priorita
  • vrací 0 a id vlákna na místo, kam ukazuje argument thread, při chybě nenulové číslo
  • funkce se vrací ihned, obě vlákna pak pokračují asynchronně

Př. Vytvořte vlákno s defaultními atributy a bez parametrů funkce vlákna.

Při vytváření atributů vlákna se postupuje takto:

S jednou strukturou je možné vytvořit více vláken a je možné ji zrušit po vytvoření vlákna.

pthread_attr_init(3)
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
  • inicializuje strukturu atributů defaultními hodnotami
  • vrací 0, při chybě nenulové číslo

pthread_attr_destroy(3)
#include <pthread.h>
int pthread_attr_destroy(pthread_attr_t *attr);
  • zruší strukturu atributů, před dalším použitím se musí znova inicializovat
  • vrací 0, při chybě nenulové číslo
  • v Linuxu nedělá nic

pthread_attr_setdetachstate(3)
pthread_attr_getdetachstate(3)
#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
  • nastaví(vrací) atribut z(do) druhého parametru
  • vrací 0, při chybě nenulové číslo
  • atribut detachstate může být:
    • PTHREAD_CREATE_JOINABLE - vlákno bude joinable, defaultní
    • PTHREAD_CREATE_DETACHED - detached

Joinable vlákno není po skončení automaticky odstraněno ze systému, jeho status ukončení je udržován, dokud si ho nevyzvedne jiné vlákno pomocí pthread_join (stejné jako zombie u procesu). Detached vlákno je naopak hned po skončení automaticky odstraněno a nelze získat jeho informace o ukončení pomocí pthread_join.

Př. Změňte vytvářené vlákno na detached.

Typ argumentu funkce vlákna je void*, nelze tedy poslat data (>4B) přímo. Předává se ukazatel na strukturu dat. Tato struktura ale nesmí být lokální v jednom vláknu (př. hlavním), protože by mohlo skončit dříve než ostatní, která tuto strukturu používají. Pokud je lokální, možným řešením je ve vláknu počkat, až ostatní vlákna skončí. Struktura se nesmí dealokovat dřív, než vlákna, která ji používají, skončí.

Př. Předejte funkci vlákna 2 řetězce jako parametry, ty spojte a výsledek vraťte jako návratovou hodnotu funkce vlákna.

pthread_join(3)
#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);
  • pozastaví vlákno, dokud neskončí vlákno specifikované pomocí th
  • pokud thread_return není NULL, vrátí návratovou hodnotu funkce vlákna na místo, kam ukazuje, pokud je vlákno zrušeno, je tam PTHREAD_CANCELED
  • jen jedno vlákno může čekat na skončení jiného vlákna
  • vrací 0, při chybě nenulové číslo
  • vlákno nesmí čekat samo na sebe, to je deadlock!

Př. Počkejte na dokončení vlákna a vypište výsledek spojení.

pthread_detach(3)
#include <pthread.h>
int pthread_detach(pthread_t th);
  • "přepne" vlákno na detached, pokud jiné vlákno nečeká na jeho ukončení
  • vrací 0, při chybě nenulové číslo

Ukončení vlákna

pthread_exit(3)
#include <pthread.h>
void pthread_exit(void *retval);
  • ukončí vlákno s návratovým kódem retval
  • nikdy se neukončuje

pthread_cancel(3)
#include <pthread.h>
int pthread_cancel(pthread_t thread);
  • pošle vláknu, specifikovanému pomocí thread, požadavek na ukončení
  • vrací 0, při chybě nenulové číslo

Vlákno často vykonává kód, který nesmí být přerušen. To je zajištěno tím, že vlákno samo si může určit místa, kde může být zrušeno a kde ne, pomocí svého stavu:

pthread_setcanceltype(3)
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
  • změní typ reakce na zrušení vlákna na type
  • type může být:
    • PTHREAD_CANCEL_ASYNCHRONOUS - asynchronní
    • PTHREAD_CANCEL_DEFERRED - synchronní
  • pokud oldtype není NULL, na místo, kam ukazuje, je uložen předchozí typ
  • vrací 0, při chybě nenulové číslo

pthread_testcancel(3)
#include <pthread.h>
void pthread_testcancel(void);
  • vytvoří bod zrušení
  • existují implicitní body zrušení (viz man), př. pthread_join

Př. Změňte vlákno na synchronně zrušitelné a vytvořte body zrušení před a po spojení.

pthread_setcancelstate(3)
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
  • změní stav zrušení vlákna na state
  • state může být:
    • PTHREAD_CANCEL_ENABLE - zrušitelný stav
    • PTHREAD_CANCEL_DISABLE - nezrušitelný
  • pokud oldstate není NULL, na místo, kam ukazuje, je uložen předchozí stav
  • vrací 0, při chybě nenulové číslo
  • umožňuje vymezení kritických sekcí - nepřerušitelný kód

Př. Uzavřete spojení do kritické sekce.

PŘÍKLAD

Lokální uložení dat

Vlákna sdílejí stejný adresový prostor. Někdy je ale potřeba, aby každé vlákno mělo vlastní kopii určité globální proměnné. Každé vlákno poskytuje lokální uložení dat (thread-specific data - TSD - area), kde se ukládají kopie globálních proměnných specifické pro každé vlákno. Proměnné v tomto uložení jsou indexovány přes klíče. Klíče jsou společné všem vláknům, hodnota proměnné je v každém vláknu jiná.

pthread_key_create(3)
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *));
  • vytvoří nový TSD klíč a uloží ho, kam ukazuje key, asociovaná hodnota je NULL
  • pokud destr_function není NULL, je to funkce, která se zavolá po ukončení vlákna s hodnotou asociované proměnné jako parametrem, před jejím voláním je ke klíči asociován NULL
  • vrací 0, při chybě nenulovou hodnotu

pthread_key_delete(3)
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
  • zruší TSD klíč
  • vrací 0, při chybě nenulovou hodnotu

pthread_setspecific(3)
#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *pointer);
  • změní hodnotu proměnné asociované s klíčem key na pointer
  • vrací 0, při chybě nenulovou hodnotu

pthread_getspecific(3)
#include <pthread.h>
void * pthread_getspecific(pthread_key_t key);
  • vrací hodnotu proměnné asociované s klíčem key, při chybě NULL

PŘÍKLAD

OTÁZKA: Procesy versus vlákna? Najděte co nejvíc rozdílů.

Další funkce týkající se vláken jsou:



Jan Outrata
outrata@phoenix.inf.upol.cz