IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

1. diel - Tvorba vlákien

Viacvláknové aplikácie sú jedným z prístupov tvorby aplikácií pre viacjadrové procesory. V rámci jedného spusteného procesu beží niekoľko nezávislých vlákien (thread) kódu, ktoré je možné vykonávať súbežne na rôznych jadrách (pri použití SMT je možné vykonávať viac vlákien aj na jedinom jadre - HyperThreading od Intelu, AMD Zen procesory, IBM POWER, Oracle SPARC) . O spôsoboch paralelizácie a rôznych prístupoch budú pojednávať ďalšie články.

Vlákno je, zjednodušene povedané, postupnosť inštrukcií, ktorú vykonáva procesor. Typicky je to podmnožina procesu (jeden proces môže mať viac vlákien, ale jedno vlákno nemôže patriť do viacerých procesov alebo existovať samostatne) a plánovač operačného systému určuje, ktoré vlákno pobeží na ktorom procesore (jedno jadro je z hľadiska OS považované za "samostatný" procesor ). Vlákno má svoj vlastný kontext (zásobník a procesorové registre) a môže mať tzv. Thread-local úložisko (pamäť prístupná iba danému vláknu), ale väčšina stavových informácií (otvorené súbory, statické dáta, sokety, apod.) Sú zdieľané v rámci procesu.

Každý proces sa spúšťa s jedným aktívnym vláknom (main thread, vykonáva funkciu main) - takto funguje každá bežná aplikácie.

Rozhranie POSIX

Rozhranie POSIX je jednotné rozhranie prístupné na všetkých Unix-like systémoch (Linux, BSD, Mac OS X, Solaris) a čiastočne aj na Windows (s využitím Cygwin). Program, ktorý ho využíva, bude (maximálne s drobnými úpravami) fungovať na všetkých týchto operačných systémoch.

Program je potrebné kompilovať a linkovať s -pthread.

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

Tvorba vlákien

Nové vlákno je možné vytvoriť volaním funkcie pthread_create (). "P" predstavuje POSIX - názov funkcie je teda skratkou pre: "POSIX Thread Create".

Tejto funkcii musíme predhodiť minimálne ukazovateľ na premennú typu pthread_t (v nej bude identifikátor vlákna) a ukazovateľ na funkciu, ktorú má vlákno vykonávať. Ďalej môžeme nastaviť atribúty vytváraného vlákna a odovzdať argumenty vykonávanej funkcii.

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

Tento kód vytvorí vlákno, ktoré bude vykonávať funkciu foo () bez argumentov. Vlákno bude mať predvolené atribúty.

Ukončenie vlákna

