Hlavní navigace

Programování pod Linuxem pro všechny (16)

Jakub Matys

Dnes si povíme něco o čistících obslužných funkcích, které lze použít k dealokaci systémových zdrojů vláken a začneme řešit problémy synchronizace.

Čistící funkce

Čistící funkce lze využít k dealokaci systémových zdrojů, což je velmi důležité při nepředpokládaném ukončení vlákna. Proto je často užitečné vytvořit čistící funkci i v případě, kdy se nevytváří speciální datová oblast, která má duplikovat jistá data pro každé vlákno. Linux za tímto účelem poskytuje čistící obslužné funkce (cleaning handlers).

Čistící obslužná funkce je funkce, která by měla být volána v okamžiku ukončení vlákna. Čistící funke má jeden parametr typu void * a její argument se předkládá při tzv. registraci čistící obslužné funkce. Pak je jednoduché použít tutéž čistící obslužnou funkci pro dealokaci několika systémových zdrojů.

Čistící funkce je dočasné zařízení používané k dealokaci systémových zdrojů, a to pouze v případě, že vlákno existuje nebo je zrušeno, přičemž se nedokončí jistá část jeho kódu. Za normálních okolností, když vlákno nekončí a není zrušeno, by měly být systémové zdroje dealokovány explicitně a čistící obslužná funkce by měla být odstraněna.

K registraci čistící funkce se používá funkce pthread_cleanup_push(), které se předává ukazatel na čistící funkci a hodnota argumentu typu void *. Volání funkce pthread_cleanup_push() musí být vyváženo odpovídajícím voláním funkce pthread_cleanup_pop(), která zruší registraci čistící obslužné funkce. Podle zavedené konvence má funkce pthread_cleanup_pop() jeden argument typu int. Je-li hodnota tohoto argumentu nenulová, bude čistící funkce před zrušením její registrace zavolána.

Příklad ukazuje, jak můžete použít čistící funkci k zajištění dealokace dynamicky alokované vyrovnávací paměti. Dealokace se provede předtím, než se vlákno ukončí.

#include <malloc.h>
#include <pthread.h>

/* Alokuje buffer */

void *allocate_buffer(size_t size)
{
  return(malloc(size));
}

/* Dealokuje buffer */

void deallocate_buffer(void *buffer)
{
  free(buffer);
}

void work()
{
  /* Alokace bufferu */
  void *temp_buffer = allocate_buffer(1024);
  /* Registrace cistice obsluzne funkce pro tento buffer */
  pthread_cleanup_push(deallocate_buffer, temp_buffer);

  /* Zde funkce pokracuje v nejake praci */

  /* Odregistrace cistici funkce. Protoze je jako argument pouzita nenulova hodnota, bude zavolana funkce deallocate_buffer(), ktera zrusi buffer. */
  pthread_cleanup_pop(1);
} 

Protože argument funkce pthread_cleanup_pop() není nulový, čistící funkce deallocate_buffer() se volá automaticky, a proto se vyrovnávací paměť nemusí dealokovat explicitně. V tomto jednoduchém příkladu bychom mohli použít standardní knihovní funkci free() místo funkce deallocace_buf­fer().

Synchronizace a kritické sekce

Nejčastější chybou v souvislosti s vlákny je, že vlákna zpřístupňují tutéž část paměti. Na jedné straně se jedná o velkou výhodu, na druhé straně však tato skutečnost může být příčinou těžko odhalitelných chyb. Když je jedno vlákno na polovině cesty při zpracování dat ve sdílené paměni a druhé začné s těmito daty pracovat, je problém takřka jistý. Proto často chybně fungující program obsahuje kód, ve kterém běží vše dobře jen tehdy, když se jedno vlákno dokončí před zahájením jiného. Tento problém se nazývá problém souběhu (race conditions). V něm se vlastně vlákna předhánějí v tom, které změní tutéž datovou strukturu.

Problém souběhu

Předpokládejme, že váš program řeší sérii úloh, jež se zpracovávají několika souběžnými vlákny. Fronta úloha je reprezentována spojovaným seznamem objektů struct job.

Jakmile každé vlákno dokončí nějakou operaci, ověří ve frontě, je-li dostupná nějaká jiná úloha. Jestliže je hodnota proměnné job_queue nenulová, vlákno odstraní aktuální položku spojovaného seznamu a hodnotu job_queue nastaví na následující úlohu v seznamu.

#include <malloc.h>

struct job {
  struct job *next;

  /* Ostatni pole popisujici ukoly... */
};

