waals van tipo stacking son que interacciones helice fuerzas estabilizan doble dna apilamientos adn c++ concurrency coroutine boost-coroutine

c++ - van - que fuerzas estabilizan la doble helice del adn



¿En qué se diferencian las corutinas apiladas de las corutinas apiladas? (2)

Lo que desea son hilos / fibras de aterrizaje de usuario; por lo general, desea suspender su código (que se ejecuta en fibra) en una pila de llamadas anidadas profundas (por ejemplo, análisis de mensajes desde la conexión TCP). En este caso, no puede utilizar el cambio de contexto sin pila (la pila de la aplicación se comparte entre las rutinas sin pila -> se sobrescribirán los marcos de pila de las subrutinas llamadas).

Puede usar algo como boost.fiber que implementa hilos / fibras de tierra de usuario basados ​​en boost.context.

Antecedentes:

Lo pregunto porque actualmente tengo una aplicación con muchos (cientos a miles) de hilos. La mayoría de esos hilos están inactivos una gran parte del tiempo, esperando que los elementos de trabajo se coloquen en una cola. Cuando un elemento de trabajo está disponible, se procesa llamando a un código existente arbitrariamente complejo. En algunas configuraciones del sistema operativo, la aplicación se encuentra con los parámetros del núcleo que rigen la cantidad máxima de procesos de usuario, por lo que me gustaría experimentar con medios para reducir la cantidad de subprocesos de trabajo.

Mi solución propuesta:

Parece que un enfoque basado en la rutina, donde reemplace cada hilo de trabajo con una rutina, ayudaría a lograr esto. Entonces puedo tener una cola de trabajo respaldada por un grupo de subprocesos de trabajo reales (kernel). Cuando un elemento se coloca en la cola de una rutina particular para su procesamiento, se colocará una entrada en la cola del grupo de subprocesos. Luego reanudaría la rutina correspondiente, procesaría sus datos en cola y luego la suspendería nuevamente, liberando el hilo de trabajo para realizar otro trabajo.

Detalles de implementacion:

Al pensar en cómo haría esto, tengo problemas para comprender las diferencias funcionales entre las rutinas apiladas y las apiladas. Tengo cierta experiencia en el uso de rutinas apiladas utilizando la biblioteca Boost.Coroutine . Creo que es relativamente fácil de comprender desde un nivel conceptual: para cada rutina, mantiene una copia del contexto y la pila de la CPU, y cuando cambia a una rutina, cambia a ese contexto guardado (al igual que lo haría un planificador en modo kernel )

Lo que no está tan claro para mí es cómo una corutina sin pila difiere de esta. En mi aplicación, la cantidad de sobrecarga asociada con la cola de elementos de trabajo descrita anteriormente es muy importante. La mayoría de las implementaciones que he visto, como la nueva biblioteca de CO2, sugieren que las corutinas sin pila proporcionan cambios de contexto mucho más bajos.

Por lo tanto, me gustaría entender las diferencias funcionales entre las rutinas apiladas y las apiladas más claramente. Específicamente, pienso en estas preguntas:

  • Referencias como esta sugieren que la distinción radica en dónde puede ceder / reanudar en una rutina apilada versus apilada. ¿Es este el caso? ¿Hay un ejemplo simple de algo que pueda hacer en una rutina de pila pero no en una sin pila?

  • ¿Hay alguna limitación en el uso de variables de almacenamiento automático (es decir, variables "en la pila")?

  • ¿Hay alguna limitación sobre qué funciones puedo llamar desde una rutina sin pila?

  • Si no se guarda el contexto de la pila para una rutina sin pila, ¿a dónde van las variables de almacenamiento automático cuando se ejecuta la rutina?


Primero, gracias por echar un vistazo al CO2 :)

El doc Boost.Coroutine describe la ventaja de la pila de corutina bien:

apilamiento

A diferencia de una rutina sin apilamiento, se puede suspender una corrida apilada desde un marco de apilamiento anidado . La ejecución se reanuda exactamente en el mismo punto del código donde se suspendió anteriormente. Con una rutina sin pila, solo se puede suspender la rutina de nivel superior. Cualquier rutina llamada por esa rutina de nivel superior puede no suspenderse. Esto prohíbe proporcionar operaciones de suspensión / reanudación en rutinas dentro de una biblioteca de uso general.

