1. díl - Tvorba vláken

C++ Aplikace pro vícejádrové procesory Tvorba vláken

Vícevláknové aplikace jsou jedním z přístupů tvorby aplikací pro vícejádrové procesory. V rámci jednoho spuštěného procesu běží několik nezávislých vláken (thread) kódu, které je možné vykonávat souběžně na různých jádrech (při použití SMT je možné provádět více vláken i na jediném jádru - HyperThreading od Intelu, AMD Zen procesory, IBM POWER, Oracle SPARC). O způsobech paralelizace a různých přístupech budou pojednávat další články.

Vlákno je, zjednodušeně řečeno, posloupnost instrukcí, kterou vykonává procesor. Typicky je to podmnožina procesu (jeden proces může mít více vláken, ale jedno vlákno nemůže patřit do více procesů nebo existovat samostatně) a plánovač operačního systému určuje, které vlákno poběží na kterém procesoru (jedno jádro je z hlediska OS považováno za "samostatný" procesor). Vlákno má svůj vlastní kontext (zásobník a procesorové registry) a může mít tzv. thread-local uložiště (paměť přístupná pouze danému vláknu), ale většina stavových informací (otevřené soubory, statická data, sokety, apod.) jsou sdíleny v rámci procesu.

Každý proces se spouští s jedním aktivním vláknem (main thread, vykonává funkci main) - takto funguje každá běžná aplikace.

Rozhraní POSIX

Rozhraní POSIX je jednotné rozhraní přístupné na všech Unix-like systémech (Linux, BSD, Mac OS X, Solaris) a částečně i na Windows (s využitím Cygwin). Program, který jej využívá, bude (maximálně s drobnými úpravami) fungovat na všech těchto operačních systémech.

Program je třeba kompilovat a linkovat s -pthread.

$ gcc -pthread -o ukazka posix_create.c
$ ./ukazka

Tvorba vláken

Nové vlákno je možné vytvořit voláním funkce pthread_create(). "p" představuje POSIX - název funkce je tedy zkratkou pro: "POSIX Thread Create".

Této funkci musíme předhodit minimálně ukazatel na proměnnou typu pthread_t (v ní bude identifikátor vlákna) a ukazatel na funkci, kterou má vlákno vykonávat. Dále můžeme nastavit atributy vytvářeného vlákna a předat argumenty vykonávané funkci.

pthread_t thread;
pthread_create(&thread, 0, &foo, 0); // (proměnná na ID, atributy, ukazatel na funkci, ukazatel na parametry)

Tento kód vytvoří vlákno, které bude vykonávat funkci foo() bez argumentů. Vlákno bude mít výchozí atributy.

Ukončení vlákna

