Differences

This shows you the differences between two versions of the page.

Link to this comparison view

so:curs:sync [2020/04/26 15:54]
razvan.deaconescu
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>​
  
Line 101: Line 108:
 Î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. Î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>​ </​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 ==== ==== Consum de timp mutex și spinlock ====
Line 111: Line 607:
  
 Pentru a contabiliza timpul de rulare rulăm cele două executabile prin comanda ''​time'':<​code bash> Pentru a contabiliza timpul de rulare rulăm cele două executabile prin comanda ''​time'':<​code bash>
-/​usr/​bin/​time ./​spinlock +/​usr/​bin/​time ​-v ./​spinlock 
-/​usr/​bin/​time ./mutex+/​usr/​bin/​time ​-v ./mutex
 </​code>​ </​code>​
  
Line 185: 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.
 +
 +==== Buffer circular ====
 +
 +Î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.
 +
 +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''​.
 +
 +Î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.
 +
 +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>​
  
so/curs/sync.1587905666.txt.gz · Last modified: 2020/04/26 15:54 by razvan.deaconescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0