c++ concurrency erlang actor message-passing

c++ - El modelo de actor: ¿Por qué es especial Erlang? O bien, ¿por qué necesita otro idioma para eso?



concurrency message-passing (6)

He estado buscando aprender erlang, y como resultado, he estado leyendo (bueno, hojeando) sobre el modelo de actor.

Según lo que entiendo, el modelo de actor es simplemente un conjunto de funciones (ejecutadas dentro de subprocesos ligeros llamados "procesos" en erlang), que se comunican entre sí solo a través del envío de mensajes.

Esto parece bastante trivial de implementar en C ++ o en cualquier otro idioma:

class BaseActor { std::queue<BaseMessage*> messages; CriticalSection messagecs; BaseMessage* Pop(); public: void Push(BaseMessage* message) { auto scopedlock = messagecs.AquireScopedLock(); messagecs.push(message); } virtual void ActorFn() = 0; virtual ~BaseActor() {} = 0; }

Con cada uno de sus procesos, una instancia de un BaseActor derivado. Los actores se comunican entre sí solo a través del envío de mensajes. (es decir, empujando). Los actores se registran con un mapa central de inicialización que permite a otros actores encontrarlos y permite que una función central los explore.

Ahora, entiendo que me estoy perdiendo, o más bien, pasando por alto un tema importante aquí, a saber: la falta de rendimiento significa que un solo Actor puede consumir injustamente un tiempo excesivo. ¿Pero son las corutinas multiplataforma lo principal que lo hace difícil en C ++? (Windows, por ejemplo, tiene fibras).

¿Hay algo más que me falta, o es el modelo realmente obvio?

Definitivamente no estoy tratando de comenzar una guerra de llama aquí, solo quiero entender lo que me estoy perdiendo, ya que esto es esencialmente lo que ya hago para poder razonar un tanto sobre el código concurrente.


El código de C ++ no trata con equidad, aislamiento, detección de fallas o distribución, que son todas las cosas que Erlang trae como parte de su modelo de actor.

  • Ningún actor tiene permitido matar de hambre a ningún otro actor (justicia)
  • Si un actor se bloquea, solo debería afectar a ese actor (aislamiento)
  • Si un actor se bloquea, otros actores deberían ser capaces de detectar y reaccionar a ese bloqueo (detección de fallas)
  • Los actores deberían poder comunicarse a través de una red como si estuvieran en la misma máquina (distribución)

También el emulador SMP de haz trae la programación JIT de los actores, moviéndolos al núcleo que es en el momento el que menos utiliza y también hiberna los hilos en ciertos núcleos si ya no son necesarios.

Además, todas las bibliotecas y herramientas escritas en Erlang pueden suponer que esta es la forma en que funciona el mundo y se diseñan en consecuencia.

Estas cosas no son imposibles de hacer en C ++, pero se vuelven cada vez más difíciles si se agrega el hecho de que Erlang funciona en casi todas las principales configuraciones de hw y os.

editar: Acabo de encontrar una descripción de Ulf Wiger sobre lo que él ve como concurrencia de estilo erlang.


Es mucho menos acerca del modelo de actor y mucho más sobre lo difícil que es escribir correctamente algo análogo a OTP en C ++. Además, los diferentes sistemas operativos proporcionan herramientas de depuración y sistema radicalmente diferentes, y la máquina virtual de Erlang y varias construcciones de lenguaje soportan una manera uniforme de descubrir qué es lo que todos esos procesos son difíciles de hacer de una manera uniforme (o tal vez en absoluto) a través de varias plataformas. (Es importante recordar que Erlang / OTP es anterior al zumbido actual sobre el término "modelo de actor", por lo que en algunos casos este tipo de discusiones están comparando manzanas y pterodáctilos, las grandes ideas son propensas a la invención independiente).

