Pentru a lansa un nou fir de execuție, există funcțiile CreateThread și CreateRemoteThread (a doua fiind folosită pentru a crea un fir de execuție în cadrul altui proces decât cel curent). Aici găsiți mai multe informații despre remote threads.
HANDLE CreateThread ( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); |
hthread = CreateThread( NULL, 0, ThreadFunc, &dwThreadParam, 0, &dwThreadId ); |
Parametrul dwStackSize
reprezintă mărimea inițială a stivei (în octeți). Sistemul rotunjește această valoare la cel mai apropiat multiplu de dimensiunea unei pagini. Dacă parametrul este 0, noul fir de execuție va folosi mărimea implicită (1 MB). lpStartAddress
este un pointer la funcția ce trebuie executată de către firul de execuție. Această funcție are următorul prototip:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
unde lpParameter
reprezintă datele care sunt pasate firului în momentul execuției. La fel ca pe Linux, se poate transmite un pointer la o structură, care conține toți parametrii necesari. Rezultatul întors poate fi obținut de un alt fir de execuție folosind funcția GetExitCodeThread.
Firele de execuție pot fi identificate în sistem în 3 moduri:
HANDLE
, obținut la crearea firului de execuție, sau folosind funcția OpenThread, căreia i se dă ca parametru identificatorul firului de execuție:HANDLE OpenThread( DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwThreadId );
pseudo-HANDLE
, o valoare specială care indică funcțiilor de lucru cu HANDLE
-uri că este vorba de HANDLE
-ul asociat cu firul de execuție curent (obținut, de exemplu, apelând GetCurrentThread). Pentru a converti un pseudo-HANDLE
într-un HANDLE
veritabil, trebuie folosită funcția DuplicateHandle. De asemenea, nu are sens să facem CloseHandle pe un pseudo-HANDLE
. Pe de altă parte, handle-ul obținut cu DuplicateHandle trebuie închis dacă nu mai este nevoie de el.DWORD
, întors la crearea firului, sau obținut folosind GetCurrentThreadId. O diferență dintre identificator și HANDLE
este faptul că nu trebuie să ne preocupăm să închidem un identificator, pe când la HANDLE
, pentru a evita leak-urile, trebuie să apelăm CloseHandle.Handle-ul obținut la crearea unui fir de execuție are implicit drepturi de acces nelimitate. El poate fi moștenit (sau nu) de procesele copil ale procesului curent, în funcție de flag-urile specificate la crearea lui. Prin funcția DuplicateHandle, se poate crea un nou handle cu mai puține drepturi. Handle-ul este valid până când este închis, chiar dacă firul de execuție pe care îl reprezintă s-a terminat.
Pe Windows, se poate aștepta terminarea unui fir de execuție folosind aceeași funcție ca pentru așteptarea oricărui obiect de sincronizare WaitForSingleObject:
DWORD WINAPI WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds );
Un fir de execuție se termină în unul din următoarele cazuri :
void ExitThread(DWORD dwExitCode);
return
.THREAD_TERMINATE
asupra firului de execuție, execută un apel TerminateThread pe acest handle : BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode );
La terminarea ultimului fir de execuție al unui proces se termină și procesul.
Pentru aflarea codului de terminare a unui fir de execuție, folosim funcția GetExitCodeThread.
BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode );
hThread
- handle al firului de execuție ce trebuie să aibă dreptul de acces THREAD_QUERY_INFORMATION
.lpExitCode
- pointer la o variabilă în care va fi plasat codul de terminare al firului. Dacă firul nu și-a terminat execuția, această valoare va fi STILL_ACTIVE
.
STILL_ACTIVE
(259), și anume aplicația care testează valoarea poate intra într-o buclă infinită.
HANDLE GetCurrentThread(void);
Rezultatul este un pseudo-handle pentru firul curent ce nu poate fi folosit decât de firul apelant. Acest handle are maximum de drepturi de acces asupra obiectului pe care îl reprezintă.
DWORD GetCurrentThreadId(void);
Rezultatul este identificatorul firului curent de execuție.
DWORD GetThreadId(HANDLE hThread);
Rezultatul este identificatorul firului ce corespunde handle-ului hThread
.
Ca și în Linux, în Windows există un mecanism prin care fiecare fir de execuție să aibă anumite date specifice. Acest mecanism poartă numele de Thread Local Storage (TLS). În Windows, pentru a accesa datele din TLS se folosesc indecșii asociați acestora (corespunzători cheilor din Linux).
Pentru a crea un nou TLS, se apelează funcția TlsAlloc:
DWORD TlsAlloc(void);
Funcția întoarce în caz de succes indexul asociat TLS-ului, prin intermediul căruia fiecare fir de execuție va putea accesa datele specifice. Valoarea stocată în TLS este inițializată cu 0. În caz de eșec, funcția întoarce valoarea TLS_OUT_OF_INDEXES
.
Pentru a stoca o nouă valoare într-un TLS, se folosește funcția TlsSetValue:
BOOL TlsSetValue( DWORD dwTlsIndex, LPVOID lpTlsValue );
Un fir de execuție poate afla valoarea specifică lui dintr-un TLS apelând funcția TlsGetValue:
LPVOID TlsGetValue(DWORD dwTlsIndex);
În caz de succes, funcția întoarce valoarea stocată în TLS, iar în caz de eșec, întoarce 0. Dacă data stocată în TLS are valoarea 0, atunci valoarea întoarsă este tot 0, dar GetLastError va întoarce NO_ERROR
. Deci trebuie verificată eroarea întoarsă de GetLastError.
Pentru a elibera un index asociat unui TLS, se folosește funcția TlsFree:
BOOL TlsFree(DWORD dwTlsIndex);
Dacă firele de execuție au alocat memorie și au stocat în TLS un pointer la memoria alocată, această funcție nu va face dealocarea memoriei. Memoria trebuie dealocată de către fire înainte de apelul lui TlsFree.
Exemplul prezintă crearea a 2 fire de execuție ce vor folosi un TLS.
#include <stdio.h> #include <windows.h> #include "utils.h" #define NO_THREADS 2 DWORD dwTlsIndex; VOID TLSUse(VOID) { LPVOID lpvData; /* get the pointer from TLS for current thread */ lpvData = TlsGetValue(dwTlsIndex); DIE((lpvData == 0) && (GetLastError() != 0), "TlsGetValue"); /* use this data */ printf("thread %d: get lpvData=%p\n", GetCurrentThreadId(), lpvData); Sleep(5000); } /* function executed by the threads */ DWORD WINAPI ThreadFunc(LPVOID lpParameter) { LPVOID lpvData; DWORD dwReturn; /* TLS init for the current thread */ lpvData = (LPVOID) LocalAlloc(LPTR, 256); DIE(lpvData == NULL, "LocallAloc"); dwReturn = TlsSetValue(dwTlsIndex, lpvData); DIE(dwReturn == FALSE, "TlsSetValue"); printf("thread %d: set lpvData=%p\n", GetCurrentThreadId(), lpvData); TLSUse(); /* free dinamic memory */ lpvData = TlsGetValue(dwTlsIndex); DIE((lpvData == 0) && (GetLastError() != 0), "TlsGetValue"); LocalFree((HLOCAL) lpvData); return 0; } DWORD main(VOID) { DWORD IDThread, dwReturn; HANDLE hThread[NO_THREADS]; int i; /* allocate TLS index */ dwTlsIndex = TlsAlloc(); DIE(dwTlsIndex == TLS_OUT_OF_INDEXES, "Eroare la TlsAlloc"); /* create threads */ for (i = 0; i < NO_THREADS; i++) { hThread[i] = CreateThread(NULL, /* default security attributes */ 0, /* default stack size */ (LPTHREAD_START_ROUTINE) ThreadFunc, /* routine to execute */ NULL, /* no thread parameter */ 0, /* immediately run the thread */ &IDThread); /* thread id */ DIE(hThread[i] == NULL, "CreateThread"); } /* wait for threads completion */ for (i = 0; i < NO_THREADS; i++) { dwReturn = WaitForSingleObject(hThread[i], INFINITE); DIE(dwReturn == WAIT_FAILED, "WaitForSingleObject"); } /* free TLS index */ dwReturn = TlsFree(dwTlsIndex); DIE(dwReturn == FALSE, "TlsFree"); return 0; }
Windows pune la dispoziție și o implementare de User-space Threads, numite fibre. Kernel-ul planifică un singur Kernel Level Thread (KLT) asociat cu un set de fibre, iar fibrele colaborează pentru a partaja timpul de procesor oferit acestuia. Deși viteza de execuție este mai bună (pentru context-switch, nu mai este necesară interacțiunea cu kernel-ul), programele scrise folosind fibre pot deveni complexe. Mai multe informații puteți găsi în cadrul secțiunii suplimentare dedicate, iar aici puteți vedea control flow-ul într-o reprezentare grafică.
Pentru sincronizarea firelor de execuție avem la dispoziție:
Standardul POSIX specifică funcții de sincronizare pentru fiecare tip de obiect de sincronizare. API-ul Win32, fiind controlat de o singură entitate, permite ca toate obiectele de sincronizare să poată fi utilizate cu funcțiile standard de sincronizare: WaitForSingleObject, WaitForMultipleObjects sau SignalObjectAndWait.
Obiectele de sincronizare Semaphore, Mutex, Event și WaitableTimer pot fi folosite atât pentru sincronizarea proceselor, cât și a firelor de execuție.
În Windows mai există un mecanism de sincronizare care este disponibil doar pentru firele de execuție ale aceluiași proces, și anume CriticalSection. Se recomandă folosirea CriticalSection pentru excluderea mutuală a firelor de execuție ale aceluiași proces, fiind mai eficient decât Mutex sau Semaphore.
Win32 API pune la dispoziție un mecanism de acces sincronizat la variabile partajate între fire de execuție prin intermediul funcțiilor interlocked (Interlocked Variable Access), precum și operații atomice de inserare și ștergere în liste simplu înlănțuite (Interlocked Singly Linked Lists).
Sunt operații prin care se obține un HANDLE
al unui obiect de tip mutex. Este necesar doar un singur apel, fie el de creare sau de deschidere (se presupune ca alt proces a creat deja mutex-ul).
Pentru a crea un mutex se folosește funcția CreateMutex cu sintaxa :
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpAttributes, BOOL bInitialOwner, LPCTSTR lpName ); |
hMutex = CreateMutex( NULL, /* default security attributes */ FALSE, /* initially not owned */ NULL, /* unnamed mutex */ ); |
Pentru a deschide un mutex deja existent este definită funcția OpenMutex cu sintaxa :
HANDLE OpenMutex( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName ); |
hMutex = OpenMutex( MUTEX_ALL_ACCESS, /* request full access */ FALSE, /* handle not inheritable */ "MyMutex" /* object name */ ); |
Obținerea unui mutex se realizează folosind una din funcțiile de așteptare tratate anterior.
Încercarea de acaparare a unui mutex presupune următorii pași:
Încercarea de obținere se poate face cu sau fară timp de expirare (time-out) în funcție de parametrii dați funcțiilor de așteptare. Cea mai des folosită funcție de așteptare este WaitForSingleObject.
Folosind funcția ReleaseMutex se cedează posesia mutex-ului, el devenind iar disponibil. Funcția are următoarea sintaxă :
BOOL ReleaseMutex( HANDLE hMutex );
Funcția va eșua dacă procesul nu deține mutex-ul.
Atenție! pentru a putea folosi această funcție HANDLE
-ul trebuie să aibă cel puțin dreptul de acces MUTEX_MODIFY_STATE
.
Operația de distrugere a unui mutex este aceeași ca pentru orice HANDLE
. Se folosește funcția CloseHandle. După ce toate HANDLE
-urile unui mutex au fost închise, mutexul este distrus și resursele ocupate de acesta eliberate.
Atenție! La terminarea execuției unui program toate HANDLE
-urile folosite de acesta sunt automat închise. Deci, spre deosebire de semafoarele IPC din Linux, este imposibil ca un mutex (sau semafor) în Windows să mai existe în sistem după ce programele care l-au folosit/creat s-au terminat.
Un semafor este un obiect de sincronizare care are intern un contor ce ia doar valori pozitive. Atât timp cât semaforul (contorul) are valori strict pozitive el este considerat disponibil (signaled). Când valoarea semaforului a ajuns la zero el devine indisponibil (nonsignaled) și următoarea încercare de decrementare va duce la o blocare a threadului/procesului de pe care s-a făcut apelul până când semaforul devine disponibil.
Operația de decrementare se realizează doar cu o singură unitate (la fel ca în API-ul POSIX), în timp ce incrementarea se poate realiza cu orice valoare în limita maximă.
Funcția de creare a semafoarelor este CreateSemaphore și are sintaxa :
HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpNAME );
Această funcție se poate folosi și pentru deschiderea unui semafor deja existent. Alternativ, pentru a folosi un semafor deja existent, este necesar obținerea HANDLE
-ului semaforului, operație ce se realizează folosind funcția OpenSemaphore cu următoarea sintaxă :
HANDLE OpenSemaphore( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpNAME );
Operația de decrementare a semaforului cu sau fără așteptare se realizează folosind una din funcțiile de așteptare. Cea mai des folosită este funcția WaitForSingleObject.
Incrementarea semaforului se realizează folosind funcția ReleaseSemaphore cu sintaxa :
BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount );
Operația de distrugere a unui semafor este similară cu cea de distrugere a unui mutex. Se folosește funcția CloseHandle. După ce toate HANDLE
-urile unui semafor au fost închise, semaforul este distrus și resursele ocupate de acesta eliberate.
Obiectele CriticalSection sunt echivalente mutex-urilor POSIX de tip RECURSIVE
. Acestea sunt folosite pentru excluderea mutuală a accesului firelor de execuție ale aceluiași proces la o secțiune critică de cod care conține operații asupra unor date partajate. Un singur fir de execuție va fi activ la un moment dat în interiorul secțiunii critice. Dacă mai multe fire așteaptă să intre, nu este garantată ordinea lor de intrare, totuși sistemul va fi echitabil față de toate.
Operațiile care se pot efectua asupra unei secțiuni critice sunt: intrarea, intrarea neblocantă, ieșirea din secțiunea critică, inițializarea și distrugerea.
Pentru serializarea accesului la o secțiune critică, fiecare fir de execuție va trebui să intre într-un obiect CriticalSection
la începutul secțiunii și să-l părăsească la sfârșitul ei. În acest fel, dacă două fire de execuție încearcă să intre în CriticalSection
simultan, doar unul dintre ele va reuși, și își va continua execuția în interiorul secțiunii critice, iar celălalt se va bloca până când obiectul CriticalSection
va fi părăsit de primul fir. Așadar, la sfârșitul secțiunii, primul fir trebuie să părăsească obiectul CriticalSection
, permițându-i celuilalt intrarea.
Pentru excluderea mutuală se pot folosi atât obiecte Mutex, cât și obiecte CriticalSection; dacă sincronizarea trebuie făcută doar între firele de execuție ale aceluiași proces este recomandată folosirea CriticalSection
, fiind un mecanism mai eficient. Operația de intrare în CriticalSection
se traduce într-o singură instrucțiune de asamblare de tip test-and-set-lock (TSL
). CriticalSection
este echivalentul futex-ului din Linux.
Alocarea memoriei pentru o secțiune critică se face prin declararea unui obiect CRITICAL_SECTION
. Acesta nu va putea fi folosit, totuși, înainte de a fi inițializat (InitializeCriticalSection, InitializeCriticalSectionAndSpinCount, SetCriticalSectionSpinCount, DeleteCriticalSection) .
void InitializeCriticalSection( LPCRITICAL_SECTION pcrit_sect ); BOOL InitializeCriticalSectionAndSpinCount( LPCRITICAL_SECTION pcrit_sect, DWORD dwSpinCount ); DWORD SetCriticalSectionSpinCount( LPCRITICAL_SECTION pcrit_sect, DWORD dwSpinCount ); void DeleteCriticalSection( LPCRITICAL_SECTION pcrit_sect );
CRITICAL_SECTION
nu poate fi copiat sau modificat după inițializare. De asemenea, un obiect CRITICAL_SECTION
nu trebuie inițializat de două ori, în caz contrar, comportamentul său fiind nedefinit.
Contorul de spin (Spin Count) are sens doar pe sistemele multiprocesor (SMP) (este ignorat pe sisteme uniprocesor). Contorul de spin reprezintă numărul de cicli pe care îl petrece un fir de execuție pe un procesor în busy-waiting, înainte de a-și suspenda execuția la un semafor asociat secțiunii critice, în așteptarea eliberării acesteia. Scopul așteptării unui număr de cicli în busy-waiting este evitarea blocării la semafor în cazul în care secțiunea critică se eliberează în intervalul respectiv, deoarece blocarea la semafor are impact asupra performanțelor. Folosirea contorului de spin este recomandată mai ales în cazul unei secțiuni critice scurte, accesate foarte des.
Secțiunile critice Windows au comportamentul mutex-urilor POSIX de tip RECURSIVE
. Un fir de execuție care se află deja în secțiunea critică nu se va bloca dacă apelează din nou EnterCriticalSection, însă va trebui să părăsească secțiunea critică de un număr de ori egal cu cel al ocupărilor, pentru a o elibera.
Pentru a încerca intrarea într-o secțiune critică fără a se bloca, un fir de execuție trebuie să apeleze TryEnterCriticalSection.
void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); /* pentru TryEnterCriticalSection _WIN32_WINNT >= 0x0400 înainte de include <windows.h> */ #define _WIN32_WINNT 0x0400 #include <windows.h> BOOL TryEnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
În cadrul unui fir de execuție, numărul apelurilor LeaveCriticalSection trebuie să fie egal cu numărul apelurilor EnterCriticalSection, pentru a elibera în final secțiunea critică. Dacă un fir de execuție care nu a intrat în secțiunea critică apelează LeaveCriticalSection, se va produce o eroare care va face ca firele care au apelat EnterCriticalSection să aștepte pentru o perioadă nedefinită de timp.
/* global critical section */ CRITICAL_SECTION CriticalSection; DWORD ThreadProc(LPVOID *param) { /* only one thread enters the critical section, the rest are blocked */ EnterCriticalSection(&CriticalSection); /* use of protected data */ /* leaves the critical section, allowing another thread to enter */ LeaveCriticalSection(&CriticalSection); } int main() { /* initialize only one time */ InitializeCriticalSection(&CriticalSection); /* the threads execution ... */ DeleteCriticalSection(&CriticalSection); return 0; }
Evenimentele reprezintă un mecanism prin care un fir de execuție poate semnaliza unul sau mai multe fire că o anumită condiție este îndeplintă. Ce e important este faptul că pot fi deblocate mai multe fire de execuție prin semnalarea unui singur eveniment. Evenimentele sunt de două tipuri, în funcție de modul în care sunt resetate:
Un eveniment este creat folosind funcția CreateEvent:
HANDLE WINAPI CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName ); |
hEvent = CreateEvent( NULL, TRUE, /* Manual Reset */ FALSE, /* Non-signaled state */ NULL /* Private variable */ ); |
Pentru a controla un eveniment se folosesc funcțiile:
Funcțiile interlocked pun la dispoziție un mecanism de sincronizare a accesului la variabile partajate între mai multe fire de execuție. Funcțiile pot fi apelate de fire de execuție ale unor procese diferite, pentru variabile aflate într-un spațiu de memorie partajată. Funcțiile interlocked reprezintă cel mai simplu mod de evitare a race-ului care apare când două fire de execuție modifică aceeași variabilă.
Operațiile atomice asupra variabilelor partajate:
LONG InterlockedIncrement( LONG volatile *lpAddend ); LONG InterlockedDecrement( LONG volatile *lpDecend );
LONG InterlockedExchange( LONG volatile *Target, LONG Value ); LONG InterlockedExchangeAdd( LPLONG volatile Addend, LONG Value ); PVOID InterlockedExchangePointer( PVOID volatile *Target, PVOID Value );
LONG InterlockedCompareExchange( LONG volatile * dest, LONG exchange, LONG comp ); PVOID InterlockedCompareExchangePointer( PVOID volatile * dest, PVOID exchange, PVOID comp );
InterlockedCompareExchange va compara dest
cu comp
; dacă sunt egale, îi va atribui lui dest
valoarea exchange
. Testul și atribuirea vor fi executate într-o singură operație atomică. Pentru variabile de tip pointer se va folosi InterlockedCompareExchangePointer. Comportamentul este echivalent cu:
atomicly_do { // execută atomic tot blocul următor tmp = *dest; // copiază valoarea din *dest if (tmp == comp) { // dacă e egală cu valoarea lui 'comp' *dest = exchange; // atunci scrie valoarea 'exchange' în *dest } }
Programele cu un număr mare de fire de execuție pot aduce probleme de performanță dincolo de cele de locking:
Pentru a facilita dezvoltarea de aplicații eficiente bazate pe fire de execuție, sistemul de operare Windows pune la dispoziție mecanismul thread pooling. Utilizarea acestuia este benefică în cazul unei aplicații bazată pe fire de execuție care au de îndeplinit taskuri relativ scurte. Prin utilizarea thread pooling, fiecare task de efectuat va fi atribuit unui fir de execuție din pool (un task este o procedură executată de un fir de execuție din thread pool).
Există două modalități prin care o aplicație poate specifica task-urile pe care le dorește executate de fire de execuție din thread pool:
Timer-Queue Timer
și funcțiile de așteptare înregistrate.
Dacă vreuna dintre funcțiile executate într-un thread-pool apelează TerminateThread
, comportamentul nu este definit.
Un exemplu practic pentru Windows ThreadPools, ce folosește noul API, se găsește aici.
Obiectele TimerQueue reprezintă cozi de timere. Ele conțin obiecte Timer-Queue Timer care au asociată o funcție callback, ce va fi executată de un fir de execuție din thread pool la expirarea timerului.
#define _WIN32_WINNT 0x0500 #include <windows.h> HANDLE CreateTimerQueue(void); // marchează coada pentru ștergere, dar *NU* așteaptă // ca toate callbackurile asociate cozii să se termine BOOL DeleteTimerQueue( HANDLE TimerQueue ); /** * CompletionEvent = NULL - marchează coada pentru ștergere și iese imediat (ca DeleteTimerQueue) * CompletionEvent = INVALID_HANDLE_VALUE - funcția așteaptă să se încheie toate callbackurile. * CompletionEvent = un handle de tip Event - un obiect Event care va fi * trecut în starea SIGNALED când se încheie toate callbackurile. */ BOOL DeleteTimerQueueEx( HANDLE TimerQueue, HANDLE CompletionEvent );
Pentru crearea unui timer se va apela funcția:
BOOL CreateTimerQueueTimer( PHANDLE phNewTimer, // aici întoarce un HANDLE la timerul nou creat HANDLE TimerQueue, // coada la care este adăugat timerul. // Dacă e NULL se folosește o coadă implicită. WAITORTIMERCALLBACK Callback, // callback de executat PVOID Parameter, // parametru trimis callbackului DWORD DueTime, // timerul va expira prima dată după 'DueTime' milisec. DWORD Period, // apoi timerul va expira periodic după 'Period' milisec. ULONG Flags // tipul callbackului: IO/NonIO, EXECUTEONLYONCE, ș.a. ); // semnătura unui callback VOID WaitOrTimerCallback( PVOID lpParameter, BOOLEAN TimerOrWaitFired ); // modificarea timpului de expirare al unui timer BOOL ChangeTimerQueueTimer( HANDLE TimerQueue, // coada la care este adăugat timerul. // Dacă e NULL se folosește o coadă implicită. HANDLE Timer, // HANDLE la timerul de modificat ULONG DueTime, // timerul va expira prima dată după 'DueTime' milisec. ULONG Period // apoi timerul va expira periodic după 'Period' milisec. ); // dezactivarea unui timer BOOL CancelTimerQueueTimer( HANDLE TimerQueue, HANDLE Timer ); // dezactivarea ȘI distrugerea unui timer. // CompletionEvent e similar cu cel din DeleteTimerQueueEx. BOOL DeleteTimerQueueTimer( HANDLE TimerQueue, HANDLE Timer, HANDLE CompletionEvent );
Operație | POSIX | Windows |
---|---|---|
Crearea unui fir de execuție | pthread_create | CreateThread |
Așteptarea unui fir de execuție | pthread_join | WaitForSingleObject |
Crearea unui mutex | pthread_mutex_init | CreateMutex |
Obținerea unui mutex | pthread_mutex_lock | WaitForSingleObject |
Cedarea unui mutex | pthread_mutex_unlock | ReleaseMutex |
Distrugerea unui mutex | pthread_mutex_destroy | CloseHandle |
Crearea unui semafor | sem_init /sem_open | CreateSemaphore |
Decrementarea unui semafor | sem_wait | WaitForSingleObject |
Incrementarea unui semafor | sem_post | ReleaseSemaphore |
Distrugerea unui semafor | sem_destroy /sem_close | CloseHandle |
Thread Local Storage | __thread, pthread_key_create, pthread_setspecific, pthread_getspecific, pthread_key_delete | TlsAlloc, TlsSetValue, TlsGetValue, TlsFree |
git clone https://github.com/systems-cs-pub-ro/so
. Dacă doriți să descărcați repositoryul în altă locație, folosiți comanda git clone https://github.com/systems-cs-pub-ro/so ${target}
.
Pentru a actualiza repository-ul, folosiți comanda git pull origin master
din interiorul directorului în care se află repository-ul. Recomandarea este să îl actualizați cât mai frecvent, înainte să începeți lucrul, pentru a vă asigura că aveți versiunea cea mai recentă. În cazul în care gitul detectează conflicte la nivelul vreunui fişier, folosiți următoarele comenzi pentru a vă păstra modificările:
git stash git pull origin master git stash pop
Pentru mai multe informații despre folosirea utilitarului git, urmați ghidul de la https://gitimmersion.com.
Încărcați proiectul 1-threading
și setați-l ca StartUp Project. Compilați și rulați programul. Aflați câte fire de execuție creează în total.
Lansați ProcessExplorer
(check Desktop) și verificați răspunsul de la întrebarea de mai sus. (View → Select Columns → Process Performance → Threads)
Aflați prioritatea procesului threading.exe
. (View → Select Columns → Process Performance → Base Priority)
Experimentați schimbând prioritatea procesului (click-dreapta pe numele procesului → Set Priority). Setați prioritatea astfel încât procesul threading.exe
să primească mai mult timp pe procesor.
real-time
și comentați linia cu Sleep
din bucla while
, cel mai probabil vi se va bloca mașina virtuală. Acest lucru s-ar întâmpla pentru că ar exista tot timpul un thread cu prioritate mai mare ca cele pentru interfața grafică, de exemplu, gata să ruleze pe procesor. Vezi și link.
Deschideți sursa 2-debug.c
din proiectul 2-debug
și completați funcția StartThread
pentru a implementa crearea unui fir de execuție (urmăriți în cod secțiunea marcată cu TODO).
Compilați și rulați sursa. Aplicația trebuie pornită din consolă: Tools → PowerShell Command Prompt
. Observați că programul se blochează. Identificați și rezolvați problema.
Sleep
. Inspectați funcțiile MakeCake
, MakeTiramisu
și MakeMarshmallows
și observați ordinea în care se face WaitForSingleObject
pe ingrediente (semafoare). Amintiți-vă din laboratorul precedent care era problema de la Exercițiul 5.
În cadrul acestui exercițiu dorim să testăm diverse tipuri de incrementări atomice ale unei variabile, comparându-le timpul de execuție. Deschideți sursa interlocked.c
din proiectul 3-interlocked
. Programul crează NO_THREADS
fire de execuție, care incrementează circular o variabilă (când se ajunge la o limită se resetează la 0).
Asigurați accesul exclusiv la variabila incrementată folosind Interlocked Variables deoarece mecanismul e mai rapid decât o incrementare normală protejată cu Mutex
sau CRITICAL_SECTION
(folosiți funcția InterlockedCompareExchange
). Incrementarea circulară se va face în funcția thread_function
(urmăriți comentariile cu TODO 1). Veți avea nevoie de două operații interlocked (InterlockedIncrement
și InterlockedCompareExchange
).
Identificați o problemă cu folosirea Interlocked Operations
pentru a incrementa circular o variabilă.
yield()
din Linux) între cele două operații interlocked. Compilați și rulați din nou programul. Observați că rezultatul nu mai este cel așteptat. Acest lucru s-a întâmplat pentru că am forțat schedulerul să schimbe firul de execuție imediat după incrementare. Chiar dacă fiecare operație în parte este atomică, succesiunea a două operații atomice NU este atomică.
Comparați timpul de execuție al programului precedent în cazul în care se folosește un mutex care să sincronizeze accesul la variabila count
, completând funcția thread_function_mutex
( TODO 2 ). Nu uitați să modificați și parametrul funcției CreateThread
din funcția main
.
Dorim să simulăm o implementare a funcției perror
. Pentru aceasta vom avea variabila globală myErrno
, dar cu valori specifice (diferite) pentru fiecare fir de execuție. Deschideți sursa tls.c
din proiectul 4-tls
și urmăriți comentariile marcate cu TODO (revedeți secțiunea despre TLS).
Deschideți sursa timer.c
din proiectul 5-timer
. Creați un Timer-Queue Timer
, a cărui rutină callback să fie declanșată de exact 3 ori, o dată la fiecare secundă. După 3 declanșări se va dezactiva timerul și se vor distruge toate resursele create. Trebuie să sincronizați rutina timer-ului cu funcția main
care va dezactiva timer-ul; pentru aceasta puteți folosi orice mecanism de semnalizare: semafor, event
(revedeți secțiunea despre Timer Queues).
Deschideți sursa barrier.c
din proiectul 6-Barrier
. Implementați o barieră reutilizabilă folosind un mutex și o variabilă de tip eveniment. Completați funcțiile de lucru cu bariera pentru a obține funcționalitatea dorită (comentariile marcate cu TODO).
Pentru a putea semnaliza un obiect și a aștepta la un alt obiect de sincronizare în același timp, puteți folosi funcția SignalObjectAndWait . De asemenea, revedeți secțiunile despre lucrul cu mutex-uri și evenimente.
Bariera va fi reprezentată prin structura:
typedef struct { HANDLE hGuard; /* mutex to protect internal variable access */ HANDLE hEvent; /* manual resetable event */ DWORD dwCount; /* number of threads to have reached the barrier */ DWORD dwThreshold; /* barrier limit */ }THRESHOLD_BARRIER, *THB_OBJECT;
Folosiți mutexul pentru a sincroniza execuția în cadrul funcției WaitThresholdBarrier
. Folosiți eventul pentru a aștepta până când toate threadurile ajung să apeleze funcția WaitThresholdBarrier
. Folosiți funcția PulseEvent pentru a semnala toate threadurile care așteaptă asupra eventului.
Deschideți sursa sort.c
din proiectul 7-sort
. Se dorește realizarea sortării unui șir de numere aleatoare dintr-un fișier în următorul mod:
Realizați partea de creare a firelor de execuție și împărțire a taskurilor în funcția init_setup()
. După ce toate firele de execuție sortează chunk-ul static, unele vor incepe sa facă merge la chunk-urile sortate. Completați funcția ThreadFunc
pentru ca, în funcție de id, un fir de execuție să apeleze funcția MergeChunks
(care realizează interclasarea a doi vectori sortați) (urmăriți comentariile cu TODO).
Șirul de numere este dat sub forma unui fișier binar care poate fi generat cu programul generator.exe
. Citirea șirului într-un vector este deja realizată în funcția init_setup
, iar fiecare fir de execuție primește o structură CHUNK
care reprezintă dimensiunea unui vector de sortat, cât și adresa inițială a vectorului. Interclasarea a două structuri CHUNK în care vectorii sunt deja sortați se realizează cu funcția MergeChunks
.
Deschideți sursa dorm_room.c
din proiectul 8-dean
. Se dorește simularea/modelarea următoarei probleme: decanul și studenții. Se dau următoarele constrângeri:
Rezolvați problema scriind cod pentru entitățile respective: decan și student. Pentru firele de execuție studenți completați funcțiile “enter_room” și “party”, iar pentru firul de execuție decan completați funcția “break_party” (revedeți secțiunea despre mutex-uri și semafoare ).
Folosiți funcțiile “dbg_student” și “dbg_decan” pentru a afișa mesaje corespunzătoare de fiecare dată când un fir de execuție își schimbă starea (ex: decanul intră în cameră, un student nu poate intra deoarece decanul e deja în cameră etc.)