This shows you the differences between two versions of the page.
so:curs:sync [2020/04/14 22:27] razvan.deaconescu [Capitol 10 - Sincronizare] |
so:curs:sync [2020/05/04 10:56] (current) alexandru.radovici |
||
---|---|---|---|
Line 11: | Line 11: | ||
* [[http://greenteapress.com/semaphores/|Allen B. Downey - The Little Book of Semaphores]] | * [[http://greenteapress.com/semaphores/|Allen B. Downey - The Little Book of Semaphores]] | ||
* [[https://deadlockempire.github.io/|The Deadlock Empire: Slay dragons, master concurency!]] | * [[https://deadlockempire.github.io/|The Deadlock Empire: Slay dragons, master concurency!]] | ||
- | * [[http://elf.cs.pub.ro/so/res/cursuri/SO_Curs-09.pdf|Curs 09 anterior]] | ||
* Filmări | * Filmări | ||
+ | * 3CA https://web.microsoftstream.com/video/75a1f57d-0d45-4188-96ca-43e62f6f6b42 | ||
* 3CC curs 13, partea 1: https://web.microsoftstream.com/video/7457610e-0320-4434-accf-cd449fdd4e75 | * 3CC curs 13, partea 1: https://web.microsoftstream.com/video/7457610e-0320-4434-accf-cd449fdd4e75 | ||
* 3CC curs 13, partea a 2-a: https://web.microsoftstream.com/video/862388ca-ad3b-4519-a344-8a7cce246103 | * 3CC curs 13, partea a 2-a: https://web.microsoftstream.com/video/862388ca-ad3b-4519-a344-8a7cce246103 | ||
+ | * 3CC curs 14, partea 1: https://web.microsoftstream.com/video/97632417-905b-4021-a458-5277cd771d81 | ||
+ | * 3CC curs 14, partea a 2-a: https://web.microsoftstream.com/video/fb26404e-495d-4c9e-ab6b-ef28715f0486 | ||
* Resurse vechi | * Resurse vechi | ||
* [[http://elf.cs.pub.ro/so/res/cursuri/SO%20-%20Curs%2009%20-%20Sincronizare.pdf|Curs 09 - Sincronizare (PDF)]] | * [[http://elf.cs.pub.ro/so/res/cursuri/SO%20-%20Curs%2009%20-%20Sincronizare.pdf|Curs 09 - Sincronizare (PDF)]] | ||
+ | * [[http://elf.cs.pub.ro/so/res/cursuri/SO_Curs-09.pdf|Curs 09 anterior]] | ||
+ | |||
+ | * Curs CA [[https://www.slideshare.net/alexandruradovici/sisteme-de-operare-sincronizare|slideshare]] | ||
===== Demo-uri ===== | ===== Demo-uri ===== | ||
Line 40: | Line 45: | ||
Programul ''thread-list-app-mutex'' folosește intern un mutex pentru sincronizare. Pentru aceasta folosim macro-ul ''USE_MUTEX'' pe care îl definim în fișierul ''Makefile''. | Programul ''thread-list-app-mutex'' folosește intern un mutex pentru sincronizare. Pentru aceasta folosim macro-ul ''USE_MUTEX'' pe care îl definim în fișierul ''Makefile''. | ||
- | Ca să urmărim ce se întâmplă cu o aplicație multithreaded cu date partajate, rulăm de mai multe ori programul ''thread-list-app'', până la obținerea unei erori:<code bash> | + | Ca să urmărim ce se întâmplă cu o aplicație multithreaded cu date partajate, rulăm de mai multe ori programul ''thread-list-app'', până la obținerea unei erori: |
- | ./thread-list-app | + | <code bash> |
+ | for i in $(seq 1 20); do ./thread-list-app; done | ||
</code> | </code> | ||
Line 50: | Line 56: | ||
Eroarea este cauzată de coruperea pointerilor din lista înlănțuită a programului. Datele sunt accesate concurent, iar în absența sincronizării, vor fi corupte. | Eroarea este cauzată de coruperea pointerilor din lista înlănțuită a programului. Datele sunt accesate concurent, iar în absența sincronizării, vor fi corupte. | ||
- | Soluția este să folosim un mutex pentru protejarea accesului la listă, lucru realizat în programul ''thread-list-app-mutex''. Dacă vom rula de mai multe ori programul ''thread-list-app-mutex'', nu vom obține niciodată eroare:<code bash> | + | Soluția este să folosim un mutex pentru protejarea accesului la listă, lucru realizat în programul ''thread-list-app-mutex''. Dacă vom rula de mai multe ori programul ''thread-list-app-mutex'', nu vom obține niciodată eroare: |
- | ./thread-list-app-mutex | + | <code bash> |
+ | for i in $(seq 1 20); do ./thread-list-app-mutex; done | ||
</code> | </code> | ||
Avem acces exclusiv la datele partajate deci am rezolvat problema coruperii datelor din cauza accesului concurent neprotejat. | Avem acces exclusiv la datele partajate deci am rezolvat problema coruperii datelor din cauza accesului concurent neprotejat. | ||
- | |||
- | ==== Consum de timp mutex și spinlock ==== | ||
- | |||
- | Dorim să investigăm overheadul produs când folosim spinlock-uri și mutex-uri pentru acces exclusiv. Pentru aceasta accesăm subdirectorul ''spinlock-mutex/''; urmărim conținutul fișierului ''spinlock-mutex.c''. Acest fișier are implementare didactică în care folosește spinlock-uri sau mutex-uri pentru asigurarea accesului exclusiv. | ||
- | |||
- | Compilăm cele două programe folosind ''make''. Rezultă două fișiere în format executabil: ''spinlock'' și ''mutex''. | ||
- | |||
- | Cele două fișiere au fost generate din același cod sursă (''spinlock-mutex.c''), după cum a fost definit sau nu macro-ul ''USE_SPINLOCK''. Macro-ul îl definim în fișierul ''Makefile''. | ||
- | |||
- | Pentru a contabiliza timpul de rulare rulăm cele două executabile prin comanda ''time'':<code bash> | ||
- | /usr/bin/time ./spinlock | ||
- | /usr/bin/time ./mutex | ||
- | </code> | ||
- | |||
- | Observăm că folosirea spinlock-urilor pentru accesul exclusiv la resurse rezultă într-un timp de rulare mai mic decât folosirea mutex-urilor. Aceasta se întâmplă pentru că avem regiune critică mică iar overhead-ul cauzat de operații pe mutex-uri este semnificativ mai mare decât cel cauzat de operații pe spinlock-uri. | ||
- | |||
- | Observăm din output-ul comenzii ''time'' că folosirea mutex-urilor înseamnă semnificativ mai multe schimbări de context: o operație de tip lock pe mutex are șanse mari să blocheze thread-ul curent și să invoce planificatorul pentru schimbarea contextului. De asemenea, observăm că programul ce folosește spinlock-uri petrece mai mult timp în user space (//User time//) și mai puțin în kernel space (//System time//). Aceasta se întâmplă pentru că implementarea de spinlock-uri este realizată în user space și toate acțiunile (inclusiv partea de busy waiting aferentă spinlock-urilor) au loc în user space. | ||
==== Race condition (TOCTTOU) ==== | ==== Race condition (TOCTTOU) ==== | ||
Line 99: | Line 89: | ||
Pentru a preveni apariția condițiilor de cursă, trebuie implementată corespunzător sincronizarea accesului. Din păcate, condițiile de cursă pot apărea foarte greu (adică programul să fie greșit dar să meargă **aproape** tot timpul); acest lucru face nedeterminist comportamentul programului și dificilă investigarea problemei. | Pentru a preveni apariția condițiilor de cursă, trebuie implementată corespunzător sincronizarea accesului. Din păcate, condițiile de cursă pot apărea foarte greu (adică programul să fie greșit dar să meargă **aproape** tot timpul); acest lucru face nedeterminist comportamentul programului și dificilă investigarea problemei. | ||
+ | |||
+ | ==== Granularitate regiune critică ==== | ||
+ | |||
+ | Dorim să urmărim cum se comportă un program atunci când diferă dimensiunea regiunii critice folosită pentru acces exclusiv. Pentru aceasta accesăm subdirectorul ''granularity/''; urmărim conținutul fișierului ''granularity.c''. Fișierul are o implementare didactică în care mai multe thread-uri accesează o regiune critică. | ||
+ | |||
+ | Compilăm programul folosind ''make''. Rezultă două fișiere în format executabil: ''granuarity-fine'' și ''granularity-coarse''. Fișierului executabil ''granularity-fine'' îi corespunde o regiune critică de mică dimensiune, dar accesată des; fișierului executabil ''granularity-coarse'' îi corespunde o regiune critică de dimensiune mai mare, dar accesată rar. | ||
+ | |||
+ | În cadrul codului, folosirea unei granularități fine sau nu este indicată de macro-ul ''GRANULARITY_TYPE'' inițializat în Makefile. | ||
+ | |||
+ | Pentru a vedea impactul tipului de granularitate, rulăm cele două fișiere în format executabil și măsurăm timpul de rulare:<code bash> | ||
+ | /usr/bin/time ./granularity-fine | ||
+ | /usr/bin/time ./granularity-coarse | ||
+ | </code> | ||
+ | |||
+ | Observăm că dureaza semnificativ mai mult rularea executabilului cu granularitate fină. Acest lucru se întâmplă pentru că regiunea critică pe care acesta o protejează este mică, iar cea mai mare parte din timp o va consuma în operațiile cu mutex-ul (//lock contention//). Observăm acest lucru și din numărul mare de schimbări de context (voluntare sau nevoluntare). | ||
+ | |||
+ | <note> | ||
+ | În general preferăm granularitate fină pentru regiunile critice (adică regiuni critice de mică dimensiune); dar granularitatea fină poate înseamna operații foarte dese pe mutex (//lock contention//). De aceea este recomandat ca folosirea granularității fine să fie echilibrată de un număr redus de thread-uri care să dorească să acceseze la un moment dat regiunea critică, pentru a genera cât mai puțină încărcare (//contention//) pe mutex. | ||
+ | </note> | ||
+ | |||
+ | ==== Operații atomice și neatomice ==== | ||
+ | |||
+ | Operațiile elementare (aritmetice) sunt în general executate de o singură instrucțiune de procesor (de exemplu instrucțiunea ''add'' sau instrucțiunea ''sub''). Cu toate acestea, o astfel de operație necesită citirea informației din memorie (//data fetch//) și apoi scrierea informației în memorie (//data write//) care sunt executate de instrucțiuni diferite. Dacă apare o întrerupere între aceste operații sau dacă lucrăm pe un sistem multiprocesor, rezultatul va fi imprevizibil: operația nu este atomică. | ||
+ | |||
+ | Pentru ca operația să fie atomică, se folosește suportul hardware care va bloca magistrala de comunicare cu memoria (//lock//) permițând accesul exclusiv la memorie; așa se întâmplă pe arhitectura x86. Altfel, pe ARM, se folosesc primitive hardware dedicate de acces exclusiv. | ||
+ | |||
+ | Pentru a demonstra operațiile atomice și neatomice pe accesăm subdirectorul ''sum-threads/'' (x86) sau subdirectorul ''sum-threads-arm'' (ARM); urmărim fișierul ''sum_threads.c'', identic în ambele subdirectoare. În fișier sunt create ''NUM_THREADS'' thread-uri care în ''NUM_ROUNDS'' runde efectuază operația ''sum += v'', cu ajutorul macro-ului ''do_op()''. Folosim suportul compilatorului GCC și realizăm atomizarea operației ''sum += v'' cu ajutorul construcției ''%%__sync_fetch_and_add()%%''. Atunci când definim macro-ul ''USE_ATOMIC'' realizăm operația în mod atomic. | ||
+ | |||
+ | <note important> | ||
+ | Pentru folosirea ''perf'' va trebui să instalați pachetul corespunzător versiunii de nucleu. Pe o distribuție Debian/Ubuntu, folosiți comanda: | ||
+ | <code> | ||
+ | sudo apt install linux-tools-$(uname -r) | ||
+ | </code> | ||
+ | </note> | ||
+ | |||
+ | === x86 === | ||
+ | |||
+ | Pentru x86, folosim subdirectorul ''sum-threads/''. La rularea comenzii ''make'' vom obține 6 fișiere executabile: | ||
+ | * ''sum_threads'': realizează operația în mod neatomic pe 64 de biți | ||
+ | * ''sum_threads_atomic'': realizează operația în mod atomic pe 64 de biți | ||
+ | * ''sum_threads_32'': realizează operația în mod neatomic pe 32 de biți | ||
+ | * ''sum_threads_atomic_32'': realizează operația în mod atomic pe 32 de biți | ||
+ | * ''sum_threads_32_longlong'': realizează operația în mod neatomic pe 32 biți folosind valori pe 64 de biți (tip ''long long'') | ||
+ | * ''sum_threads_atomic_32_longlong'': realizează operația în mod atomic pe 32 biți folosind valori pe 64 de biți (tip ''long long'') | ||
+ | |||
+ | La rulare, observăm că doar variantele atomice duc la rezultatul corect (''4500000''): | ||
+ | |||
+ | <code> | ||
+ | $ ./sum_threads | ||
+ | sum is: 682453 | ||
+ | $ ./sum_threads | ||
+ | sum is: 841578 | ||
+ | $ ./sum_threads_atomic | ||
+ | sum is: 4500000 | ||
+ | $ ./sum_threads_atomic | ||
+ | sum is: 4500000 | ||
+ | $ ./sum_threads_32 | ||
+ | sum is: 541841 | ||
+ | $ ./sum_threads_32 | ||
+ | sum is: 807242 | ||
+ | $ ./sum_threads_atomic_32 | ||
+ | sum is: 4500000 | ||
+ | $ ./sum_threads_atomic_32 | ||
+ | sum is: 4500000 | ||
+ | $ ./sum_threads_32_longlong | ||
+ | sum is: 1375752 | ||
+ | $ ./sum_threads_32_longlong | ||
+ | sum is: 627688 | ||
+ | $ ./sum_threads_atomic_32_longlong | ||
+ | sum is: 4500000 | ||
+ | $ ./sum_threads_atomic_32_longlong | ||
+ | sum is: 4500000 | ||
+ | </code> | ||
+ | |||
+ | Ca să vedem diferențele între implementări dezasamblăm codul din executabile și urmărim în funcția ''thread_func'' secvența în limbaj de asamblare pentru ''sum += v'' (în forma neatomică sau în forma atomică dată de construcția ''%%__sync_fetch_and_add()%%''): | ||
+ | |||
+ | <code> | ||
+ | $ objdump -d -M intel sum_threads | grep -A 12 '^[0-9a-f]\+ <thread_func>:' | ||
+ | 0000000000400737 <thread_func>: | ||
+ | [...] | ||
+ | 400751: 48 8b 15 18 09 20 00 mov rdx,QWORD PTR [rip+0x200918] # 601070 <sum> | ||
+ | 400758: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] | ||
+ | 40075c: 48 01 d0 add rax,rdx | ||
+ | 40075f: 48 89 05 0a 09 20 00 mov QWORD PTR [rip+0x20090a],rax # 601070 <sum> | ||
+ | |||
+ | $ objdump -d -M intel sum_threads_atomic | grep -A 11 '^[0-9a-f]\+ <thread_func>:' | ||
+ | 0000000000400737 <thread_func>: | ||
+ | [...] | ||
+ | 400751: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] | ||
+ | 400755: f0 48 01 05 13 09 20 lock add QWORD PTR [rip+0x200913],rax # 601070 <sum> | ||
+ | 40075c: 00 | ||
+ | |||
+ | $ objdump -d -M intel sum_threads_32 | grep -A 11 '[0-9a-f]\+ <thread_func>:' | ||
+ | 080485e6 <thread_func>: | ||
+ | [...] | ||
+ | 80485fb: 8b 15 40 a0 04 08 mov edx,DWORD PTR ds:0x804a040 | ||
+ | 8048601: 8b 45 fc mov eax,DWORD PTR [ebp-0x4] | ||
+ | 8048604: 01 d0 add eax,edx | ||
+ | 8048606: a3 40 a0 04 08 mov ds:0x804a040,eax | ||
+ | |||
+ | $ objdump -d -M intel sum_threads_atomic_32 | grep -A 9 '[0-9a-f]\+ <thread_func>:' | ||
+ | 080485e6 <thread_func>: | ||
+ | [...] | ||
+ | 80485fb: 8b 45 fc mov eax,DWORD PTR [ebp-0x4] | ||
+ | 80485fe: f0 01 05 40 a0 04 08 lock add DWORD PTR ds:0x804a040,eax | ||
+ | |||
+ | $ objdump -d -M intel sum_threads_32_longlong | grep -A 16 '[0-9a-f]\+ <thread_func>:' | ||
+ | 080485e6 <thread_func>: | ||
+ | [...] | ||
+ | 80485fc: 8b 4d f4 mov ecx,DWORD PTR [ebp-0xc] | ||
+ | 80485ff: bb 00 00 00 00 mov ebx,0x0 | ||
+ | 8048604: a1 40 a0 04 08 mov eax,ds:0x804a040 | ||
+ | 8048609: 8b 15 44 a0 04 08 mov edx,DWORD PTR ds:0x804a044 | ||
+ | 804860f: 01 c8 add eax,ecx | ||
+ | 8048611: 11 da adc edx,ebx | ||
+ | 8048613: a3 40 a0 04 08 mov ds:0x804a040,eax | ||
+ | 8048618: 89 15 44 a0 04 08 mov DWORD PTR ds:0x804a044,edx | ||
+ | |||
+ | $ objdump -d -M intel sum_threads_atomic_32_longlong | grep -A 27 '[0-9a-f]\+ <thread_func>:' | ||
+ | 080485e6 <thread_func>: | ||
+ | [...] | ||
+ | 80485fe: 8b 75 ec mov esi,DWORD PTR [ebp-0x14] | ||
+ | 8048601: bf 00 00 00 00 mov edi,0x0 | ||
+ | 8048606: a1 40 a0 04 08 mov eax,ds:0x804a040 | ||
+ | 804860b: 8b 15 44 a0 04 08 mov edx,DWORD PTR ds:0x804a044 | ||
+ | 8048611: 89 c1 mov ecx,eax | ||
+ | 8048613: 89 d3 mov ebx,edx | ||
+ | 8048615: 01 f1 add ecx,esi | ||
+ | 8048617: 11 fb adc ebx,edi | ||
+ | 8048619: 89 4d d8 mov DWORD PTR [ebp-0x28],ecx | ||
+ | 804861c: 89 5d dc mov DWORD PTR [ebp-0x24],ebx | ||
+ | 804861f: 8b 5d d8 mov ebx,DWORD PTR [ebp-0x28] | ||
+ | 8048622: 8b 4d dc mov ecx,DWORD PTR [ebp-0x24] | ||
+ | 8048625: f0 0f c7 0d 40 a0 04 lock cmpxchg8b QWORD PTR ds:0x804a040 | ||
+ | 804862c: 08 | ||
+ | 804862d: 0f 94 c1 sete cl | ||
+ | 8048630: 84 c9 test cl,cl | ||
+ | 8048632: 74 dd je 8048611 <thread_func+0x2b> | ||
+ | </code> | ||
+ | |||
+ | În cazul ''sum_threads'' (neatomic, 64 de biți), pentru lucrul cu variabila ''sum'' (''rip+0x200918'') se efectuează trei instrucțiuni: data fetch, add și write back. Între cele trei instrucțiuni pot apărea întreruperi și atunci ansamblul de instrucțiuni este neatomic: **operația este neatomică**. | ||
+ | |||
+ | În cazul ''sum_threads_atomic'' (atomic, 64 de biți), pentru lucrul cu variabila ''sum'' (''rip+0x200913'') se efectuează o singură instrucțiune (''add''), deci operația este atomică la nivelul procesorului. Pentru cazul multi-core se protejează instrucțiunea cu prefixul ''lock'' care asigură accesul exclusiv la magistrală pe durata instrucțiunii, compusă din ciclul clasic: data fetch, add și writeback. În felul acesta **operația este atomică inclusiv în context multi-core**. | ||
+ | |||
+ | În cazul ''sum_threads_32'' (neatomic, 32 de biți), pentru lucrul cu variabila ''sum'' (''ds:0x804a040'') se efectuează trei instrucțiuni la fel în cazul ''sum_threads'': **operația este neatomică**. | ||
+ | |||
+ | În cazul ''sum_threads_atomic_32'' (atomic, 32 de biți), pentru lucrul cu variabila ''sum'' (''ds:0x804a040'') se efectuează o singură instrucțiuni (''add'') protejată de prefixul ''lock'' pentru cazul multi-core: **operația este atomică inclusiv în context multi-core**. | ||
+ | |||
+ | În cazul ''sum_threads_32_longlong'' (neatomic, 32 de biți), pentru lucrul cu variabila ''sum'' (''ds:0x804a040'', 8 octeți) se folosesc perechi de registre pe 32 de biți pentru a efectua operații pe 64 de biți (tipul de date ''long long''). Sunt folosite registrele ''edx:eax'' pentru reține suma (''sum'') și registrele ''ebx:ecx'' pentru a reține valoarea (''val'') apoi sunt adunate două câte două (''edx'' cu ''ebx'' și ''eax'' cu ''ecx''). Sunt multe operații între care poate apărea o instrucțiune și atunci ansamblul de instrucțiuni este neatomic: **operația este neatomică**. | ||
+ | |||
+ | În cazul ''sum_threads_atomic_32_longlong'' (neatomic, 32 de biți), pentru lucrul cu variabila ''sum'' (''ds:0x804a040'', 8 octeți) se folosesc perechi de registre pe 32 de biți pentru a efectua operații pe 64 de biți (tipul de date ''long long''). Sunt folosite registrele ''edx:eax'' pentru reține suma (''sum''), registrele ''edi:esi'' pentru a reține valoarea (''val'') apoi sunt adunate două câte două în registrele ''ebx:ecx'' (''ebx <- edx + edi'', ''ecx <- eax + esi''). Sunt multe operații între care poate apărea o instrucțiune și atunci ansamblul de instrucțiuni este neatomic. Punctul cheie este folosirea instrucțiunii [[http://faydoc.tripod.com/cpu/cmpxchg8b.htm|cmpxchg8b]]. Instrucțiunea compară în mod atomic valoarea sumei din memorie (''ds:0x804a040'') cu suma reținută în registre (''edx:eax''). Dacă acestea sunt identice (adică dacă **NU** au fost modificări ale sumei în memorie - de alte thread / procesor), atunci se încarcă în memorie noua valoare, din registrele ''ebx:ecx''. Astfel avem suma nou calculată. În același timp se activează ''ZF'' (//zero flag//); dacă ZF a fost activat, operația a reușit. Altfel, se reîncearcă operația prin salt la adresa de început a secvenței (''0x8048611''). În final, după eventual mai multe încercări, operația va reuși atomic. Operația ''cmpxchg8b'' este protejată de prefixul ''lock'' pentru cazul multi-core: **operația este atomică inclusiv în context multi-core**. | ||
+ | |||
+ | Pentru a verifica impactul de performanță dat de operațiile atomice, putem folosi ''perf'': | ||
+ | |||
+ | <code> | ||
+ | $ sudo perf stat ./sum_threads | ||
+ | sum is: 974359 | ||
+ | |||
+ | Performance counter stats for './sum_threads': | ||
+ | |||
+ | 22.408346 task-clock (msec) # 5.670 CPUs utilized | ||
+ | 4 cpu-migrations # 0.179 K/sec | ||
+ | 85 page-faults # 0.004 M/sec | ||
+ | 62,480,526 cycles # 2.788 GHz | ||
+ | 8,715,598 instructions # 0.14 insn per cycle | ||
+ | 1,328,346 branches # 59.279 M/sec | ||
+ | 14,605 branch-misses # 1.10% of all branches | ||
+ | |||
+ | 0.003952352 seconds time elapsed | ||
+ | |||
+ | $ sudo perf stat ./sum_threads_atomic | ||
+ | sum is: 4500000 | ||
+ | |||
+ | Performance counter stats for './sum_threads_atomic': | ||
+ | |||
+ | 116.167475 task-clock (msec) # 6.852 CPUs utilized | ||
+ | 9 context-switches # 0.077 K/sec | ||
+ | 4 cpu-migrations # 0.034 K/sec | ||
+ | 81 page-faults # 0.697 K/sec | ||
+ | 428,643,912 cycles # 3.690 GHz | ||
+ | 6,810,381 instructions # 0.02 insn per cycle | ||
+ | 1,345,633 branches # 11.584 M/sec | ||
+ | 15,216 branch-misses # 1.13% of all branches | ||
+ | |||
+ | 0.016954975 seconds time elapsed | ||
+ | |||
+ | $ sudo perf stat ./sum_threads_32 | ||
+ | sum is: 899485 | ||
+ | |||
+ | Performance counter stats for './sum_threads_32': | ||
+ | |||
+ | 21.302230 task-clock (msec) # 5.606 CPUs utilized | ||
+ | 8 context-switches # 0.376 K/sec | ||
+ | 4 cpu-migrations # 0.188 K/sec | ||
+ | 61 page-faults # 0.003 M/sec | ||
+ | 78,517,636 cycles # 3.686 GHz | ||
+ | 8,510,544 instructions # 0.11 insn per cycle | ||
+ | 1,293,122 branches # 60.704 M/sec | ||
+ | 13,379 branch-misses # 1.03% of all branches | ||
+ | |||
+ | 0.003799853 seconds time elapsed | ||
+ | |||
+ | $ sudo perf stat ./sum_threads_atomic_32 | ||
+ | sum is: 4500000 | ||
+ | |||
+ | Performance counter stats for './sum_threads_atomic_32': | ||
+ | |||
+ | 134.351733 task-clock (msec) # 7.151 CPUs utilized | ||
+ | 8 context-switches # 0.060 K/sec | ||
+ | 3 cpu-migrations # 0.022 K/sec | ||
+ | 62 page-faults # 0.461 K/sec | ||
+ | 453,735,505 cycles # 3.377 GHz | ||
+ | 6,725,869 instructions # 0.01 insn per cycle | ||
+ | 1,331,426 branches # 9.910 M/sec | ||
+ | 14,755 branch-misses # 1.11% of all branches | ||
+ | |||
+ | 0.018787564 seconds time elapsed | ||
+ | |||
+ | $ sudo perf stat ./sum_threads_32_longlong | ||
+ | sum is: 1661489 | ||
+ | |||
+ | Performance counter stats for './sum_threads_32_longlong': | ||
+ | |||
+ | 33.744994 task-clock (msec) # 6.420 CPUs utilized | ||
+ | 8 context-switches # 0.237 K/sec | ||
+ | 3 cpu-migrations # 0.089 K/sec | ||
+ | 60 page-faults # 0.002 M/sec | ||
+ | 124,455,183 cycles # 3.688 GHz | ||
+ | 12,602,237 instructions # 0.10 insn per cycle | ||
+ | 1,309,271 branches # 38.799 M/sec | ||
+ | 13,912 branch-misses # 1.06% of all branches | ||
+ | |||
+ | 0.005256141 seconds time elapsed | ||
+ | |||
+ | $ sudo perf stat ./sum_threads_atomic_32_longlong | ||
+ | sum is: 4500000 | ||
+ | |||
+ | Performance counter stats for './sum_threads_atomic_32_longlong': | ||
+ | |||
+ | 649.797615 task-clock (msec) # 7.242 CPUs utilized | ||
+ | 25 context-switches # 0.038 K/sec | ||
+ | 4 cpu-migrations # 0.006 K/sec | ||
+ | 61 page-faults # 0.094 K/sec | ||
+ | 2,329,594,557 cycles # 3.585 GHz | ||
+ | 61,285,022 instructions # 0.03 insn per cycle | ||
+ | 5,775,210 branches # 8.888 M/sec | ||
+ | 1,012,473 branch-misses # 17.53% of all branches | ||
+ | |||
+ | 0.089722917 seconds time elapsed | ||
+ | </code> | ||
+ | |||
+ | Observăm că durata și numărul de cicli de procesor sunt mai mari în cazul atomic decât în cazul atomic, foarte evident în cazul operației atomiei pe 32 de biți cu numere pe 64 de biți unde se poate repeta de mai multe ori secvența de adunare pentru a fi corect rezultatul. | ||
+ | |||
+ | === ARM === | ||
+ | |||
+ | <note important> | ||
+ | Pentru ARM trebuie să avem instalate pachetele pentru cross-compiling pe ARM și rulare sub emulatorul QEMU. Pe distribuțiile Debian/Ubuntu, rulăm: | ||
+ | <code> | ||
+ | sudo apt-get install gcc make gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi libc6-armel-cross libc6-dev-armel-cross | ||
+ | sudo apt-get install qemu-system-arm qemu-user | ||
+ | </code> | ||
+ | </note> | ||
+ | |||
+ | Pentru ARM, folosim subdirectorul ''sum-threads-arm/''. La rularea comenzii ''make'' vom obține 2 fișiere executabile: | ||
+ | * ''sum_threads'': realizează operația în mod neatomic | ||
+ | * ''sum_threads_atomic'': realizează operația în mod atomic | ||
+ | |||
+ | La rulare, observăm că doar variantele atomice duc la rezultatul corect (''4500000''): | ||
+ | |||
+ | <code> | ||
+ | $ ./sum_threads | ||
+ | sum is: 1333911 | ||
+ | $ ./sum_threads | ||
+ | sum is: 1020572 | ||
+ | $ ./sum_threads_atomic | ||
+ | sum is: 4500000 | ||
+ | </code> | ||
+ | |||
+ | Ca să vedem diferențele între implementări dezasamblăm codul din executabile și urmărim în funcția ''thread_func'' secvența în limbaj de asamblare pentru ''sum += v'' (în forma neatomică sau în forma atomică dată de construcția ''%%__sync_fetch_and_add()%%''): | ||
+ | |||
+ | <code> | ||
+ | $ arm-linux-gnueabi-objdump -d sum_threads | grep -A 15 '^[0-9a-f]\+ <thread_func>:' | ||
+ | 000102dc <thread_func>: | ||
+ | [...] | ||
+ | 10300: e59f3040 ldr r3, [pc, #64] ; 10348 <thread_func+0x6c> | ||
+ | 10304: e5932000 ldr r2, [r3] | ||
+ | 10308: e51b3008 ldr r3, [fp, #-8] | ||
+ | 1030c: e0823003 add r3, r2, r3 | ||
+ | 10310: e59f2030 ldr r2, [pc, #48] ; 10348 <thread_func+0x6c> | ||
+ | 10314: e5823000 str r3, [r2] | ||
+ | |||
+ | $ arm-linux-gnueabi-objdump -d sum_threads_atomic | grep -A 13 '^[0-9a-f]\+ <thread_func>:' | ||
+ | 000102dc <thread_func>: | ||
+ | [...] | ||
+ | 10300: e59f3034 ldr r3, [pc, #52] ; 1033c <thread_func+0x60> | ||
+ | 10304: e51b1008 ldr r1, [fp, #-8] | ||
+ | 10308: e1a00003 mov r0, r3 | ||
+ | 1030c: eb00181d bl 16388 <__sync_fetch_and_add_4> | ||
+ | $ arm-linux-gnueabi-objdump -d sum_threads_atomic | grep -A 14 '^[0-9a-f]\+ <__sync_fetch_and_add_4>:' | ||
+ | 00016388 <__sync_fetch_and_add_4>: | ||
+ | 16388: e92d41f0 push {r4, r5, r6, r7, r8, lr} | ||
+ | 1638c: e1a05000 mov r5, r0 | ||
+ | 16390: e1a07001 mov r7, r1 | ||
+ | 16394: e59f6020 ldr r6, [pc, #32] ; 163bc <__sync_fetch_and_add_4+0x34> | ||
+ | 16398: e5954000 ldr r4, [r5] | ||
+ | 1639c: e1a02005 mov r2, r5 | ||
+ | 163a0: e0841007 add r1, r4, r7 | ||
+ | 163a4: e1a00004 mov r0, r4 | ||
+ | 163a8: e12fff36 blx r6 | ||
+ | 163ac: e3500000 cmp r0, #0 | ||
+ | 163b0: 1afffff8 bne 16398 <__sync_fetch_and_add_4+0x10> | ||
+ | 163b4: e1a00004 mov r0, r4 | ||
+ | 163b8: e8bd81f0 pop {r4, r5, r6, r7, r8, pc} | ||
+ | 163bc: ffff0fc0 .word 0xffff0fc0 | ||
+ | </code> | ||
+ | |||
+ | În cazul ''sum_threads'' (neatomic), pentru lucrul cu variabila ''sum'' (adresa ''0x10348'') se efectuează trei instrucțiuni: data fetch(''ldr''), add și write back (''str''). Între cele trei instrucțiuni pot apărea întreruperi și atunci ansamblul de instrucțiuni este neatomic: **operația este neatomică**. | ||
+ | |||
+ | În cazul ''sum_threads_atomic'' (atomic), pentru lucrul cu variabila ''sum'' (adresa ''0x1033c'') se apelează funcția ''%%__sync_fetch_and_add_4()%%'' unde are loc toată magia. Funcția are un comportament similar implementării sincronizării pentru ''sum_thread_atomic_32_longlong'' pentru x86. Partea cea mai interesantă este apelul ''blx r6'' care duce la execuția codului de la adresa ''0xffff0fc0'' (''r6'' are valoarea ''0x163bc''). La acea adresă se găsește un cod din kernel space accesibil user space, numit [[https://www.kernel.org/doc/Documentation/arm/kernel_user_helpers.txt|cod kernel user helper]], anume implementarea funcției ''%%__kuser_cmpxchg%%''. În contextul funcției ''%%__sync_fetch_and_add_4()%%'', aceasta compară valoarea registrului ''r4'' (suma inițială) cu ce se găsește la adresa indicată de ''r5'' (suma din memorie); dacă sunt egale, pune la valorea indicată de registrul ''r5'' noua sumă, din registrul ''r1''. Dacă operația a reușit, în registrul ''r0'' se plasează valoarea ''0''. Dacă nu a reușit, se reîncearcă operația prin intermediul instrucțiunii ''bne 16398''. În felul acesta **operația este atomică**. | ||
+ | |||
+ | Întrucât acest cod rulează emulat, nu este atât de relevantă comparația timpilor de rulare. Dar putem investiga folosind comanda ''time'': | ||
+ | |||
+ | <code> | ||
+ | $ /usr/bin/time -v ./sum_threads | ||
+ | sum is: 1306038 | ||
+ | Command being timed: "./sum_threads" | ||
+ | User time (seconds): 0.09 | ||
+ | System time (seconds): 0.00 | ||
+ | Percent of CPU this job got: 333% | ||
+ | Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.02 | ||
+ | Average shared text size (kbytes): 0 | ||
+ | Average unshared data size (kbytes): 0 | ||
+ | Average stack size (kbytes): 0 | ||
+ | Average total size (kbytes): 0 | ||
+ | Maximum resident set size (kbytes): 9380 | ||
+ | Average resident set size (kbytes): 0 | ||
+ | Major (requiring I/O) page faults: 0 | ||
+ | Minor (reclaiming a frame) page faults: 1041 | ||
+ | Voluntary context switches: 1015 | ||
+ | Involuntary context switches: 3 | ||
+ | Swaps: 0 | ||
+ | File system inputs: 0 | ||
+ | File system outputs: 0 | ||
+ | Socket messages sent: 0 | ||
+ | Socket messages received: 0 | ||
+ | Signals delivered: 0 | ||
+ | Page size (bytes): 4096 | ||
+ | Exit status: 0 | ||
+ | |||
+ | $ /usr/bin/time -v ./sum_threads_atomic | ||
+ | sum is: 4500000 | ||
+ | Command being timed: "./sum_threads_atomic" | ||
+ | User time (seconds): 42.39 | ||
+ | System time (seconds): 45.72 | ||
+ | Percent of CPU this job got: 449% | ||
+ | Elapsed (wall clock) time (h:mm:ss or m:ss): 0:19.60 | ||
+ | Average shared text size (kbytes): 0 | ||
+ | Average unshared data size (kbytes): 0 | ||
+ | Average stack size (kbytes): 0 | ||
+ | Average total size (kbytes): 0 | ||
+ | Maximum resident set size (kbytes): 9420 | ||
+ | Average resident set size (kbytes): 0 | ||
+ | Major (requiring I/O) page faults: 0 | ||
+ | Minor (reclaiming a frame) page faults: 1051 | ||
+ | Voluntary context switches: 18502292 | ||
+ | Involuntary context switches: 588 | ||
+ | Swaps: 0 | ||
+ | File system inputs: 0 | ||
+ | File system outputs: 0 | ||
+ | Socket messages sent: 0 | ||
+ | Socket messages received: 0 | ||
+ | Signals delivered: 0 | ||
+ | Page size (bytes): 4096 | ||
+ | Exit status: 0 | ||
+ | </code> | ||
+ | |||
+ | Observăm o durată mult mai mare pentru varianta atomică. Operațiile atomice serializează secvențe de cod și au impact de performanță. | ||
+ | |||
+ | ==== Implementarea accesului exclusiv ==== | ||
+ | |||
+ | Dorim să investigăm diferite moduri de a realiza accesul exclusiv în modul busy waiting pentru crearea unei regiuni critice. Pentru aceasta accesăm subdirectorul ''lock/''; urmărim conținutul fișierului ''lock.c'' unde avem implementate 3 forme de locking: busy waiting simplu dar incorect (''simple_lock()'' și ''simple_unlock()''), busy waiting folosind suportul compilatorului (cu funcția ''_sync_bool_compare_and_swap()'') și busy waiting folosind spinlock-uri POSIX. | ||
+ | |||
+ | În director se găsesc deja compilate 3 fișiere executabile: | ||
+ | * ''lock'': folosește implementarea simplă | ||
+ | * ''lock_atomic'': folosește suportul compilatorului | ||
+ | * ''lock_spin'': folosește spinlock-uri POSIX | ||
+ | |||
+ | <note important> | ||
+ | **NU** recompilați fișierele executabile folosind comanda ''make'' pentru a nu modifica fișierele. Vom dezasambla codul și atunci ne sunt utile informațiile neschimbate (adrese). | ||
+ | </note> | ||
+ | |||
+ | La rularea celor trei executabile vedem că implementarea simplă este incorectă, în vreme ce implementările celelalte sunt corecte: | ||
+ | <code> | ||
+ | $ ./lock | ||
+ | sum is: 1042179 | ||
+ | $ ./lock | ||
+ | sum is: 1173743 | ||
+ | $ ./lock_atomic | ||
+ | sum is: 4500000 | ||
+ | $ ./lock_atomic | ||
+ | sum is: 4500000 | ||
+ | $ ./lock_spin | ||
+ | sum is: 4500000 | ||
+ | $ ./lock_spin | ||
+ | sum is: 4500000 | ||
+ | </code> | ||
+ | |||
+ | Implementarea simplă este incorectă pentru că între comparația din bucla ''while'' din funcția ''simple_lock()'' și atribuirea ''glock = 1'' programul poate fi întrerupt. | ||
+ | |||
+ | Ca să vedem impactul de performanță al fiecărei implementări putem folosi ''perf'': | ||
+ | <code> | ||
+ | $ sudo perf stat ./lock | ||
+ | sum is: 1531839 | ||
+ | |||
+ | Performance counter stats for './lock': | ||
+ | |||
+ | 71.512060 task-clock (msec) # 6.824 CPUs utilized | ||
+ | 15 context-switches # 0.210 K/sec | ||
+ | 4 cpu-migrations # 0.056 K/sec | ||
+ | 81 page-faults # 0.001 M/sec | ||
+ | 263,849,410 cycles # 3.690 GHz | ||
+ | 26,814,836 instructions # 0.10 insn per cycle | ||
+ | 6,355,162 branches # 88.868 M/sec | ||
+ | 32,210 branch-misses # 0.51% of all branches | ||
+ | |||
+ | 0.010480000 seconds time elapsed | ||
+ | |||
+ | $ sudo perf stat ./lock_atomic | ||
+ | sum is: 4500000 | ||
+ | |||
+ | Performance counter stats for './lock_atomic': | ||
+ | |||
+ | 2267.178750 task-clock (msec) # 7.680 CPUs utilized | ||
+ | 58 context-switches # 0.026 K/sec[K | ||
+ | 7 cpu-migrations # 0.003 K/sec | ||
+ | 81 page-faults # 0.036 K/sec | ||
+ | 8,338,793,835 cycles # 3.678 GHz | ||
+ | 150,365,398 instructions # 0.02 insn per cycle | ||
+ | 23,652,236 branches # 10.432 M/sec | ||
+ | 959,916 branch-misses # 4.06% of all branches | ||
+ | |||
+ | 0.295213041 seconds time elapsed | ||
+ | |||
+ | $ sudo perf stat ./lock_spin | ||
+ | sum is: 4500000 | ||
+ | |||
+ | Performance counter stats for './lock_spin': | ||
+ | |||
+ | 841.995688 task-clock (msec) # 7.698 CPUs utilized | ||
+ | 33 context-switches # 0.039 K/sec | ||
+ | 5 cpu-migrations # 0.006 K/sec | ||
+ | 45 page-faults # 0.053 K/sec | ||
+ | 2,992,569,499 cycles # 3.554 GHz | ||
+ | 76,846,679 instructions # 0.03 insn per cycle | ||
+ | 32,136,591 branches # 38.167 M/sec | ||
+ | 1,832,402 branch-misses # 5.70% of all branches | ||
+ | |||
+ | 0.109379961 seconds time elapsed | ||
+ | </code> | ||
+ | |||
+ | Așa cum era de așteptat, implementarea simplă (dar incorectă) este rapidă. Implementările corecte sunt mai încete cu o performanță mai ridicată pentru implementarea cu spinlock-uri POSIX. | ||
+ | |||
+ | Pentru detalii, dezasamblăm codul celor 3 executabile: | ||
+ | |||
+ | <code> | ||
+ | $ objdump -d -M intel lock | grep -A 8 '[0-9a-f]\+ <simple_lock>:' | ||
+ | 00000000004009bd <simple_lock>: | ||
+ | [...] | ||
+ | 4009c2: 8b 05 e8 16 20 00 mov eax,DWORD PTR [rip+0x2016e8] # 6020b0 <glock> | ||
+ | 4009c8: 85 c0 test eax,eax | ||
+ | 4009ca: 75 f6 jne 4009c2 <simple_lock+0x5> | ||
+ | 4009cc: c7 05 da 16 20 00 01 mov DWORD PTR [rip+0x2016da],0x1 # 6020b0 <glock> | ||
+ | 4009d3: 00 00 00 | ||
+ | |||
+ | $ objdump -d -M intel lock_atomic | grep -A 11 '[0-9a-f]\+ <atomic_lock>:' | ||
+ | 0000000000400966 <atomic_lock>: | ||
+ | [...] | ||
+ | 40096b: b8 00 00 00 00 mov eax,0x0 | ||
+ | 400970: ba 01 00 00 00 mov edx,0x1 | ||
+ | 400975: f0 0f b1 15 33 17 20 lock cmpxchg DWORD PTR [rip+0x201733],edx # 6020b0 <glock> | ||
+ | 40097c: 00 | ||
+ | 40097d: 0f 94 c0 sete al | ||
+ | 400980: 83 f0 01 xor eax,0x1 | ||
+ | 400983: 84 c0 test al,al | ||
+ | 400985: 75 e4 jne 40096b <atomic_lock+0x5> | ||
+ | |||
+ | $ objdump -d -M intel lock_spin | grep -A 7 '[0-9a-f]\+ <spin_lock>:' | ||
+ | 0000000000400a03 <spin_lock>: | ||
+ | 400a03: 55 push rbp | ||
+ | 400a04: 48 89 e5 mov rbp,rsp | ||
+ | 400a07: bf f4 ab 6d 00 mov edi,0x6dabf4 | ||
+ | 400a0c: e8 5f 3f 00 00 call 404970 <pthread_spin_lock> | ||
+ | 400a11: 90 nop | ||
+ | 400a12: 5d pop rbp | ||
+ | 400a13: c3 ret | ||
+ | $ objdump -d -M intel lock_spin | grep -A 4 '[0-9a-f]\+ <pthread_spin_lock>:' | ||
+ | 0000000000404970 <pthread_spin_lock>: | ||
+ | 404970: f0 ff 0f lock dec DWORD PTR [rdi] | ||
+ | 404973: 75 0b jne 404980 <pthread_spin_lock+0x10> | ||
+ | 404975: 31 c0 xor eax,eax | ||
+ | 404977: c3 ret | ||
+ | </code> | ||
+ | |||
+ | În cazul implementării simple (dar incorecte), funcția ''simple_lock()'' conține mai multe instrucțiuni (atribuire, comparație, salt); o întrerupere între acestea va duce la rezultate inconsecvente, de unde implementarea incorectă (neatomică). | ||
+ | |||
+ | În cazul suportului de compilator (folosind ''%%__sync_bool_compare_and_swap%%'') implementarea este similară cu ''sum_threads_atomic_32_longlong'' cu o comparație atomică ''lock cmpxhcg'' și apoi reluarea secvenței dacă între timp zona de memorie s-a modificat. | ||
+ | |||
+ | În cazul spinlock-ului POSIX, implementarea este mai simplă de unde și performanța mai bună față de cazul suportului în compilator. Funcția ''spin_lock()'' apelează funcția ''pthread_spin_lock()''. Aceasta din urmă decrementează valoarea din variabila care reprezintă spinlock-ul (de la adresa indicată de registrul ''rdi'' - o valoare întreagă). Doar în cazul în care această valoare era ''1'' și ajunge în cazul decrementării la ''0'' se va ieși din bucla funcției; altfel se va bucla continuu până când un thread va elibera spinlock-ul prin atribuirea valorii 1 spinlock-ului. | ||
+ | |||
+ | ==== Consum de timp mutex și spinlock ==== | ||
+ | |||
+ | Dorim să investigăm overheadul produs când folosim spinlock-uri și mutex-uri pentru acces exclusiv. Pentru aceasta accesăm subdirectorul ''spinlock-mutex/''; urmărim conținutul fișierului ''spinlock-mutex.c''. Acest fișier are implementare didactică în care folosește spinlock-uri sau mutex-uri pentru asigurarea accesului exclusiv. | ||
+ | |||
+ | Compilăm cele două programe folosind ''make''. Rezultă două fișiere în format executabil: ''spinlock'' și ''mutex''. | ||
+ | |||
+ | Cele două fișiere au fost generate din același cod sursă (''spinlock-mutex.c''), după cum a fost definit sau nu macro-ul ''USE_SPINLOCK''. Macro-ul îl definim în fișierul ''Makefile''. | ||
+ | |||
+ | Pentru a contabiliza timpul de rulare rulăm cele două executabile prin comanda ''time'':<code bash> | ||
+ | /usr/bin/time -v ./spinlock | ||
+ | /usr/bin/time -v ./mutex | ||
+ | </code> | ||
+ | |||
+ | Observăm că folosirea spinlock-urilor pentru accesul exclusiv la resurse rezultă într-un timp de rulare mai mic decât folosirea mutex-urilor. Aceasta se întâmplă pentru că avem regiune critică mică iar overhead-ul cauzat de operații pe mutex-uri este semnificativ mai mare decât cel cauzat de operații pe spinlock-uri. | ||
+ | |||
+ | Observăm din output-ul comenzii ''time'' că folosirea mutex-urilor înseamnă semnificativ mai multe schimbări de context: o operație de tip lock pe mutex are șanse mari să blocheze thread-ul curent și să invoce planificatorul pentru schimbarea contextului. De asemenea, observăm că programul ce folosește spinlock-uri petrece mai mult timp în user space (//User time//) și mai puțin în kernel space (//System time//). Aceasta se întâmplă pentru că implementarea de spinlock-uri este realizată în user space și toate acțiunile (inclusiv partea de busy waiting aferentă spinlock-urilor) au loc în user space. | ||
+ | |||
==== Deadlock ==== | ==== Deadlock ==== | ||
Line 115: | Line 631: | ||
Pentru a evita apariția deadlock-urilor în momentul în care folosim lock-uri/mutex-uri, o condiție importantă este ca lock-urile să fie achiziționate în aceeași ordine. | Pentru a evita apariția deadlock-urilor în momentul în care folosim lock-uri/mutex-uri, o condiție importantă este ca lock-urile să fie achiziționate în aceeași ordine. | ||
</note> | </note> | ||
+ | |||
==== Așteptare nedefinită ==== | ==== Așteptare nedefinită ==== | ||
Line 164: | Line 681: | ||
După actualizarea programului, recompilăm folosind ''make'' și apoi îl rulăm. Acum programul va funcționa corespunzător fără problema așteptării nedefinite. | După actualizarea programului, recompilăm folosind ''make'' și apoi îl rulăm. Acum programul va funcționa corespunzător fără problema așteptării nedefinite. | ||
- | ==== Granularitate regiune critică ==== | ||
- | Dorim să urmărim cum se comportă un program atunci când diferă dimensiunea regiunii critice folosită pentru acces exclusiv. Pentru aceasta accesăm subdirectorul ''granularity/''; urmărim conținutul fișierului ''granularity.c''. Fișierul are o implementare didactică în care mai multe thread-uri accesează o regiune critică. | + | ==== Buffer circular ==== |
- | Compilăm programul folosind ''make''. Rezultă două fișiere în format executabil: ''granuarity-fine'' și ''granularity-coarse''. Fișierului executabil ''granularity-fine'' îi corespunde o regiune critică de mică dimensiune, dar accesată des; fișierului executabil ''granularity-coarse'' îi corespunde o regiune critică de dimensiune mai mare, dar accesată rar. | + | În multe dintre cazurile în care există entități de tipul producător și entități de tipul consumator, structura folosită este un buffer circular (//ring buffer//). Într-un astfel de buffer producătorii folosesc un capăt al buffer-ului, iar consumatorii celălalt capăt. |
- | În cadrul codului, folosirea unei granularități fine sau nu este indicată de macro-ul ''GRANULARITY_TYPE'' inițializat în Makefile. | + | O implementare de buffer circular, cu sincronizarea aferentă, se găsește în subdirectorul ''ring-buffer/''. Structura buffer-ului circular (''struct rbuf'') este declarată în fișierul header ''ring_buffer.h'', împreună cu declarațiile funcțiilor. Implementarea funcțiilor se găsește în fișierul cod sursă ''ring_buffer.c''. |
- | Pentru a vedea impactul tipului de granularitate, rulăm cele două fișiere în format executabil și măsurăm timpul de rulare:<code bash> | + | În fișierul cod sursă ''producer_consumer.c'' se creează ''NUM_CONSUMERS'' thread-uri de tip consumator și ''NUM_PRODUCERS'' thread-uri de tip producător. În timp de ''NUM_ROUNDS'' runde, aceste thread-uri consumă, respectiv produc câte un element folosind buffer-ul circular. |
- | /usr/bin/time ./granularity-fine | + | |
- | /usr/bin/time ./granularity-coarse | + | Compilăm folosind ''make'' și obținem fișierul executabil ''producer_consumer''. Rulăm acest executabil pentru a urmări folosirea cu succes a buffer-ului circular sincronizat: |
+ | <code> | ||
+ | ./producer_consumer | ||
</code> | </code> | ||
- | Observăm că dureaza semnificativ mai mult rularea executabilului cu granularitate fină. Acest lucru se întâmplă pentru că regiunea critică pe care acesta o protejează este mică, iar cea mai mare parte din timp o va consuma în operațiile cu mutex-ul (//lock contention//). Observăm acest lucru și din numărul mare de schimbări de context (voluntare sau nevoluntare). | ||
- | |||
- | <note> | ||
- | În general preferăm granularitate fină pentru regiunile critice (adică regiuni critice de mică dimensiune); dar granularitatea fină poate înseamna operații foarte dese pe mutex (//lock contention//). De aceea este recomandat ca folosirea granularității fine să fie echilibrată de un număr redus de thread-uri care să dorească să acceseze la un moment dat regiunea critică, pentru a genera cât mai puțină încărcare (//contention//) pe mutex. | ||
- | </note> |