continuación de primera clase

Una continuación de primera clase se puede pasar como argumento, devuelta por una función y almacenada en una estructura de datos para ser utilizada más tarde. En algunas implementaciones (por ejemplo, rendimiento de C #), no se puede acceder a la continuación ni manipularla directamente.

Sin apilamiento y semántica de primera clase, algunos flujos de control de ejecución útiles no pueden ser soportados (por ejemplo, multitarea cooperativa o puntos de verificación).

¿Qué significa eso para usted? por ejemplo, imagine que tiene una función que lleva a un visitante:

template<class Visitor> void f(Visitor& v);

Desea transformarlo en iterador, con una rutina de pila, puede:

asymmetric_coroutine<T>::pull_type pull_from([](asymmetric_coroutine<T>::push_type& yield) { f(yield); });

Pero con la rutina sin pila, no hay forma de hacerlo:

generator<T> pull_from() { // yield can only be used here, cannot pass to f f(???); }

En general, la rutina de pila es más poderosa que la de pila sin pila. Entonces, ¿por qué queremos corutina sin pila? respuesta corta: eficiencia.

La rutina de apilamiento generalmente necesita asignar una cierta cantidad de memoria para acomodar su pila de tiempo de ejecución (debe ser lo suficientemente grande), y el cambio de contexto es más costoso en comparación con el sin pila, por ejemplo, Boost. La rutina toma 40 ciclos mientras que el CO2 toma solo 7 ciclos en promedio en mi máquina, porque lo único que necesita restaurar una rutina sin pila es el contador del programa.

Dicho esto, con soporte de idioma, probablemente la rutina de pila también puede aprovechar el tamaño máximo calculado por el compilador para la pila, siempre que no haya recursividad en la rutina, por lo que el uso de la memoria también se puede mejorar.

Hablando de la rutina sin pila, tenga en cuenta que no significa que no haya una pila de tiempo de ejecución en absoluto, solo significa que usa la misma pila de tiempo de ejecución que el lado del host, por lo que también puede llamar a funciones recursivas, solo eso todas las recursiones sucederán en la pila de tiempo de ejecución del host. En contraste, con la rutina de pila, cuando se llaman funciones recursivas, las recursiones sucederán en la pila de la misma.

Para responder las preguntas:

  • ¿Hay alguna limitación en el uso de variables de almacenamiento automático (es decir, variables "en la pila")?

No. Es la limitación de la emulación del CO2. Con soporte de idioma, las variables de almacenamiento automático visibles para la rutina se colocarán en el almacenamiento interno de la misma. Tenga en cuenta mi énfasis en "visible para la rutina", si la rutina llama a una función que utiliza variables de almacenamiento automático internamente, esas variables se colocarán en la pila de tiempo de ejecución. Más específicamente, la rutina sin pila solo tiene que preservar las variables / temporarios que se pueden usar después de reanudar.

Para que quede claro, también puede usar variables de almacenamiento automático en el cuerpo de rutina de CO2:

auto f() CO2_RET(co2::task<>, ()) { int a = 1; // not ok CO2_AWAIT(co2::suspend_always{}); { int b = 2; // ok doSomething(b); } CO2_AWAIT(co2::suspend_always{}); int c = 3; // ok doSomething(c); } CO2_END

Mientras la definición no preceda ninguna await .

  • ¿Hay alguna limitación sobre qué funciones puedo llamar desde una rutina sin pila?

No.

  • Si no se guarda el contexto de la pila para una rutina sin pila, ¿a dónde van las variables de almacenamiento automático cuando se ejecuta la rutina?

Respondido anteriormente, una rutina sin pila no se preocupa por las variables de almacenamiento automático utilizadas en las funciones llamadas, solo se colocarán en la pila de tiempo de ejecución normal.

Si tiene alguna duda, simplemente verifique el código fuente de CO2, puede ayudarlo a comprender la mecánica debajo del capó;)