Unidad corutina vs hilos

13

¿Cuál es la diferencia entre una corutina y un hilo? ¿Hay alguna ventaja de usar uno sobre el otro?

StrengthPlusPlus
fuente
3
Corutinas trabajando en un hilo principal.
Woltus
2
No puede usar ninguna función de Unity en un hilo. Las corutinas pueden.
Hellium
1
@Hellium Correction, no puede usar ninguna función de Unity en un hilo diferente, es decir, las funciones de Unity se pueden usar en el hilo principal .
Pharap

Respuestas:

24

Si bien las Coroutinas parecen funcionar como hilos a primera vista, en realidad no están utilizando ningún subprocesamiento múltiple. Se ejecutan secuencialmente hasta que ellos yield. El motor verificará todas las corrutinas producidas como parte de su propio bucle principal (en qué punto depende exactamente del tipo de yield, verifique este diagrama para obtener más información ), las continuará una tras otra hasta la siguiente yieldy luego continuará con el bucle principal.

Esta técnica tiene la ventaja de que puede usar corutinas sin los dolores de cabeza causados ​​por problemas con subprocesos múltiples reales. No obtendrá puntos muertos, condiciones de carrera o problemas de rendimiento causados ​​por cambios de contexto, podrá depurar correctamente y no necesitará utilizar contenedores de datos seguros para subprocesos. Esto se debe a que cuando se ejecuta una rutina, el motor de Unity está en un estado controlado. Es seguro usar la mayoría de las funciones de Unity.

Con los hilos, por otro lado, no tienes absolutamente ningún conocimiento sobre en qué estado se encuentra el bucle principal de Unity en este momento (de hecho, es posible que ya no se esté ejecutando). Por lo tanto, su hilo podría causar muchos estragos al hacer algo a la vez que se supone que no debe hacer eso. No toque ninguna funcionalidad nativa de Unity desde un subproceso . Si necesita comunicarse entre un subproceso y su subproceso principal, haga que el subproceso escriba en algún objeto contenedor seguro para subprocesos (!) Y haga que un MonoBehaviour lea esa información durante las funciones habituales de eventos de Unity.

La desventaja de no realizar subprocesos múltiples "reales" es que no se pueden usar corutinas para paralelizar cálculos intensivos de CPU en múltiples núcleos de CPU. Sin embargo, puede usarlos para dividir un cálculo en varias actualizaciones. Entonces, en lugar de congelar tu juego por un segundo, solo obtienes una tasa de cuadros promedio más baja durante varios segundos. Pero en ese caso, usted es responsable de yieldsu rutina siempre que quiera permitir que Unity ejecute una actualización.

Conclusión:

  • Si desea utilizar la ejecución asincrónica para expresar la lógica del juego, use corutinas.
  • Si desea utilizar la ejecución asincrónica para utilizar múltiples núcleos de CPU, use hilos.
Philipp
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
MichaelHouse
10

Las rutinas son lo que en informática llamamos "multitarea cooperativa". Son una forma de que múltiples flujos de ejecución diferentes se intercalen entre sí de forma cooperativa. En la multitarea cooperativa, un flujo de ejecución tiene la propiedad exclusiva e indiscutible de la CPU hasta que alcanza a yield. En ese momento, Unity (o cualquier marco que esté utilizando) tiene la opción de cambiar a un flujo de ejecución diferente. A continuación, también obtiene la propiedad exclusiva e indiscutible de la CPU hasta que se yieldejecute.

Los hilos son lo que llamamos "multitarea preventiva". Cuando está utilizando hilos, el marco se reserva el derecho en cualquier momento de detener el hilo a medio pensamiento y cambiar a otro hilo. No importa donde estés. ¡Incluso puede detenerse a mitad de camino al escribir una variable en la memoria en algunos casos!

Hay pros y contras para cada uno. Los contras de las corutinas son probablemente los más fáciles de entender. En primer lugar, todas las rutinas se ejecutan en un solo núcleo. Si tiene una CPU de cuatro núcleos, las corutinas solo usarán uno de los cuatro núcleos. Esto simplifica las cosas, pero puede ser un problema de rendimiento en algunos casos. La segunda desventaja es que debe tener en cuenta que cualquier rutina puede detener todo su programa simplemente negándose a hacerlo yield. Este fue un problema en Mac OS9, hace muchos años. OS9 solo admite multitarea cooperativa en toda la computadora. Si uno de sus programas se bloqueó, podría detener la computadora tan ferozmente que el sistema operativo ni siquiera podría mostrar el texto del mensaje de error para hacerle saber lo que sucedió.