/* Fronta ukolu */
struct job *job_queue;

/* Zpracovava ukoly ve fronte, pokud neni prazdna */

void *thread_function(void *arg)
{
  while(job_queue != NULL){
    /* Ziska dalsi dostupny ukol */
    struct job *next_job = job_queue;
    /* Odstrani tento ukol ze seznamu */
    job_queue = job_queue->next;
    /* Realizuje ukol */
    process_job(next_job);
    /* Ocista :) */
    free(next_job);
  }
  return(NULL);
}

Nyní předokládejme, že se dvěma vlákům podaří ukončit svoji činnost přibližně v tutéž dobu, ale ve frontě je již jediná úloha. První vlákno ověří, zda je hodnota proměnné job_queue nulová; jestliže není, zahájí cyklus a uloží ukazatel na objekt job do proměnné next_job. V tomto okamžiku jádro přeruší realizaci prního vlákna a zahájí činnost druhého. Druhé vlákno rovněž zkontroluje proměnnou job_queue, opět zjistí, že není nulová, a také přiřadí tentýž ukazatel do přoměnné next_job. Touto nešťastnou shodou máme nyní dvě vlákna, která řeší tutéž úlohu.

Navíc jedno vlákno odpojí úlohy z fronty a nechá hodnotu job_queue nulovou. Když pak druhé vlákno vyhodnotí proměnnou job_queue->next, nastane chyba segmentace.

Tyto nepříznivé podmínky lze řešit pouze tzv. atomickou neboli elementární operací. Jedná se o operaci, která je nedělitelná na menší operace, a tudíž je nepřerušitelná (jakmile jednou začne, nelze ji ukončit). V našem konkrétním případě musíme několik operací (otestování proměnné job_queue, a je-li nenulová, odstranění první úlohy) realizovat prostřednictvím jediné atomické operace.

To by bylo pro dnešek vše, v příštím dílu dořešíme „nadhozený“ problém – použijeme k tomu mutexy.

Našli jste v článku chybu?

14. 5. 2004 12:37

Mormegil (neregistrovaný)

"Jakmile jednou zacne, nejde ji ukoncit" -- nevim, nevim, jestli to je dobry popis atomicke operace, spis mne to pripomina atomovou operaci... :o)

14. 5. 2004 9:00

Tomas Zellerin (neregistrovaný)

Jen bych upozornil, že pthread_cleanup_xxx AFAIK nejsou funkce, ale makra, která se z hlediska uživatele expandují mimo jiné do { či }. Takže je více než vhodné je mít ve stejné úrovni zanoření - nelze to mít např. ve větvi if, zapouzdřené v separátní funkci a pod.

DigiZone.cz: „Black Friday 2016“: závěrečné zhodnocení

„Black Friday 2016“: závěrečné zhodnocení

DigiZone.cz: Flix TV startuje i na Slovensku

Flix TV startuje i na Slovensku

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Vitalia.cz: 9 největších mýtů o mase

9 největších mýtů o mase

Lupa.cz: Co se dá měřit přes Internet věcí

Co se dá měřit přes Internet věcí

Vitalia.cz: Taky věříte na pravidlo 5 sekund?

Taky věříte na pravidlo 5 sekund?

Vitalia.cz: Mondelez stahuje rizikovou čokoládu Milka

Mondelez stahuje rizikovou čokoládu Milka

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

120na80.cz: Pánové, pečujte o svoje přirození a prostatu

Pánové, pečujte o svoje přirození a prostatu

Lupa.cz: Teletext je „internetem hipsterů“

Teletext je „internetem hipsterů“

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA

Vitalia.cz: Když přijdete o oko, přijdete na rok o řidičák

Když přijdete o oko, přijdete na rok o řidičák

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

Podnikatel.cz: Snížení DPH na 15 % se netýká všech

Snížení DPH na 15 % se netýká všech

Měšec.cz: Kdy vám stát dá na stěhování 50 000 Kč?

Kdy vám stát dá na stěhování 50 000 Kč?

Měšec.cz: Zdravotní a sociální pojištění 2017: Připlatíte

Zdravotní a sociální pojištění 2017: Připlatíte

Lupa.cz: Propustili je z Avastu, už po nich sahá ESET

Propustili je z Avastu, už po nich sahá ESET

120na80.cz: Na ucho teplý, nebo studený obklad?

Na ucho teplý, nebo studený obklad?

Měšec.cz: Air Bank zruší TOP3 garanci a zdražuje kurzy

Air Bank zruší TOP3 garanci a zdražuje kurzy