Na ukončovanie vlákien máme dve možnosti. Prvou možnosťou je pthread_exit (). Toto volanie jednoducho ukončí vlákno (či už stihlo dokončiť svoju prácu alebo nie. Je tiež vyzvaná, aby doplávala automaticky, keď vlákno dokončí svoju prácu.

Druhou možnosťou je pthread_join (), ktoré čaká na dokončenie daného vlákna. Môžeme ho teda využiť na synchronizáciu vlákien s hlavným vláknom. Prvým argumentom je ID vlákna, druhým ukazovateľ na premennú, kam sa má nakopírovať ukazovateľ na návratovú hodnotu.

Nasledujúci príklad ukazuje základné vytvorenie vlákna a následné čakanie na jeho dokončenie. Môžete si skúsiť vyskúšať, čo sa stane, keď pthread_join vynecháte (niekedy sa vlákno vykoná, niekedy 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áca s návratovou hodnotou

V základnom príklade nedostane vytvorené vlákno žiadne parametre a nezaujíma nás jeho návratová hodnota. Ukážeme si, ako s nimi môžeme pracovať.

Z definície vracia funkcia, ktorú má vlákno vykonávať, ukazovateľ. Máme v zásade dve možnosti, ako sa s návratovú hodnotou popasovať.

1. alokovať príslušné miesto na hromade a po použití zase uvoľniť.

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

Takto potom hodnotu získame:

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

Pozor! Nezabudnite po sebe upratať, ak alokuje pamäť na hromade.

2. Ak potrebujeme len číslo (tj. Rovnaké správanie ako keď je návratová hodnota int), tak môžeme spraviť menší "hack", ktorý je ale podstatne výkonnejšie. Jednoducho dané číslo vrátime ako ukazovateľ a potom ukazovateľ použijeme ako číslo.

return (void *) 5;

Takto potom hodnotu získame:

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

Dôrazne odporúčam použiť pretypovanie, aby bolo z kódu jasné, čo ste zamýšľali. Long som využil, pretože na 64bit architektúre majú ukazovatele 8B (rovnako ako long) a na 32bit majú 4B (rovnako ako long).

Ešte by som chcel upozorniť na častú chybu začiatočníkov .. Nevracajte ukazovateľ na lokálnej premennej!

int a = 5;
return &a;

Lokálne premenné sa nachádza na zásobníku a ten s ukončením funkcie prestáva existovať. Navrátený ukazovateľ teda ukazuje na nedefinované miesto v pamäti a môže sa stať čokoľvek (vyčítanie správne hodnoty, zlé hodnoty a alebo rovno segfault).

Práca s parametrami

Funkcia vykonávaná vláknom má jeden parameter - a to ukazovateľ do pamäte, kde sú uložené parametre. Programátor je teda musia určitým spôsobom uložiť do pamäti a potom zase rovnakým vyčítať. Ak chcete jeden parameter, bude stačiť obyčajné pretypovanie ukazovateľov. Riešením viac parametrov rôzneho typu je napríklad štruktúra.

Takto jednoducho odovzdáme funkciu vlákna jeden parameter (s poľom intů by to bolo podobné):

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

Vo vlákne potom vykonávame dereferencia ukazovatele. Musíme ale najskôr povedať prekladači, na čo daný ukazovateľ ukazuje (tj. Přetypujeme na ukazovateľ na int).

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

Operačný systém Windows

Tvorba vlákien

Windows ponúka v rámci svojho API funkciu CreateThread (), ktorá vytvorí vlákno na úrovni jadra - takto vytvorené vlákno potom nemá prístup k funkciám behové knižnice (v našom prípade knižnice C). Ak tieto funkcie ale nepotrebujete a chcete vlákno využiť len k nejakej práci, bude toto volanie pravdepodobne o niečo menej náročné.

WINAPO je plné rôznych #define pre prakticky každý typ - pri použití CreateThread () budete musieť používať práve tieto makrá.

#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");

    CloseHandle(handle);
    return 0;
}

Pre bežné použitie v C aplikáciách (iné jazyky budú mať svoje vlastné funkcie) máme k dispozícii dve funkcie - _beginthread () a _beginthreadex (). Tieto funkcie obaľujú CreateThread () a vykonajú všetky potrebné inicializácia, takže vlákno môže bezpečne používať runtime knižnicu.

#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;
}

Pri použití týchto funkcií si musíte dať pozor na niekoľko vecí. Po prvé je potrebné pretypovať návratovú hodnotu na HANDLE a potom je potrebné počítať s výrazne odlišným rozhraním. _beginthread () je veľmi jednoduchá a sympatická funkcie. Bohužiaľ je reálne trochu nepoužiteľná - po dokončení takto vytvoreného vlákna je totiž handle ihneď vrátený systému a môže byť znovu použitý pre iné vlákno. Neexistuje teda žiadny spoľahlivý spôsob, ako takéto vlákno synchronizovať s hlavným vláknom.

Nezostáva teda nič iné, než používať _beginthreadex (). Tá z nejakého (ma neznámeho) dôvodu používa stdcall a túto skutočnosť musíme oznámiť prekladači, aby všetko fungovalo podľa očakávaní. Na rozdiel od predchádzajúcej funkcie leží upratovanie handle na programátorovi, takže je možné použiť napríklad WaitForSingleObject () na synchronizáciu s hlavným vláknom. Handle potom upratujeme s pomocou CloseHandle ().

Okrem tretieho a štvrtého tvrdenia je zaujímavý ešte argument posledný - ide o ukazovateľ an unsigned int, ktorý obsahuje návratovú hodnotu vlákna. Ostatné atribúty nie sú typicky potrebné a ich popis nájdete na MSDN.

Ukončenie vlákna

Pre nútené ukončenie vlákien môžeme používať funkcie _endthread a _endthreadex alebo ExitThread - vždy podľa toho, čo ste použili pre vytvorenie vlákna.

V budúcej lekcii, Knižnica štandardu C , si predstavíme knižnicu threads.h jazyka C, použiteľnú v C / C ++, a ako s ňou tvoriť vlákna, mutexy a podmienené premenné.


 

Všetky články v sekcii
Paralelné aplikácie pre viacjadrové procesory v C
Preskočiť článok
(neodporúčame)
Knižnica štandardu C
Článok pre vás napísal David Novák
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se zajímá především o nízkoúrovňové programování (C/C++, ASM) a návrh hardwaru (VHDL).
Aktivity