Fil d'execució
En informàtica, un fil d'execució (thread en anglès) és la seqüència més petita d'instruccions programades que pot ser gestionada de manera independent per un planificador del sistema operatiu. El fil és un component d'un procés, el qual pot contenir un o més fils que comparteixen els mateixos recursos. Aquesta característica permet al programador dissenyar aplicacions que duguin a terme diferents tasques de forma concurrent (compartint el temps de CPU) o en paral·lel (en sistemes amb múltiples nuclis o processadors).
La tècnica de programació amb fils d'execució s'anomena multifil (multithreading en anglès) i permet simplificar el disseny d'aplicacions concurrents. És una alternativa més eficient a l'ús de múltiples processos, ja que els fils consumeixen menys recursos i la seva creació i destrucció és molt més ràpida. Com que els fils d'un mateix procés comparteixen l'espai de memòria, poden sorgir estats de competició (race conditions en anglès) si dos o més fils intenten modificar les mateixes dades simultàniament. Per evitar-ho, el programador defineix seccions crítiques protegides per mecanismes de sincronització (com els cadenats d'exclusió mútua). La concurrència es pot assolir mitjançant canvis de context ràpids, però en sistemes moderns amb múltiples nuclis, els fils permeten l'execució en paral·lel real, de manera simultània en diferents unitats de processament.

Els canvis de context es produeixen quan el planificador del sistema operatiu interromp l'execució d'un fil per permetre que un altre ocupi el processador. En aquest procés, el fil no és 'eliminat', sinó que el seu estat es guarda per poder reprendre l'execució més endavant. En sistemes d'un sol nucli, aquests canvis ràpids creen la il·lusió de simultaneïtat.
En els sistemes multifil, un mateix procés pot estar format per múltiples fils d'execució que comparteixen recursos comuns com l'espai de memòria, el codi executable, els fitxers oberts i els permisos. No obstant això, cada fil manté el seu propi estat d'execució independent, el qual inclou la seva pròpia pila, un conjunt de registres i el seu propi comptador de programa per determinar quina part del codi compartit està processant en cada moment.
Els fils d'execució, també són coneguts com a processos lleugers. L'origen del nom rau en el fet que els fils d'execució consumeixen menys recursos de sistema que els processos, ja que comparteixen l'espai de memòria i el codi executable del procés pare, evitant la sobrecàrrega de crear unitats totalment aïllades.
La majoria de llenguatges de programació moderns, disposen de llibreries específiques per tal de programar amb fils i altres com C o C++ han d'utilitzar les crides de sistema que donen aquest suport.
Un parell d'exemples típics on s'utilitzen fils són:
- Aplicacions gràfiques: Un fil s'encarrega de la interfície gràfica d'usuari mentrestant un altre efectua les operacions.
- Aplicacions client/servidor: el servidor crea múltiples fils per tal de donar servei a múltiples clients alhora.
En sistemes POSIX hi ha 2 llibreries per a treballar amb fils d'execució:
- Native POSIX Thread Library per a Linux
- POSIX Threads standard
Història
[modifica]Els fils d'execució van aparèixer per primera vegada sota la denominació de "tasques" (tasks en anglès) l'any 1967, en el sistema operatiu de processament per lots OS/360 d'IBM. Aquest sistema permetia als usuaris escollir entre tres configuracions de control, una de les quals era la multiprogramació amb un nombre variable de tasques (MVT). L'encunyació del terme "fil" s'atribueix a Victor A. Vyssotsky l'any 1966.
La implementació de fils en el sistema Mach es va descriure l'estiu de 1986. Poc després, el 1987, el sistema operatiu OS/2 1.0 ja oferia suport per a fils. Pel que fa a Microsoft, la primera versió de Windows a incorporar fils d'execució va ser Windows NT, llançada l'any 1993.
L'any 1995, l'IEEE va definir l'API "pthreads", que va estandarditzar una interfície per a la programació multifil portable en una gran varietat de sistemes operatius de tipus Unix. Des de llavors, l'estàndard "pthreads" també s'ha implementat en sistemes Windows mitjançant paquets de tercers com pthreads-w32, que n'adapten les funcionalitats sobre l'API nativa de Windows.
L'ús de fils en les aplicacions de programari es va generalitzar a principis de la dècada de 2000, coincidint amb el moment en què les CPU van començar a incorporar múltiples nuclis. Les aplicacions que volien aprofitar els avantatges de rendiment d'aquestes noves arquitectures van haver d'implementar la concurrència per poder utilitzar els diversos nuclis de processament de manera efectiva.
Tipus de fils segons la seva implementació
[modifica]Fils de nucli (Kernel-Level Threads)
[modifica]Els fils de nucli són les unitats de planificació gestionades directament pel sistema operatiu. En sistemes multiprocessador, el nucli té la visibilitat total del maquinari per assignar cada fil a un nucli de CPU o fil de maquinari específic, que permet un paral·lelisme real. Tot i ser més "lleugers" que un procés, el seu cost computacional no és negligible.
L'avantatge principal d'aquesta implementació és la planificació preemptiva. El nucli pot interrompre un fil bloquejat (per exemple, per I/O) i assignar la CPU a un altre, evitant que tot el procés s'aturi. A més, els fils de nucli també es defineixen per la seva eficiència en l'ús de la memòria cau. Aquests prescindeixen de la modificació de l'espai d'adreçament virtual, fet que evita la invalidació del TLB (Translation Lookaside Buffer), malgrat que la informació a les jerarquies L1/L2 de la memòria cau pugui perdre localitat temporal després de la transició.
Fils d'Usuari (User-Level Threads)
[modifica]Els fils d'usuari, sovint anomenats "green threads" en entorns de màquines virtuals (com Java o les primeres versions de Python), són invisibles per al sistema operatiu. La gestió, creació i planificació es realitzen íntegrament mitjançant una llibreria en l'espai d'usuari, sense cap intervenció del nucli.
Aquest model és extremadament eficient pel que fa al canvi de context, ja que no requereix canvis de mode privilegiat ni crides al sistema. Tanmateix, des del punt de vista de la computació d'alt rendiment, presenten un defecte fatal: el bloqueig d'I/O. Si un sol fil realitza una crida al sistema síncrona i es bloqueja, el nucli, que només veu un únic procés, bloquejarà tot l'entorn d'execució, aturant la resta de fils d'usuari encara que tinguin feina pendent.
Per superar aquesta limitació, les arquitectures modernes utilitzen les següents estratègies:
- API de bloqueig simulat: Interfícies de programació que encapsulen la latència d'espera de manera transparent per a l'aplicació, gestionant les cues d'execució internament per mantenir la coherència del flux síncron.
- I/O no bloquejant: Primitives de sistema dissenyades per retornar el control al flux d'execució de manera immediata, evitant la suspensió del fil mentre s'espera la disponibilitat del recurs o la finalització de l'operació.
- Paradigmes de programació asíncrona (Async/Await): Abstraccions d'alt nivell que permeten la suspensió i la represa de l'estat d'execució d'una tasca sense bloquejar el flux principal. Aquesta arquitectura habilita el temps d'execució per alliberar els recursos de computació mentre s'espera la resolució d'operacions externes, optimitzant la concurrència i la capacitat de resposta del sistema sense la necessitat de fils de nucli addicionals.
La necessitat d'un control encara més fi de l'execució sense les servituds del sistema operatiu va donar lloc al següent concepte, les fibres.
Fibres
[modifica]Les fibres són la unitat d'execució més lleugera existent, operant sota un model de planificació cooperativa. A diferència dels fils, on el planificador "treu" el control de la CPU, en les fibres és la mateixa fibra la que ha de cedir voluntàriament el control. Aquest model és essencial en sistemes on l'aplicació necessita una gestió de tasques que el planificador del nucli, dissenyat per a propòsit general, no pot optimitzar. Un exemple clar és la recerca en el model OpenMP, que utilitza fibres per gestionar milers de petites tasques amb una sobrecàrrega mínima.
Cal diferenciar les fibres de les corutines, ja que, tot i simplificar la sincronització en eliminar la concurrència no determinista, deleguen la gestió de la latència íntegrament al desenvolupador. Una implementació no adequada, que ometi la cessió del control, pot monopolitzar el recurs de computació, comprometent l'estabilitat de l'aplicació.
Exemple de fils d'execució en Linux
[modifica]Els fils d'execució no són processos en si, sinó que formen part d'un procés. Es pot dir que tots els fils d'execució d'un procés són germans perquè comparteixen el mateix procés. Podem observar aquest fet utilitzant l'ordre ps quan una aplicació que utilitza fils com Firefox s'està executant:
$ ps ax|grep firefox 6952 ? Sl 11:39 /usr/lib/firefox-3.0.4/firefox ...
Com podeu veure només hi ha un procés, però múltiples fils d'execució (que es poden mostrar amb l'opció H):
$ ps axH|grep firefox 6952 ? Sl 11:13 /usr/lib/firefox-3.0.4/firefox 6952 ? Sl 0:02 /usr/lib/firefox-3.0.4/firefox 6952 ? Sl 0:19 /usr/lib/firefox-3.0.4/firefox 6952 ? Sl 0:00 /usr/lib/firefox-3.0.4/firefox 6952 ? Sl 0:00 /usr/lib/firefox-3.0.4/firefox 6952 ? Sl 0:02 /usr/lib/firefox-3.0.4/firefox 24872 pts/6 S+ 0:00 grep firefox
Els fils s'identifiquen per la l de la columna STAT, i com podeu veure formen part tots del mateix procés (amb identificador de procés 6952)