Todo esto significa que si bien puedes escribir un conjunto de programas de "actor modelo" en otro idioma (lo sé, lo he hecho durante mucho tiempo en Python, C y Guile sin darme cuenta antes de encontrarme con Erlang, incluida una forma de monitores y enlaces, y antes de haber escuchado alguna vez el término "modelo de actor"), entendiendo cómo los procesos que en realidad genera su código y lo que está sucediendo entre ellos es extremadamente difícil. Erlang impone reglas que un sistema operativo simplemente no puede sin grandes revisiones del kernel: revisión del kernel que probablemente no sea beneficiosa en general. Estas reglas se manifiestan tanto como restricciones generales del programador (que siempre se pueden obtener si realmente se necesita) como promesas básicas que el sistema garantiza para el programador (que se pueden romper deliberadamente si realmente se necesita).

Por ejemplo, exige que dos procesos no puedan compartir estado para protegerlo de los efectos secundarios. Esto no significa que cada función debe ser "pura" en el sentido de que todo es referencialmente transparente (obviamente no, aunque hacer que gran parte de tu programa sea referencialmente transparente como práctico es un objetivo de diseño claro de la mayoría de los proyectos de Erlang), sino que los procesos no crean constantemente condiciones de carrera relacionadas con el estado compartido o la contención. (Esto es más lo que los "efectos secundarios" significan en el contexto de Erlang, por cierto, saber que puede ayudar a descifrar algo de la discusión cuestionando si Erlang es "realmente funcional o no" en comparación con Haskell o juguetes "puros" idiomas .)

Por otro lado, el tiempo de ejecución de Erlang garantiza la entrega de mensajes. Esto es algo que se echa mucho de menos en un entorno en el que debe comunicarse puramente a través de puertos no administrados, tuberías, memoria compartida y archivos comunes que el núcleo del sistema operativo es el único que gestiona (y la administración de kernel de estos recursos es necesariamente mínima en comparación con lo que Erlang tiempo de ejecución proporciona). Esto no significa que Erlang garantice RPC (de todos modos, el envío de mensajes no es RPC, ni es una invocación de método), no promete que su mensaje se dirige correctamente, y no promete que usted está procesando un mensaje. tratando de enviar un mensaje a Existe o está vivo, tampoco. Simplemente garantiza la entrega si lo que envía es válido en ese momento.

Construido sobre esta promesa es la promesa de que los monitores y los enlaces son precisos. Y basado en eso, el tiempo de ejecución de Erlang hace que todo el concepto de "cluster de red" desaparezca una vez que comprendes lo que está sucediendo con el sistema (y cómo usar erl_connect ...). Esto le permite saltar sobre un conjunto de casos difíciles de concurrencia, lo que le da a uno una gran ventaja en la codificación para el caso exitoso en lugar de enredarse en el pantano de las técnicas defensivas requeridas para la programación simultánea desnuda.

Por lo tanto, no se trata realmente de necesitar Erlang, el lenguaje, el tiempo de ejecución y la OTP ya existentes, expresarse de una manera bastante limpia e implementar algo cercano a él en otro idioma que sea extremadamente difícil. OTP es solo un acto difícil de seguir. En la misma línea, tampoco necesitamos realmente C ++, podríamos pegarnos a la entrada binaria en bruto, Brainfuck y considerar a Assembler como nuestro lenguaje de alto nivel. Tampoco necesitamos trenes ni barcos, ya que todos sabemos caminar y nadar.

Dicho todo esto, el bytecode de la VM está bien documentado, y han surgido varios lenguajes alternativos que lo compilan o trabajan con el tiempo de ejecución de Erlang. Si dividimos la pregunta en una parte de lenguaje / sintaxis ("¿Tengo que entender Moon Runes para hacer concurrencia?") Y una parte de plataforma ("¿Es la OTP la forma más madura de hacer concurrencia, y me guiará por el camino más complicado? , las trampas más comunes que se encuentran en un entorno simultáneo y distribuido? "), entonces la respuesta es (" no "," sí ").


Esta es realmente una excelente pregunta y ha recibido respuestas excelentes que tal vez no sean convincentes.

Para agregar sombra y énfasis a las otras grandes respuestas que ya están aquí, considere lo que Erlang quita (en comparación con los lenguajes de uso general tradicionales como C / C ++) para lograr tolerancia a fallas y tiempo de actividad.