Las ventajas de las corutinas son que son relativamente fáciles de entender. Los errores que tiene son mucho más predecibles. Por lo general, también requieren menos recursos, lo que puede ser útil a medida que asciendes en los 10 de miles de corutinas o hilos. Candid Moon mencionó en los comentarios que, si no ha estudiado los hilos correctamente, simplemente manténgase en las rutinas, y tienen razón. Las corutinas son mucho más simples para trabajar.

Los hilos son una bestia completamente diferente. Siempre debes estar en guardia contra la posibilidad de que otro hilo pueda interrumpirte en cualquier momentoy meterte con tus datos. Las bibliotecas de subprocesos proporcionan conjuntos completos de herramientas poderosas para ayudarlo con esto, como mutexes y variables de condición que lo ayudan a decirle al sistema operativo cuándo es seguro que ejecute uno de sus otros subprocesos y cuándo no es seguro. Hay cursos completos dedicados a cómo usar bien estas herramientas. Uno de los problemas famosos que surge es un "punto muerto", que es cuando dos hilos se "atascan" esperando que el otro libere algunos recursos. Otro problema, que es muy importante para Unity, es que muchas bibliotecas (como Unity) no están diseñadas para admitir llamadas de múltiples subprocesos. Puede romper fácilmente su marco si no presta atención a qué llamadas están permitidas y cuáles están prohibidas.

La razón de esta complejidad adicional es realmente muy simple. El modelo multitarea preventivo es realmente similar al modelo de subprocesamiento múltiple que le permite no solo interrumpir otros subprocesos, sino también ejecutar subprocesos uno al lado del otro en diferentes núcleos. Esto es increíblemente poderoso, ya que es la única forma de aprovechar realmente estas nuevas CPU de cuatro núcleos y códigos hexadecimales que están saliendo, pero abre la caja de pandoras. Las reglas de sincronización sobre cómo administrar estos datos en un mundo de subprocesos múltiples son positivamente brutales. En el mundo de C ++, hay artículos completos dedicados a los MEMORY_ORDER_CONSUMEcuales es un rincón de sincronización de subprocesos múltiples.

Entonces, ¿los contras de enhebrar? Simple: son difíciles. Puedes encontrar clases enteras de errores que nunca has visto antes. Muchos son los llamados "heisenbugs" que a veces aparecen y luego desaparecen cuando los depura. Las herramientas que se le dan para lidiar con estos son muy poderosas, pero también son de muy bajo nivel. Están diseñados para ser eficientes en las arquitecturas de los chips modernos en lugar de estar diseñados para ser fáciles de usar.

Sin embargo, si desea utilizar toda la potencia de su CPU, son la herramienta que necesita. Además, en realidad hay algoritmos que son más fáciles de entender en subprocesos múltiples que en las rutinas simplemente porque dejas que el sistema operativo maneje todas las preguntas sobre dónde pueden tener lugar las interrupciones.

El comentario de Candid Moon de apegarse a las corutinas también es mi recomendación. Si quieres el poder de los hilos, entonces comprométete a ello. Sal y realmente aprende hilos, formalmente. Hemos tenido varias décadas para descubrir cómo organizar la mejor manera de pensar sobre los hilos para que pueda obtener resultados seguros y repetibles de manera temprana y agregar rendimiento a medida que avanza. Por ejemplo, todos los cursos sanos enseñarán mutexes antes de enseñar variables de condición. Todos los cursos sanos que cubren atómicos enseñarán completamente mutexes y variables de condición antes de mencionar que existen atómicos. (Nota: no existe un tutorial sensato sobre atómica). Intenta aprender a enhebrar poco a poco y estás pidiendo una migraña.

