Differences

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

Link to this comparison view

sde:laboratoare:08_ro_python [2020/04/08 13:06]
ioana_maria.culic [Exercițiul 6]
— (current)
Line 1: Line 1:
-====== Laborator 8 - Thread-uri Linux ====== 
-===== Materiale ajutătoare ===== 
  
-  *[[http://​elf.cs.pub.ro/​so/​res/​laboratoare/​lab08-slides.pdf | lab08-slides.pdf]] 
-  *[[http://​elf.cs.pub.ro/​so/​res/​laboratoare/​lab08-refcard.pdf | lab08-refcard.pdf]] 
- 
-==== Nice to read ==== 
- 
-  * TLPI - Chapter 29, Threads: Introduction 
-  * TLPI - Chapter 30, Threads: Thread Synchronization 
-  * TLPI - Chapter 31, Threads: Thread Safety and Per-Thread Storage 
-===== Prezentare teoretică ===== 
- 
-În laboratoarele anterioare a fost prezentat conceptul de **proces**, acesta fiind unitatea elementară de alocare a resurselor utilizatorilor. În cadrul acestui laborator este prezentat conceptul de **fir de execuție** (sau **thread**),​ acesta fiind unitatea elementară de planificare într-un sistem. Ca și procesele, firele de execuție reprezintă un mecanism prin care un calculator poate sǎ ruleze mai multe task-uri simultan. 
- 
-Un fir de execuție există în cadrul unui proces, și reprezintă o unitate de execuție mai fină decât acesta. În momentul în care un proces este creat, în cadrul lui există un singur fir de execuție, care execută programul secvențial. Acest fir poate la rândul lui sǎ creeze alte fire de execuție; aceste fire vor rula porțiuni ale binarului asociat cu procesul curent, posibil aceleași cu firul inițial (care le-a creat). 
-==== Diferențe dintre fire de execuție și procese ==== 
- 
-  *procesele nu partajează resurse între ele (decât dacă programatorul folosește un mecanism special pentru asta - shared memory spre exemplu), pe când firele de execuție partajează în mod implicit majoritatea resurselor unui proces. Modificarea unei astfel de resurse dintr-un fir este vizibilă instantaneu și din celelalte fire: 
-    *segmentele de memorie precum ''​.heap'',​ ''​.data''​ și ''​.bss''​ (deci și variabilele stocate în ele) 
-    *descriptorii de fișiere (așadar, închiderea unui fișier este vizibilă imediat pentru toate firele de execuție), indiferent de tipul fișierului:​ 
-      *sockeți 
-      *fișiere normale 
-      *pipe-uri 
-      *fișiere ce reprezintă dispozitive hardware (de ex. ''/​dev/​sda1''​). 
-  *fiecare fir are un context de execuție propriu, format din: 
-    *stivă 
-    *set de registre (deci și un contor de program - registrul ''​(E)IP''​) 
- 
-Procesele sunt folosite de SO pentru a grupa și aloca resurse, iar firele de execuție pentru a planifica execuția de cod care accesează (în mod partajat) aceste resurse. 
-==== Avantajele firelor de execuție ==== 
- 
-Deoarece toate firele de execuție ale unui proces folosesc spațiul de adrese al procesului de care aparțin, folosirea lor are o serie de avantaje: 
-  *crearea/​distrugerea unui fir de execuție durează mai puțin decât crearea/​distrugerea unui proces 
-  *durata context switch-ului între firele de execuție aceluiași proces este foarte mică, întrucât nu e necesar să se "​comute"​ și spațiul de adrese (pentru mai multe informații,​ căutați „TLB flush”) 
-  *comunicarea între firele de execuție are un overhead mai mic (realizată prin modificarea unor zone de memorie din spațiul comun de adrese) 
- 
-Firele de execuție se pot dovedi utile în multe situații, de exemplu, pentru a îmbunătăți timpul de răspuns al aplicațiilor cu interfețe grafice (GUI), unde prelucrările CPU-intensive se fac de obicei într-un fir de execuție diferit de cel care afișează interfața. ​ 
- 
-De asemenea, ele simplifică structura unui program și conduc la utilizarea unui număr mai mic de resurse (pentru că nu mai este nevoie de diversele forme de IPC pentru a comunica). 
-==== Tipuri de fire de execuție ==== 
- 
-Din punctul de vedere al implementării,​ există 3 categorii de fire de execuție: 
-  *Kernel Level Threads (KLT) 
-  *User Level Threads (ULT) 
-  *Fire de execuție hibride 
- 
-<spoiler Detalii despre categoriile de fire de execuţie>​ 
- 
-** Kernel Level Threads ** 
- 
-Managementul și planificarea firelor de execuție sunt realizate în kernel; programele creează/​distrug fire de execuție prin apeluri de sistem. Kernel-ul menține informații de context, atât pentru procese, cât și pentru firele de execuție din cadrul proceselor, iar planificarea execuției se face la nivel de fir. 
- 
-__Avantaje__ : 
-  *dacă avem mai multe procesoare putem lansa în execuție simultană mai multe fire de execuție ale aceluiași proces; 
-  *blocarea unui fir nu înseamnă blocarea întregului proces; 
-  *putem scrie cod în kernel care să se bazeze pe fire de execuție. 
- 
-__Dezavantaje__ : 
-  *comutarea contextului este efectuată de kernel (cu o viteză de comutare mai mică): 
-    *se trece dintr-un fir de execuție în kernel 
-    *kernelul întoarce controlul unui alt fir de execuție. 
- 
-** User Level Threads ** 
- 
-Kernel-ul nu este conștient de existența firelor de execuție, iar managementul acestora este realizat de procesul în care ele există (implementarea managementului firelor de execuție este realizată de obicei în biblioteci). Schimbarea contextului nu necesită intervenția kernel-ului,​ iar algoritmul de planificare depinde de aplicație. 
- 
-__Avantaje__ : 
-  *schimbarea de context nu implică kernelul ⇒ comutare rapidă 
-  *planificarea poate fi aleasă de aplicație; aplicația poate folosi acea planificare care favorizează creșterea performanțelor ​ 
-  *firele de execuție pot rula pe orice SO, inclusiv pe SO-uri care nu suportă fire de execuție la nivel kernel (au nevoie doar de biblioteca care implementează firele de execuție la nivel utilizator). 
- 
-__Dezavantaje__ : 
-  *kernel-ul nu știe de fire de execuție ⇒ dacă un fir de execuție face un apel blocant toate firele de execuție planificate de aplicație vor fi blocate. Acest lucru poate fi un impediment întrucât majoritatea apelurilor de sistem sunt blocante. O soluție este utilizarea unor variante non-blocante pentru apelurile de sistem. 
-  *nu se pot utiliza la maximum resursele hardware: kernelul planifică firele de execuție de care știe, câte unul pe fiecare procesor. Kernelul nu este conștient de existența firelor de execuție user-level ⇒ el va vedea un singur fir de execuție ⇒ va planifica procesul respectiv pe maximum un procesor, chiar dacă aplicația ar avea mai multe fire de execuție planificabile în același timp. 
- 
-** Fire de execuție hibride ** 
- 
-Aceste fire încearcă să combine avantajele firelor de execuție user-level cu cele ale firelor de execuție kernel-level. O modalitate de a face acest lucru este de a utiliza fire kernel-level pe care să fie multiplexate fire user-level. KLT sunt unitățile elementare care pot fi distribuite pe procesoare. De regulă, crearea firelor de execuție se face în user space și tot aici se face aproape toată planificarea și sincronizarea. Kernel-ul știe doar de KLT-urile pe care sunt multiplexate ULT, și doar pe acestea le planifică. Programatorul poate schimba eventual numărul de KLT alocate unui proces. 
- 
-</​spoiler>​ 
-===== Modulul threading ===== 
- 
-În ceea ce privește firele de execuție, POSIX nu specifică dacă acestea trebuie implementate în user-space sau kernel-space. Linux le implementează în kernel-space,​ dar nu diferențiază firele de execuție de procese decât prin faptul că firele de execuție partajează spațiul de adresă (atât firele de execuție, cât și procesele, sunt un caz particular de "​task"​). Pentru folosirea firelor de execuție în Python trebuie să includem modulul [[https://​docs.python.org/​3/​library/​threading.html|threading]]. 
-==== Crearea firelor de execuție ==== 
- 
-Modulul ''​threading''​ expune clasa ''​Thread''​. Astfel, un fir de execuție este creat prin instantierea clasei: 
- 
-<code python> 
-import threading 
-threading.Thread(group=None,​ target=None,​ name=None, args=(), kwargs={}, *, daemon=None) 
-</​code>​ 
-  * group - se foloseste la extinderea clasei Thread; se recomanda pastrarea valoarei ''​None'';​ 
-  * target - functia care va fi apelata la rularea threadului; 
-  * name - numele threadului; daca acesta nu este specificat, se va genera un nume de forma''​Thread-N'',​ unde e un numar; 
-  * args - lista de parametrii care va fi pasata functiei ''​target'';​ parametrii vor fi pasati in ordinea in care functia ii primeste; 
-  * kwargs - dictionar care va stoca parametrii pasati functiei ''​target'',​ sub forma numele parametru-valoare;​ 
-  * daemon - specifică dacă threadul este de tip daemon; dacă valoarea este None, proprietatea va fi moștenită de la threadul curent. 
- 
-Noul fir creat poate fi lansat prin apelarea functiei [[https://​docs.python.org/​3/​library/​threading.html#​threading.Thread.start|start()]]. Acesta va executa codul specificat de funcția ''​target''​ căreia i se vor pasa argumentele din ''​args''​ sau ''​kwargs''​. 
- 
-Pentru a determina firului de execuție curent se poate folosi funcția [[https://​docs.python.org/​3/​library/​threading.html#​threading.current_thread|current_thread]]:​ 
- 
-<code python> 
-import threading 
-threading.current_thread() 
-</​code>​ 
- 
-==== Așteptarea firelor de execuție ==== 
- 
-Firele de execuție se așteaptă folosind funcția [[https://​docs.python.org/​3/​library/​threading.html#​threading.Thread.join|join]]:​ 
- 
-<code python> 
-t = threading.Thread() 
-t.join(timeout=None) 
-</​code>​ 
- 
-Odata ce un thread a facut join pe un altul, acesta se va bloca pana threadul pe care s-a facut join isi va incheia executia. 
-Daca threadul pe care s-a facut join va arunca o exceptie de tipul [[https://​docs.python.org/​3/​library/​threading.html#​threading.excepthook|excepthook]] va fi aruncata in threadul care a facut join. 
- 
-<note info> 
-Este recomandat ca programul principal să apeleze întotdeauna funcția ''​join''​ pentru threadurile pe care le generează. 
-</​note>​ 
- 
-==== Fire de execuție de tip daemon ==== 
-Un thread de tip daemon are drept scop procesarea unor operații fară a impacta firul de execuție principal. 
- 
-Proprietatea principală a acestor threaduri este că programul principal își va încheia execuția dacă rămân doar threaduri de acest tip.  
- 
-<note warning> 
-Deși este o operație legală, se recomandă să nu folosiți funcția ''​join''​ pe un thread de tip daemon. 
-</​note>​ 
-==== Terminarea firelor de execuție ==== 
- 
-Un fir de execuție își încheie execuția în mod automat, la sfârșitul codului firului de execuție. 
- 
-==== Interacțiunea cu firele de execuție ==== 
- 
-Pentru a schimba informații între programul principal si alte threaduri, vom folosi modulul [[https://​docs.python.org/​3/​library/​queue.html|queue]]. Folosind acest modul, putem implementa o coadă în care vom plasa mesajele din partea firelor de execuție. 
- 
-Modululul suportă crearea a 6 tipuri diferite de cozi: 
-  * Queue - coadă de tip FIFO, primul element inserat în coadă va fi primul extras; 
-  * LifoQueue - ultimul element inserat în coadă va fi primul extras; 
-  * PriorityQueue - Fiecare element are o prioritate, pe baza căreia se alege care element va fi cel extras; 
-  * SimpleQueue - coadă de tip FIFO cu funcționalități limitate; 
-  * Empty - se aruncă o excepție dacă se încearcă extragerea unui element când coada e goală; 
-  * Full - se aruncă o excepție dacă se încearcă adăugarea unui element când coada e plină. 
- 
-<note info> 
-În continuare vom discuta doar despre primele 3 tipuri de cozi, la restul aplicându-se alte operații. Pentru mai multe detalii despre restul claselor din modulul ''​queue'',​ vă recomandăm citirea documentației. 
-</​note>​ 
- 
-Pentru a crea o coadă, trebuie să instanțiem una din clasele expuse de modulul ''​queue'':​ 
-<code python> 
-import queue 
- 
-fifo_queue = queue.Queue() 
-lifo_queue = queue.LifoQueue() 
-priority_queue = queue.PriorityQueue() 
-</​code>​ 
- 
-Funcțiile [[https://​docs.python.org/​3/​library/​queue.html#​queue.Queue.put|put]] și [[https://​docs.python.org/​3/​library/​queue.html#​queue.Queue.put_nowait|put_nowait]] adaugă elemente în coadă. 
-<code python> 
-q.put (item, block=True, timeout=None) 
-q.put_nowait (item) 
-</​code>​ 
- 
-Funcțiile [[https://​docs.python.org/​3/​library/​queue.html#​queue.Queue.get|get]] și [[https://​docs.python.org/​3/​library/​queue.html#​queue.Queue.get_nowait|get_nowait]] extrag elemente în coadă. 
-<code python> 
-q.get (block=True,​ timeout=None) 
-q.get_nowait () 
-</​code>​ 
- 
-Dacă coada este goală la apelarea uneia din cele două funcții, o excepție de tipul [[https://​docs.python.org/​3/​library/​queue.html#​queue.Empty|Empty]] este generată. 
- 
-<spoiler Detalii despre așteptarea cozilor> 
- 
-** Funcția join ** 
- 
-Similar cu așteptarea firelor de execuție, putem să apelăm funcția [[https://​docs.python.org/​3/​library/​queue.html#​queue.Queue.join|join]] pe o coadă, pentru a opri execuția programului până toate elementele din coadă au fost procesate. 
- 
-Funcția ''​join''​ trebuie apelată în paralel cu funcția [[https://​docs.python.org/​3/​library/​queue.html#​queue.Queue.task_done| task_done]]. Fiecare thread va apela această funcție pentru a semnala terminarea procesării elementului extras din coadă. În momentul în care numărul de apeluri ''​task_done''​ este egal cu numărul de elemente care a fost adăugat în coadă, programul care a apelat ''​join''​ își va continua execuția. 
- 
-</​spoiler>​ 
- 
-=== Exemplu === 
-Mai jos vom crea un program care generează un thread ce primește valori de la programul principal și răspunde la aceastea. 
- 
-<code python> 
-import queue 
-import threading 
- 
-def worker (): 
-    while True: 
-        item = q1.get() 
-        if item == '​Hello':​ 
-            print (item) 
-            q2.put ('​SDE'​) 
-        elif item == '​Good':​ 
-            print (item) 
-            q2.put ('​bye'​) 
-        else: 
-            break 
- 
-q1 = queue.Queue() 
-q2 = queue.Queue() 
- 
-t = threading.Thread (target=worker) 
-t.start () 
- 
-q1.put ('​Hello'​) 
-print (q2.get ()) 
- 
-q1.put ('​Good'​) 
-print (q2.get ()) 
- 
-q1.put ('​exit'​) 
- 
-t.join() 
-</​code>​ 
- 
-==== Thread Specific Data (TSD) ==== 
- 
-Uneori este util ca o variabilă să fie specifică unui fir de execuție (invizibilă pentru celelalte fire). Linux permite memorarea de perechi (cheie, valoare) într-o zonă special desemnată din stiva fiecărui fir de execuție al procesului curent. Cheia are același rol pe care îl are numele unei variabile: desemnează locația de memorie la care se află valoarea. 
- 
-Fiecare fir de execuție va avea propria copie a unei "​variabile"​ corespunzătoare unei chei ''​k'',​ pe care o poate modifica, fără ca acest lucru să fie observat de celelalte fire, sau să necesite sincronizare. De aceea, TSD este folosită uneori pentru a optimiza operațiile care necesită multă sincronizare între fire de execuție: fiecare fir calculează informația specifică, și există un singur pas de sincronizare la sfârșit, necesar pentru reunirea rezultatelor tuturor firelor de execuție. 
- 
-=== Crearea unei variabile === 
- 
-Pentru a crea o variabila specifică threadului, se folosește clasa [[https://​docs.python.org/​3/​library/​threading.html#​threading.local|local]] a modulului ''​threading''​. 
- 
-<code python> 
-import threading 
- 
-local_data = threading.local() 
-local_data.my_var = '​SDE'​ 
-</​code>​ 
- 
-====== Exerciţii de laborator ====== 
- 
-===== Exercițiul 1 ===== 
- 
-a. Creați un program care pornește 5 fire de execuție. Fiecare fir de execuție afișeaƶă numerele de la 0 la 5. 
- 
-b. Afișați identificatorul fiecărui fir de execuție (hint: [[https://​docs.python.org/​3/​library/​threading.html#​threading.Thread.ident|ident]]). 
- 
- 
- 
-===== Exercițiul 2 ===== 
- 
-Creați un program care primește 3 parametrii din linia de comandă și pornește 3 fire de execuție. Fiecare fir de execuție va primi ca parametru de la programul principal unul din cele 3 numere si va afișa pe ecran ''​par''​ sau ''​impar'',​ în funcție de paritatea numărului. 
- 
-===== Exercițiul 3 ===== 
- 
-În fișierul din directorul 3-numbers, porniți un thread care afișează numerele de la 0 la 5 la interval de o secundă. 
-<note warning> 
-Nu folosiți funcția ''​join''​. 
-</​note>​ 
- 
-Observați când procesul își termină execuția. 
-Modificați programul principal pentru a își înceta execuția imediat după ce afișează cele două linii 
-<code python> 
-main 1 
-main 2 
-</​code>​ 
- 
-===== Exercițiul 4 ===== 
-În fișierul din directorul 4-alive, porniți câte un thread pentru fiecare funcție worker definită si așteptați doar după threadurile inca in executie (hint: [[https://​docs.python.org/​3/​library/​threading.html#​threading.Thread.is_alive|is_alive]]). 
- 
-===== Exercițiul 5 ===== 
-Creați două threaduri, unul care să genereze numerele prime de la 0 la 50 și unul care să genereze numerele patrate perfecte de la 0 la 50. Programul principal va afișa numerele generate de cele două threaduri, dupa care isi va incheia executia. 
- 
-===== Exercițiul 6 ===== 
-Creați un program care primește comenzi bash de la tastatură și le execută intr-un thread. 
- 
-===== Exercițiul 7 ===== 
-Folosiți 3 threaduri pentru a găsi maximul dintr-un șir. Fiecare thread primește o bucată din acel șir printr-o coadă și pune intr-o altă coadă maximul. (hint: [[https://​docs.python.org/​3/​library/​queue.html#​queue.Queue.join| queue.join]]) 
sde/laboratoare/08_ro_python.1586340379.txt.gz · Last modified: 2020/04/08 13:06 by ioana_maria.culic
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