La distinción principal, como señala en su pregunta, es si el programador alguna vez se adelantará a un hilo. La forma en que un programador piensa en compartir estructuras de datos o en sincronizar entre "hilos" es muy diferente en sistemas preventivos y cooperativos.
En un sistema cooperativo (que va por muchos nombres, de cooperación multi-tarea , nonpreemptive multitarea , las discusiones a nivel de usuario , hilos verdes , y las fibras son cinco de las más comunes en la actualidad) que el programador está garantizado que su código se ejecutará de forma atómica , siempre y cuando No hacen llamadas ni llamadas al sistema yield()
. Esto hace que sea particularmente fácil tratar con estructuras de datos compartidas entre múltiples fibras. A menos que necesite realizar una llamada al sistema como parte de una sección crítica, no es necesario marcar las secciones críticas (con mutex lock
y unlock
llamadas, por ejemplo). Entonces en código como:
x = x + y
y = 2 * x
el programador no necesita preocuparse de que alguna otra fibra pueda estar trabajando con las variables x
y y
al mismo tiempo. x
y y
se actualizarán juntos atómicamente desde la perspectiva de todas las otras fibras. Del mismo modo, todas las fibras podrían compartir una estructura más complicada, como un árbol y una llamada similar tree.insert(key, value)
no necesitaría estar protegida por ningún mutex o sección crítica.
Por el contrario, en un sistema de subprocesamiento múltiple preventivo, como con subprocesos verdaderamente paralelos / multinúcleo, todas las posibles intercalaciones de instrucciones entre subprocesos son posibles a menos que haya secciones críticas explícitas. Una interrupción y una preferencia podrían convertirse entre dos instrucciones. En el ejemplo anterior:
thread 0 thread 1
< thread 1 could read or modify x or y at this point
read x
< thread 1 could read or modify x or y at this point
read y
< thread 1 could read or modify x or y at this point
add x and y
< thread 1 could read or modify x or y at this point
write the result back into x
< thread 1 could read or modify x or y at this point
read x
< thread 1 could read or modify x or y at this point
multiply by 2
< thread 1 could read or modify x or y at this point
write the result back into y
< thread 1 could read or modify x or y at this point
Entonces, para ser correcto en un sistema preventivo, o en un sistema con hilos verdaderamente paralelos, debe rodear cada sección crítica con algún tipo de sincronización, como un mutex lock
al principio y un mutex unlock
al final.
Por lo tanto, las fibras son más similares a las bibliotecas de E / S asíncronas que a los hilos preventivos o hilos verdaderamente paralelos. Se invoca el planificador de fibra y puede cambiar fibras durante operaciones de E / S de latencia larga. Esto puede brindar el beneficio de múltiples operaciones simultáneas de E / S sin requerir operaciones de sincronización alrededor de secciones críticas. Por lo tanto, el uso de fibras puede, quizás, tener menos complejidad de programación que los hilos preventivos o verdaderamente paralelos, pero la falta de sincronización alrededor de las secciones críticas conduciría a resultados desastrosos si intentara ejecutar las fibras de manera realmente simultánea o preventiva.
La respuesta es en realidad que podrían, pero hay un deseo de no hacerlo.
Las fibras se usan porque le permiten controlar cómo se produce la programación. En consecuencia, es mucho más simple diseñar algunos algoritmos usando fibras porque el programador ha dicho en qué fibra se está ejecutando en cualquier momento. Sin embargo, si desea que dos fibras se ejecuten en dos núcleos diferentes al mismo tiempo, debe programarlas manualmente para que lo hagan.
Los subprocesos dan el control de qué código se está ejecutando en el sistema operativo. A cambio, el sistema operativo se encarga de muchas tareas feas para usted. Algunos algoritmos se vuelven más difíciles, porque el programador tiene menos voz en qué código se ejecuta en un momento dado, por lo que pueden surgir más casos inesperados. Se agregan herramientas como mutexes y semáforos a un sistema operativo para dar al programador el control suficiente para hacer que los hilos sean útiles y vencer parte de la incertidumbre, sin atascar al programador.
Esto lleva a algo que es aún más importante que cooperativo frente a preventivo: las fibras son controladas por el programador, mientras que los hilos son controlados por el sistema operativo.
No querrás tener que generar una fibra en otro procesador. Los comandos de nivel de ensamblaje para hacerlo son atrozmente complicados y, a menudo, son específicos del procesador. No desea tener que escribir 15 versiones diferentes de su código para manejar estos procesadores, por lo que recurre al sistema operativo. El trabajo del sistema operativo es abstraer estas diferencias. El resultado es "hilos".
Las fibras se ejecutan sobre hilos. No corren solos. En consecuencia, si desea ejecutar dos fibras en diferentes núcleos, simplemente puede generar dos hilos y ejecutar una fibra en cada uno de ellos. En muchas implementaciones de fibras, puede hacer esto fácilmente. El soporte multinúcleo no proviene de las fibras, sino de los hilos.
Se vuelve fácil mostrar que, a menos que desee escribir su propio código específico de procesador, no hay nada que pueda hacer al asignar fibras a múltiples núcleos que no podría hacer al crear hilos y asignar fibras a cada uno. Una de mis reglas favoritas para el diseño de API es "Una API no se realiza cuando ha terminado de agregarle todo, sino más bien cuando ya no puede encontrar nada más que eliminar". Dado que el multinúcleo se maneja perfectamente al alojar fibras en subprocesos, no hay razón para complicar la API de fibra al agregar multinúcleo a ese nivel.
fuente