Primero, quita los bloqueos. El libro de Joe Armstrong presenta este experimento mental: supongamos que su proceso adquiere un bloqueo y luego se bloquea inmediatamente (un error de la memoria hace que el proceso se bloquee, o que la energía deje de formar parte del sistema). La próxima vez que un proceso espere ese mismo bloqueo, el sistema acaba de estancarse. Esto podría ser un bloqueo obvio, como en la llamada AquireScopedLock () en el código de ejemplo; o podría ser un bloqueo implícito adquirido en su nombre por un administrador de memoria, por ejemplo al llamar a malloc () o libre ().

En cualquier caso, su falla en el proceso ha impedido que todo el sistema avance. Fini. Fin de la historia. Tu sistema está muerto. A menos que pueda garantizar que cada biblioteca que use en C / C ++ nunca llame a malloc y nunca adquiera un bloqueo, su sistema no tolerará fallas. Los sistemas de Erlang pueden matar procesos a voluntad cuando se realizan cargas pesadas para progresar, por lo que a escala, los procesos de Erlang deben poderse eliminar (en cualquier punto de ejecución) para mantener el rendimiento.

Existe una solución parcial: utilizar concesiones en todas partes en lugar de cerraduras, pero no tiene garantía de que todas las bibliotecas que utilice también lo hagan. Y la lógica y el razonamiento sobre la corrección se ponen realmente peludos rápidamente. Además, los arriendos se recuperan lentamente (después de que expira el tiempo de espera), por lo que todo el sistema se vuelve realmente lento ante el fracaso.

En segundo lugar, Erlang elimina el tipado estático, que a su vez permite el intercambio de código caliente y la ejecución simultánea de dos versiones del mismo código. Esto significa que puede actualizar su código en tiempo de ejecución sin detener el sistema. Así es como los sistemas se mantienen activos durante nueve o 32 meses de inactividad / año. Simplemente se actualizan en su lugar. Sus funciones C ++ tendrán que volver a vincularse manualmente para poder actualizarse, y no se admite la ejecución de dos versiones al mismo tiempo. Las actualizaciones de código requieren un tiempo de inactividad del sistema, y ​​si tiene un clúster grande que no puede ejecutar más de una versión de código a la vez, deberá eliminar todo el clúster de una vez. Ay. Y en el mundo de las telecomunicaciones, no es tolerable.

Además, Erlang elimina la memoria compartida y la recolección de basura compartida compartida; cada proceso liviano es basura recolectada de forma independiente. Esta es una extensión simple del primer punto, pero enfatiza que para la verdadera tolerancia a fallas necesita procesos que no estén enclavados en términos de dependencias. Significa que las pausas de su GC en comparación con Java son tolerables (pequeñas en lugar de pausar una media hora para que se complete un GC de 8GB) para sistemas grandes.



No me gusta citarme a mí mismo, sino a partir de la primera regla de programación de Virding

Cualquier programa simultáneo suficientemente complicado en otro idioma contiene una implementación lenta ad hoc especificada de forma informal e incorrecta de la mitad de Erlang.

Con respecto a Greenspun. Joe (Armstrong) tiene una regla similar.

El problema no es implementar actores, eso no es tan difícil. El problema es hacer que todo trabaje en conjunto: procesos, comunicación, recolección de basura, primitivas del lenguaje, manejo de errores, etc. Por ejemplo, usar subprocesos del sistema operativo escasea mucho, por lo que debe hacerlo usted mismo. Sería como tratar de "vender" un idioma OO donde solo puedes tener 1k objetos y son pesados ​​de crear y usar. Desde nuestro punto de vista, la concurrencia es la abstracción básica para las aplicaciones de estructuración.

Dejándome llevar así que me detendré aquí.


Casablanca es otro chico nuevo en el bloque modelo de actor. Una aceptación asincrónica típica se ve así:

PID replyTo; NameQuery request; accept_request().then([=](std::tuple<NameQuery,PID> request) { if (std::get<0>(request) == FirstName) std::get<1>(request).send("Niklas"); else std::get<1>(request).send("Gustafsson"); }

(Personalmente, considero que github.com/actor-framework/actor-framework hace un mejor trabajo al ocultar el patrón que se encuentra detrás de una interfaz agradable).