Pro ukončování vláken máme dvě možnosti. První možností je pthread_exit(). Toto volání jednoduše ukončí vlákno (ať už stihlo dokončit svou práci nebo ne. Je také zavoláno automaticky, když vlákno dokončí svou práci.

Druhou možností je pthread_join(), které čeká na dokončení daného vlákna. Můžeme jej tedy využít k synchronizaci vláken s hlavním vláknem. Prvním argumentem je ID vlákna, druhým ukazatel na proměnnou, kam se má nakopírovat ukazatel na návratovou hodnotu.

Následující příklad ukazuje základní vytvoření vlákna a následné čekání na jeho dokončení. Můžete si zkusit vyzkoušet, co se stane, když pthread_join vynecháte (někdy se vlákno vykoná, někdy to nestihne).

#include <pthread.h>
#include <stdio.h>

void *foo (void *param)
{
        printf("New thread\n");

        return 0;
}

int main()
{
        pthread_t thread;
        pthread_create(&thread, 0, &foo, 0);

        printf("Main thread\n");

        pthread_join(thread, 0);  // 0 -> návratovou hodnotu ignorujeme

        return 0;
}

Práce s návratovou hodnotou

V základním příkladu nedostane vytvořené vlákno žádné parametry a nezajímá nás jeho návratová hodnota. Ukážeme si, jak s nimi můžeme pracovat.

Z definice vrací funkce, kterou má vlákno vykonávat, ukazatel. Máme v zásadě dvě možnosti, jak se s návratovou hodnotou poprat.

1. Alokovat příslušné místo na hromadě a po použití zase uvolnit.

void *msg = malloc(5*sizeof(char));
msg = strcpy(msg, "Ahoj");
return msg;

Takto pak hodnotu získáme:

void *retval;
pthread_join(thread, &retval);
printf("New thread returned with msg: %s.\n", (char *) retval);
free(retval);

Pozor! Nezapomeňte po sobě uklidit, pokud alokujete paměť na hromadě.

2. Pokud potřebujeme jen číslo (tj. stejné chování jako když je návratová hodnota int), tak můžeme udělat menší "hack", který je ale podstatně výkonnější. Jednoduše dané číslo vrátíme jako ukazatel a pak ukazatel použijeme jako číslo.

return (void *) 5;

Takto pak hodnotu získáme:

void *retval;
pthread_join(thread, &retval);
printf("New thread returned with %ld.\n", (long) retval);

Důrazně doporučuji použít přetypování, aby bylo z kódu jasné, co jste zamýšleli. Long jsem využil, protože na 64bit architektuře mají ukazatele 8B (stejně jako long) a na 32bit mají 4B (stejně jako long).

Ještě bych chtěl upozornit na častou chybu začátečníků.. Nikdy nevracejte ukazatel na lokální proměnné!

int a = 5;
return &a;

Lokální proměnné se nachází na zásobníku a ten s ukončením funkce přestává existovat. Navrácený ukazatel tedy ukazuje na nedefinované místo v paměti a může se stát cokoliv (vyčtení správné hodnoty, špatné hodnoty a nebo rovnou segfault).

Práce s parametry

Funkce prováděná vláknem má jeden parametr - a to ukazatel do paměti, kde jsou uložené parametry. Programátor je tedy musí určitým způsobem uložit do paměti a pak zase stejným vyčíst. Pokud chcete jeden parametr, bude stačit obyčejné přetypování ukazatelů. Řešením více parametrů různého typu je například struktura.

Takto jednoduše předáme funkci vlákna jeden parametr (s polem intů by to bylo obdobné):

int param = 1;
pthread_create(&thread, 0, &foo, &param);

Ve vláknu pak provádíme dereferenci ukazatele. Musíme ale nejdříve říct překladači, na co daný ukazatel ukazuje (tj. přetypujeme na ukazatel na int).

printf("New thread, param: %d\n", *(int *)param);

Operační systém Windows

Tvorba vláken

Windows nabízí v rámci svého API funkci CreateThread(), která vytvoří vlákno na úrovni jádra - takto vytvořené vlákno pak nemá přístup k funkcím běhové knihovny (v našem případě knihovny C). Pokud tyto funkce ale nepotřebujete a chcete vlákno využít jen k nějaké práci, bude toto volání nejspíše o něco méně náročné.

WinAPI je plné různých #define pro prakticky každý typ - při použití CreateThread() budete muset používat právě tyto makra.

#include <Windows.h>
#include <stdio.h>

DWORD WINAPI foo(__in LPVOID lpParameter)
{
        printf("New Thread.\n");
        return 0;
}


int main()
{
        HANDLE handle;
        handle = CreateThread(0, 0, foo, 0, 0, 0);

        printf("Main Thread.\n");

        return 0;
}

Pro běžné užití v C aplikacích (jiné jazyky budou mít své vlastní funkce) máme k dispozici dvě funkce - _beginthread() a _beginthreadex(). Tyto funkce obalují CreateThread() a provedou všechny potřebné inicializace, takže vlákno může bezpečně používat běhovou knihovnu.

#include <stdio.h>
#include <Windows.h>
#include <process.h>

void foo(void *data)
{
        printf("New Thread.\n");
}

unsigned int __stdcall foo_ex(void *data)
{
        printf("New Thread created with _beginthreadex, argument is: %d.\n", *(int *)data);
        return 0;
}

int main()
{
        HANDLE h, h_ex;
        int a = 5;

        h = (HANDLE) _beginthread(&foo, 0, 0);

        h_ex = (HANDLE) _beginthreadex(0, 0, &foo_ex, &a, 0, 0);
        WaitForSingleObject(h_ex, INFINITE);
        CloseHandle(h_ex);

        printf("Main Thread.\n");

        return 0;
}

Při použití těchto funkcí si musíte dát pozor na několik věcí. Za prvé je třeba přetypovat návratovou hodnotu na HANDLE a pak je třeba počítat s výrazně odlišným rozhraním. _beginthread() je velice jednoduchá a sympatická funkce. Bohužel je reálně poněkud nepoužitelná - po dokončení takto vytvořeného vlákna je totiž handle ihned vrácen systému a může být znovu použit pro jiné vlákno. Neexistuje tedy žádný spolehlivý způsob, jak takové vlákno synchronizovat s hlavním vláknem.

Nezbývá tedy nic jiného, než používat _beginthreadex(). Ta z nějakého (mě neznámého) důvodu používá stdcall a tuto skutečnost musíme oznámit překladači, aby vše fungovalo podle očekávání. Na rozdíl od předchozí funkce leží úklid handle na programátorovi, takže je možné použít například WaitForSingle­Object() k synchronizaci s hlavním vláknem. Handle pak uklízíme s pomocí CloseHandle().

Kromě třetího a čtvrtého argumentu je zajímavý ještě argument poslední - jde o ukazatel an unsigned int, který obsahuje návratovou hodnotu vlákna. Ostatní atributy nejsou typicky třeba a jejich popis naleznete na MSDN.

Ukončení vlákna

Pro nucené ukončení vláken můžeme používat funkce _endthread a _endthreadex nebo ExitThread - vždy podle toho, co jste použili pro vytvoření vlákna.


 

  Aktivity (1)

Článek pro vás napsal David Novák
Avatar
Autor v současné době studuje FIT VUT Brno a zajímá se především o nízkoúrovňové programování (C/C++, ASM) a návrh hardwaru (VHDL). Je zde také členem výzkumného týmu ANT@FIT (Accelerated Network Technologies).

Jak se ti líbí článek?
Celkem (3 hlasů) :
55555


 



 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!