Cort Ammon
fuente
Los subprocesos agregan complejidad principalmente en los casos en que las operaciones y las dependencias están intercaladas. Si el bucle principal del juego contiene tres funciones, x (), y () y z (), y ninguna de ellas afecta nada que otro necesite, comenzar las tres simultáneamente pero luego esperar a que todas se completen antes de continuar. Permitir que uno reciba algún beneficio de una máquina de múltiples núcleos sin tener que agregar demasiada complejidad. Si bien las variables de condición generalmente se usan junto con mutexes, el paradigma de pasos paralelos independientes realmente no necesita mutexes como tal ...
supercat
... pero simplemente una forma para que los hilos que manejan y () y z () esperen hasta que se activen, y luego una forma para que el hilo principal espere, después de ejecutar x (), hasta y () y z () Han completado.
supercat
@supercat Siempre debe tener alguna sincronización para realizar la transición entre fases paralelas independientes y fases secuenciales. A veces eso no es más que un join(), pero necesitas algo. Si no tiene un arquitecto que diseñó un sistema para realizar la sincronización por usted, debe escribirlo usted mismo. Como alguien que hace cosas multiproceso, creo que los modelos mentales de las personas sobre el funcionamiento de las computadoras deben ajustarse antes de que realicen la sincronización de manera segura (que es lo que enseñarán los buenos cursos)
Cort Ammon
Por supuesto, debe haber alguna sincronización, y lograr la máxima eficiencia generalmente requerirá algo más que eso join. Mi punto era que perseguir una fracción moderada de los posibles beneficios de rendimiento del enhebrado, fácilmente, a veces puede ser mejor que usar un enfoque de enhebrado más complicado para cosechar una fracción más grande.
supercat
Acerca de los nuevos tipos de errores: tenga en cuenta también que si no puede probar lógicamente que el código es seguro, es imposible saber qué errores habrá en la puerta. Aunque el orden de ejecución puede ser indefinido, a menudo toma rutas similares cada vez que lo ejecuta en 1 máquina. A menudo encontrará que falla con frecuencia y, a menudo, en una fracción de las máquinas, y no puede probar en todas las máquinas posibles. En el trabajo teníamos un error que solo se manifestaba en 1 de nuestras máquinas, solo si el registro estaba desactivado; A muchas personas les llevó mucho tiempo localizarlo. No quiero eso en la naturaleza. No solo pruebe la sincronización, pruébelo lógicamente.
Aaron
2

En los términos más simples posibles ...


Hilos

Un subproceso no decide cuándo cede, el sistema operativo (el 'SO', por ejemplo, Windows) decide cuándo se entrega un subproceso. El sistema operativo es casi completamente responsable de la programación de subprocesos, decide qué subprocesos ejecutar, cuándo ejecutarlos y durante cuánto tiempo.

Además, un subproceso puede ejecutarse sincrónicamente (un subproceso tras otro) o asincrónicamente (diferentes subprocesos que se ejecutan en diferentes núcleos de CPU). La capacidad de ejecutarse de forma asincrónica significa que los subprocesos pueden realizar más trabajo en la misma cantidad de tiempo (porque los subprocesos literalmente están haciendo dos cosas al mismo tiempo). Incluso los subprocesos sincrónicos realizan mucho trabajo si el sistema operativo es bueno para programarlos.

Sin embargo, este poder de procesamiento adicional viene con efectos secundarios. Por ejemplo, si dos subprocesos intentan acceder al mismo recurso (por ejemplo, una lista) y cada subproceso puede detenerse aleatoriamente en cualquier punto del código, las modificaciones del segundo subproceso podrían interferir con las modificaciones realizadas por el primer subproceso. (Ver también: Condiciones de carrera y punto muerto ).

Los subprocesos también se consideran 'pesados' porque tienen muchos gastos generales, lo que significa que se incurre en una penalización de tiempo considerable al cambiar los subprocesos.


Corutinas

A diferencia de los subprocesos, las corutinas son completamente sincrónicas, solo se puede ejecutar una en cualquier momento. Además, las corutinas pueden elegir cuándo ceder y, por lo tanto, pueden optar por ceder en un punto del código que sea conveniente (por ejemplo, al final de un ciclo de bucle). Esto tiene la ventaja de que problemas como las condiciones de carrera y los callejones sin salida son mucho más fáciles de evitar, así como también facilita que las corutinas cooperen entre sí.

Sin embargo, esta es una responsabilidad importante también, si una rutina no rinde correctamente, podría terminar consumiendo mucho tiempo de procesador y aún puede causar errores si modifica incorrectamente los recursos compartidos.

Las corutinas generalmente no requieren cambio de contexto y, por lo tanto, son rápidas de cambiar y son bastante livianas.


En resumen:

Hilo:

  • Síncrono o asíncrono
  • Cedido por el sistema operativo
  • Cedido al azar
  • Pesado

Corutina:

  • Sincrónico
  • Se rinde
  • Rendimientos por elección
  • Ligero

Los roles de los hilos y las corutinas son muy similares, pero difieren en la forma en que hacen el trabajo, lo que significa que cada uno se adapta mejor a las diferentes tareas. Los subprocesos son mejores para tareas en las que pueden concentrarse en hacer algo por su cuenta sin ser interrumpidos, y luego devolver la señal cuando hayan terminado. Las rutinas son las mejores para tareas que se pueden realizar en muchos pasos pequeños y tareas que requieren el procesamiento cooperativo de datos.

Pharap
fuente