En el libro "Python. A las alturas de la excelencia ”Luciano Ramallo describe una historia. En 2000, Luciano tomó cursos, y una vez Guido van Rossum miró a la audiencia. Una vez que tal evento apareció, todos comenzaron a hacerle preguntas. Cuando se le preguntó acerca de las funciones que Python tomó prestadas de otros idiomas, Guido respondió: "Todo lo que es bueno en Python es robado de otros idiomas".
Realmente lo es Python ha vivido durante mucho tiempo en el contexto de otros lenguajes de programación y absorbe conceptos de su entorno: asyncio es prestado, gracias a las expresiones lambda de Lisp aparecieron, y Tornado fue copiado de libevent. Pero si alguien toma prestadas ideas, es de Erlang. Fue creado hace 30 años, y todos los conceptos en Python que se están implementando actualmente o que se acaban de describir han funcionado durante mucho tiempo en Erlang: mensajes de múltiples núcleos como base de comunicación, llamadas a métodos e introspección dentro de un sistema de producción en vivo. Estas ideas, de una forma u otra, encuentran su expresión en sistemas como
Seastar.io .
Si no tiene en cuenta Data Science, en el que Python ahora está fuera de competencia, entonces todo lo demás ya está implementado en Erlang: trabajar con una red, manejar HTTP y sockets web, trabajar con bases de datos. Por lo tanto, es importante que los desarrolladores de Python entiendan hacia dónde se moverá el lenguaje: a lo largo de un camino que ya ha pasado hace 30 años.
Para comprender la historia del desarrollo de otros lenguajes y comprender dónde progresa el progreso, invitamos a
Maxim Lapshin (
erlyvideo ), el autor del proyecto Erlyvideo.ru, a
Moscow Python Conf ++ .
Debajo del corte está la versión de texto de este informe, a saber: en qué dirección se ve obligado a desarrollarse el sistema, que continúa migrando de un código lineal simple a liberante y más allá, lo cual es común y cuáles son las diferencias entre Elixir y Python. Prestaremos especial atención a cómo administrar sockets, hilos y datos en diferentes lenguajes y plataformas de programación.
Erlyvideo.ru tiene un sistema de video vigilancia en el que el control de acceso para cámaras está escrito en Python. Esta es una tarea clásica para este idioma. Hay usuarios y cámaras, videos desde los cuales pueden mirar: alguien ve algunas cámaras, mientras que otros ven un sitio normal.
Se eligió Python porque es conveniente escribir un servicio de este tipo en él: después de todo, hay marcos, ORM, programadores. El software desarrollado se empaqueta y se vende a los usuarios. Erlyvideo.ru es una empresa que vende software y no solo brinda servicio.
Qué problemas con Python quiero resolver.
¿Por qué hay tales problemas con multinúcleo? Ejecutamos Flussonic en computadoras de estadios incluso antes que Intel. Pero Python tiene dificultades con esto: ¿por qué sigue sin utilizar los 80 núcleos de nuestros servidores para trabajar?
¿Cómo no sufrir de enchufes abiertos? Monitorear el número de tomas abiertas es un gran problema. Cuando llegue al límite, cierre y evite las fugas también.
¿Las variables globales olvidadas tienen solución? La filtración de variables globales es un infierno para cualquier lenguaje de recolección de basura como Java o C #.
¿Cómo usar el hierro sin desperdiciar recursos? ¿Cómo sobrevivir sin ejecutar 40 trabajadores de Jung y 64 GB de RAM si queremos usar servidores de manera eficiente y no arrojar cientos de miles de dólares al mes en hardware innecesario?
Por qué se necesita multinúcleo
Para que todos los núcleos se utilicen por completo, se necesitan muchos más trabajadores que núcleos. Por ejemplo, para 40 núcleos de procesador, se necesitan 100 trabajadores: un trabajador fue a la base de datos, el otro está ocupado con otra cosa.
Un trabajador puede consumir 300-400 MB . Todavía estamos escribiendo esto en Python, y no en Ruby on Rails, que puede consumir varias veces más y 40 GB de RAM se desperdiciarán fácil y fácilmente. No es muy caro, pero ¿por qué comprar memoria donde no se puede comprar?
Multi-core ayuda a buscar datos compartidos y reducir el consumo de memoria , ejecutar convenientemente y con seguridad muchos procesos independientes. Es mucho más fácil de programar, pero más caro desde la memoria.
Gestión de enchufes
En el socket web, sondeamos los datos de tiempo de ejecución de las cámaras desde el backend. El software Python se conecta a Flussonic y sondea los datos de estado de las cámaras: si funcionan o no, ¿hay algún evento nuevo?
Por otro lado, el cliente se conecta y, a través del socket web, enviamos estos datos al navegador. Queremos transferir los datos del cliente en tiempo real: la cámara se encendió y apagó, el gato comió, durmió, rompió un sofá, presionó el botón y se llevó al gato.
Pero, por ejemplo, se produjo algún tipo de problema: la base de datos no respondió a la solicitud, todo el código se cayó, había dos zócalos abiertos. Comenzamos a recargar, hicimos algo, nuevamente este problema: había dos enchufes. El error de la base de datos se procesó incorrectamente y se bloquearon dos conexiones abiertas. Con el tiempo, esto conduce a fugas en el zócalo.
Variables globales olvidadas
Hizo un dict global para la lista de navegadores conectados a través del socket web. Una persona inicia sesión en el sitio, abrimos un socket web para él. Luego colocamos el socket web con su identificador en algún tipo de dict global, y resulta que se produce algún tipo de error.
Por ejemplo, grabaron un enlace de conexión en dict para enviar datos.
Una excepción funcionó, olvidé eliminar el enlace y los datos se colgaron . Entonces, después de algún tiempo, se comienzan a perder 64 GB, y quiero duplicar la memoria en el servidor. Esto no es una solución, porque los datos se filtrarán de todos modos.
Siempre cometemos errores: somos personas y no podemos hacer un seguimiento de todo.
La pregunta es que ocurren algunos errores, incluso aquellos que no esperábamos ver.
Excursión histórica
Para llegar al tema principal, profundicemos en la historia. De todo lo que estamos hablando sobre Python, Go y Erlang ahora, otras personas hicieron todo esto hace unos 30 años. Nosotros en Python recorremos un largo camino y llenamos los baches que ya se han superado hace décadas. El camino se repite de una manera asombrosa.
Dos
Primero, pasemos a DOS, está más cerca. Antes de él había cosas completamente diferentes y no todos están vivos y recuerdan las computadoras antes de DOS.
El programa DOS ocupaba la computadora (casi) exclusivamente . Mientras un juego, por ejemplo, se está ejecutando, no se ejecuta nada más. No accederá a Internet, todavía no está allí y ni siquiera llegará a ninguna parte. Fue triste, pero los recuerdos son cálidos, porque está asociado con la juventud.
Multitarea cooperativa
Como fue realmente doloroso con DOS, aparecieron nuevos desafíos, las computadoras se volvieron más poderosas.
Hace décadas, desarrollaron el concepto de multitarea cooperativa , incluso antes de Windows 3.11.
Los datos están separados por procesos, y cada proceso se realiza por separado: de alguna manera están protegidos entre sí. El código incorrecto en un proceso no podrá estropear el código en el navegador (entonces los primeros navegadores ya aparecieron).
La siguiente pregunta es: ¿cómo se distribuirá el tiempo de computación entre los diferentes procesos? Entonces no era que no hubiera más de un núcleo, un sistema de doble procesador era una rareza. El esquema era este: mientras que un proceso fue, por ejemplo, a un disco para datos, el segundo proceso recibe el control del sistema operativo. El primero podrá obtener el control cuando el segundo ceda voluntariamente. Simplifico enormemente la situación, pero el
proceso de alguna manera permitió voluntariamente eliminarlo del procesador .
Multitarea preventiva
La multitarea cooperativa condujo al siguiente problema: el proceso podría simplemente bloquearse porque está mal escrito.
Si el procesador tarda mucho tiempo en procesarse, bloquea el resto . En este caso, la computadora se bloqueó y no se pudo hacer nada con ella, por ejemplo, cambiar la ventana.
En respuesta a este problema, se inventó la multitarea preventiva. El sistema operativo ahora se maneja duro: elimina los procesos de la ejecución, separa por completo sus datos, protege la memoria del proceso entre sí y les da a todos una cierta cantidad de tiempo computacional.
El sistema operativo asigna los mismos intervalos de tiempo a cada proceso .
La cuestión de la pérdida de tiempo aún está abierta. Hoy en día, los desarrolladores de sistemas operativos todavía están pensando qué es lo correcto, en qué orden, a quién y cuánto tiempo dar para la administración. Hoy vemos el desarrollo de estas ideas.
Corrientes
Pero esto no fue suficiente. Los procesos necesitan intercambiar datos: a través de la red es costoso, de alguna manera aún complicado. Por lo tanto, se inventó el
concepto de flujos .
Los subprocesos son procesos ligeros que comparten una memoria común.
Las transmisiones se crearon con la esperanza de que todo sea fácil, simple y divertido. Ahora la programación
multiproceso se considera antipatrón . Si la lógica de negocios está escrita en subprocesos, este código probablemente debería descartarse, ya que probablemente haya errores en él. Si le parece que no hay errores, simplemente no los ha encontrado todavía.
La programación multiproceso es una cosa extremadamente compleja. Hay pocas personas que realmente se dedicaron a la capacidad de escribir en hilos y obtienen algo que realmente funciona.
Mientras tanto, aparecieron
computadoras multinúcleo . Trajeron cosas terribles con ellos. Tomó un enfoque completamente diferente a los datos, surgieron preguntas con la localidad de los datos, ahora debe comprender desde qué núcleo va a qué datos.
Un núcleo necesita poner los datos aquí, el otro allí, y en ningún caso confundir estas cosas, porque los grupos realmente aparecieron dentro de la computadora. Dentro de una computadora moderna, hay un clúster cuando parte de la memoria se suelda a un núcleo y la otra a otro. El tiempo de tránsito entre estos datos puede variar según el orden de magnitud.
Ejemplos de Python
Considere un ejemplo simple de "Servicio para ayudar al cliente". Selecciona el mejor precio para los productos en varias plataformas: manejamos en nombre de los productos y buscamos pisos comerciales con un precio mínimo.
Este es el código en el antiguo Django, Python 2. Hoy en día no es muy popular, pocas personas comienzan proyectos en él.
@api_view(['GET']) def best_price(request): name = request.GET['name'] price1 = http_fetch_price('market.yandex.ru', name) price2 = http_fetch_price('ebay.com', name) price3 = http_fetch_price('taobao.com', name) return Response(min([price1,price2,price3]))
Llega una solicitud, vamos a un backend y luego a otro. En los lugares donde se
http_fetch_price
, los hilos se bloquean. En este momento, todo el trabajador se embarca en un viaje a Yandex.Market, luego a eBay, luego hasta un tiempo de espera en Taobao, y al final da una respuesta.
Todo este tiempo todo el trabajador está de pie .
Es muy difícil sondear múltiples backends al mismo tiempo. Esta es una mala situación: se consume memoria, se requiere el lanzamiento de una gran cantidad de trabajadores y se debe monitorear todo el servicio. Es necesario observar cuán frecuentes son tales solicitudes, si aún necesita ejecutar trabajadores o si hay alguna otra vez más. Estos son los problemas de los que hablé.
Es necesario interrogar varios backends a su vez .
¿Qué vemos en Python?
Un proceso por tarea, en Python todavía no hay multinúcleo. La situación es clara: en los idiomas de esta clase es difícil hacer un multinúcleo simple y seguro, ya que
matará el rendimiento .
Si va al dict desde diferentes flujos, entonces el acceso a los datos se puede escribir de esta manera: pegue dos instancias de Python en la memoria para que puedan revolver los datos, simplemente los rompen. Por ejemplo, para ir a dictar y no romper nada, debe poner mutexes delante de él. Si hay un mutex antes de cada dict, entonces el sistema se ralentizará aproximadamente 1000 veces, simplemente será un inconveniente. Es difícil arrastrarlo a un multinúcleo.
Tenemos
solo un hilo de ejecución y
solo los procesos pueden escalar . De hecho, reinventamos DOS dentro del proceso, el lenguaje de secuencias de comandos de 2010. Dentro del proceso hay una cosa que se parece a DOS: mientras hacemos algo, todos los demás procesos no funcionan. A nadie le gustó el enorme exceso de costos y la lenta respuesta.
Los reactores de socket aparecieron en Python hace algún tiempo, aunque el concepto en sí nació hace mucho tiempo. Ahora puede esperar la disponibilidad de varios enchufes a la vez.
Al principio, el reactor tuvo demanda en servidores como nginx. Incluso debido al uso correcto de esta tecnología, se ha vuelto popular. Luego, el concepto se arrastró a lenguajes de script como Python y Ruby.
La idea del reactor es que pasamos a la programación orientada a eventos.
Programación Orientada a Eventos
Un contexto de ejecución produce una solicitud. Mientras espera una respuesta, se está ejecutando un contexto diferente. Es de destacar que casi pasamos por la misma etapa de evolución que la transición de DOS a Windows 3.11. Solo las personas hicieron esto 20 años antes, y en Python y Ruby apareció hace 10 años.
Torcido
Este es un marco de red basado en eventos. Apareció en 2002 y está escrito en Python. Tomé el ejemplo anterior y lo reescribí en Twisted.
def render_GET(self, request): price1 = deferred_fetch_price('market.yandex.ru', name) price2 = deferred_fetch_price('ebay.com', name) price3 = deferred_fetch_price('taobao.com', name) dl = defer.DeferredList([price1,price2,price3]) def reply(prices): request.write('%d'.format(min(prices))) request.finish() dl.addCallback(reply) return server.NOT_DONE_YET
Puede haber errores, imprecisiones, y el manejo notorio de errores no es suficiente. Pero el esquema aproximado es este: no hacemos una solicitud, sino que solicitamos que la solicitemos más tarde, cuando haya tiempo. En la línea con
defer.DeferredList
, queremos recopilar las respuestas de varias consultas.
De hecho, el código consta de dos partes. En la primera parte, lo que sucedió antes de la solicitud, y en la segunda, lo que sucedió después.
Toda la historia de la programación orientada a eventos está saturada con el dolor de romper el código lineal en "antes de la solicitud" y "después de la solicitud".
Esto duele porque los fragmentos de código están mezclados: las últimas líneas aún se ejecutan en la solicitud original, y la función de
reply
se llamará después.
No es fácil tenerlo en cuenta precisamente porque rompimos el código lineal, pero tenía que hacerse. Sin entrar en detalles, el código que se ha reescrito de Django a Twisted
producirá una pseudoaceleración completamente increíble .
Idea retorcida
Se puede activar un objeto cuando el zócalo está listo.
Tomamos objetos en los que recopilamos los datos necesarios del contexto y vinculamos su activación al socket. La disponibilidad de sockets es ahora uno de los controles más importantes para todo el sistema. Los objetos serán nuestros contextos.
Pero al mismo tiempo, el lenguaje aún separa el concepto mismo del contexto de ejecución en el que viven las excepciones.
El contexto de ejecución vive separado de los objetos y está conectado libremente con ellos . Aquí el problema surge con el hecho de que estamos tratando de recopilar datos dentro de los objetos: no hay manera sin ellos, pero el lenguaje no lo admite.
Todo esto lleva a un clásico infierno de devolución de llamada. Por lo que, por ejemplo, aman Node.js: hasta hace poco, no había otros métodos, pero aún aparecía en Python. El problema es que hay
saltos de código en los puntos del IO externo que conducen a la devolución de llamada.
Hay muchas preguntas ¿Es posible "pegar" los bordes del espacio en el código? ¿Es posible volver al código humano normal? ¿Qué hacer si un objeto lógico funciona con dos sockets y uno de ellos está cerrado? ¿Cómo no olvidar cerrar el segundo? ¿Es posible usar de alguna manera todos los núcleos?
Async io
Una buena respuesta a estas preguntas es Async IO. Este es un gran paso adelante, aunque no fácil. Async IO es una cosa complicada, bajo la cual hay muchos matices dolorosos.
async def best_price(request): name = request.GET['name'] price1 = async_http_fetch_price('market.yandex.ru', name) price2 = async_http_fetch_price('ebay.com', name) price3 = async_http_fetch_price('taobao.com', name) prices = await asyncio.wait([price1,price2,price3]) return min(prices)
La brecha de código está oculta bajo la sintaxis
async/await
. Tomamos todo lo que era antes, pero no fuimos a la red en este código. Eliminamos
Callback(reply)
, que estaba en el ejemplo anterior, y lo escondimos detrás de la
await
: el lugar donde se cortará el código con unas tijeras. Se dividirá en dos partes: la parte que llama y la parte de devolución de llamada, que procesa los resultados.
Este es un
gran azúcar sintáctico . Hay métodos para pegar múltiples expectativas en una sola. Esto es genial, pero hay un matiz:
todo se puede romper con un zócalo "clásico" . En Python, todavía hay una gran cantidad de bibliotecas que van al socket sincrónicamente, crean una
timer library
y arruinan todo por ti. Cómo depurar esto, no lo sé.
Pero
asyncio no ayuda con fugas y multinúcleo . Por lo tanto, no hay cambios fundamentales, aunque ha mejorado.
Todavía tenemos todos los problemas de los que hablamos al principio:
- fácil de filtrar con enchufes;
- enlaces fáciles de dejar en variables globales;
- manejo de errores muy minucioso;
- Todavía es difícil hacer multi-core.
Que hacer
No sé si todo esto evolucionará, pero mostraré la implementación en otros idiomas y plataformas.
Contextos de ejecución aislados. En contextos de ejecución, los resultados se acumulan, se mantienen los sockets: objetos lógicos en los que generalmente almacenamos todos los datos sobre devoluciones de llamadas y sockets. Un concepto: tomar contextos de ejecución, pegarlos a hilos de ejecución y aislarlos completamente unos de otros.
Cambio de paradigma de objetos. Conectemos el contexto al hilo de ejecución. Hay análogos, esto no es algo nuevo. Si alguien intentó editar el código fuente de Apache y escribirles módulos, entonces sabe que hay un grupo de Apache.
No se permiten enlaces entre los grupos de Apache. Los datos de un grupo de Apache: el grupo asociado con las solicitudes, se encuentra dentro de él, y no puede obtener nada de él.
Teóricamente es posible, pero si lo hace, o alguien lo regañará, o no aceptará el parche, o tendrá una depuración larga y dolorosa en la producción. Después de eso, nadie hará esto y permitirá que otros hagan tales cosas. Simplemente es imposible hacer referencia a datos entre contextos, se necesita un aislamiento total.
¿Cómo intercambiar actividad? Lo que se necesita no son pequeñas mónadas, que están cerradas dentro de sí mismas y no se comunican entre sí. Los necesitamos para comunicarse. Un enfoque es la mensajería. Este es aproximadamente el camino que Windows ha tomado al intercambiar mensajes entre procesos. En un sistema operativo normal, no puede proporcionar un enlace a la memoria de otro proceso, pero puede enviar señales a través de la red, como en UNIX, o mediante mensajes, como en Windows.
Todos los recursos dentro del proceso y el contexto se convierten en un hilo de ejecución . Pegamos juntos:
- datos de tiempo de ejecución en una máquina virtual en la que se producen excepciones;
- el hilo de ejecución, como lo que se está ejecutando en el procesador;
- Un objeto en el que todos los datos se recopilan lógicamente.
¡Felicidades, inventamos UNIX dentro de un lenguaje de programación! Esta idea fue inventada alrededor de 1969. Hasta ahora, todavía no está en Python, pero es probable que Python llegue a esto. Y tal vez ella no venga, no lo sé.
Que da
En primer lugar,
control automático sobre los recursos . En Moscow Python Conf ++ 2019
dijeron que puedes escribir un programa en Go y procesar todos los errores. El programa se mantendrá como un guante y funcionará durante meses. Esto es cierto, pero no manejamos todos los errores.
Somos personas vivas, siempre tenemos plazos, el deseo de hacer algo útil y no manejar el error 535 por hoy. El código que está repleto de manejo de errores nunca causa sentimientos cálidos en nadie.
Por lo tanto, todos escribimos "camino feliz", y luego lo resolveremos en producción. Seamos honestos: solo cuando necesita procesar algo, entonces comenzamos a procesar. La programación defensiva es un poco diferente, y no es un desarrollo comercial.
Por lo tanto,
cuando tenemos autocontrol para errores, esto está bien . Pero los sistemas operativos surgieron hace 50 años: si algún proceso muere, entonces todo lo que abre se cerrará automáticamente. Hoy nadie necesita escribir código que limpiará los archivos detrás del proceso finalizado. Esto no ha existido durante 50 años en ningún sistema operativo, pero en Python aún debe seguir esto cuidadosamente y con cuidado con las manos. Esto es raro
Puede llevar la informática pesada a un contexto diferente , pero ya puede ir a otro núcleo. Compartimos los datos, ya no necesitamos mutexes. Puede enviar los datos en un contexto diferente, diga: "Lo hará en algún lugar y luego avíseme que ha terminado y hecho algo".
Una implementación asincio sin las palabras "async / await" . Además, un poco de ayuda de la máquina virtual, en tiempo de ejecución. Esto es de lo que hablamos con
async/await
: también puede convertir a mensajes, eliminar
async/await
y obtenerlo a nivel de máquina virtual.
Procesos Erlang
Erlang fue inventado hace 30 años. Los barbudos, que no eran muy barbudos entonces, miraron UNIX y transfirieron todos los conceptos al lenguaje de programación. Decidieron que ahora tendrían lo suyo para dormir por la noche e ir a pescar en silencio sin una computadora. Entonces todavía no había computadoras portátiles, pero los tipos con barba ya sabían que esto debería pensarse de antemano.
Tenemos Erlang (Elixir): contextos activos que se ejecutan solos . Además mi ejemplo sobre Erlang. En Elixir, se ve casi igual, con algunas variaciones.
best_price(Name) -> Price1 = spawn_price_fetcher('market.yandex.ru', Name), Price2 = spawn_price_fetcher('ebay.com', Name), Price3 = spawn_price_fetcher('taobao.com', Name), lists:min(wait4([Price1,Price2,Price3])).
Lanzamos varios buscadores: estos son varios contextos nuevos que estamos esperando. Esperaron, recopilaron los datos y devolvieron el resultado como el precio mínimo. Todo esto es similar a
async/await
, pero sin las palabras "async / await".
Características del elixir
Elixir se encuentra en la base de Erlang, y todos los conceptos de lenguaje se transfieren silenciosamente a Elixir. ¿Cuáles son sus características?
Prohibición de enlaces entre procesadores. Por proceso me refiero a un proceso ligero dentro de una máquina virtual: contexto. Simplificado, si se transfiere a Python, los enlaces de datos dentro de otro objeto están prohibidos en Erlang. Puede tener un enlace a todo el objeto como un cuadro cerrado, pero no puede hacer referencia a los datos que contiene. Ni siquiera puede obtener sintácticamente un puntero a los datos que están dentro de otro objeto. Solo puedes saber sobre el objeto en sí.
No hay mutexes dentro de los procesos (objetos). Esto es importante: personalmente, nunca quiero en mi vida cruzarme con la historia de la depuración de vuelos de múltiples hilos a la producción. No le deseo esto a nadie.
Los procesos pueden moverse por los núcleos, es seguro. Ya no necesitamos omitir, como en Java, un montón de otros
pointer
y reescribirlos al mover datos de un lugar a otro: no tenemos datos comunes y enlaces internos. Por ejemplo, ¿de dónde viene el problema de escasez de cadera? Debido al hecho de que alguien se refiere a estos datos.
Si transferimos los datos dentro del montón a otra ubicación para la compactación, debemos revisar todo el sistema. Puede ocupar decenas de gigabytes y actualizar todos los punteros; esto es una locura.
Seguridad total de subprocesos , debido a que toda la comunicación pasa por mensajes. A la rendición de todo esto, nos
despojamos del proceso de desplazamiento . Lo consiguió fácil y barato.
Los mensajes como base de la comunicación. Objetos internos, llamadas a funciones ordinarias y entre objetos de mensaje. La llegada de datos de la red es un mensaje, la respuesta de otro objeto es un mensaje, algo más afuera también es un mensaje en una cola entrante. Esto no está en UNIX porque no ha echado raíces.
Método de llamadas. Tenemos objetos que llamamos procesos. Los métodos en los procesos se llaman a través de mensajes.
Los métodos de llamada también envían un mensaje. Es genial que ahora se pueda hacer con un tiempo de espera. Si algo nos responde lentamente, llamamos al método en otro objeto. Pero al mismo tiempo decimos que estamos listos para esperar no más de 60 s, porque tengo un cliente con un tiempo de espera de 70 s. Tendré que ir y decirle "503" - ven mañana, ahora no te están esperando.
Además, la
respuesta a la llamada puede posponerse . Dentro del objeto, puede aceptar la solicitud de llamar al método y decir: "Sí, sí, lo dejaré ahora, vuelva en media hora, le responderé". No se puede hablar, pero se reserva en silencio. A veces lo usamos.
¿Cómo trabajar con una red?
Puede escribir código lineal, devoluciones de llamada o al estilo de
asyncio.gather
. Un ejemplo de cómo se verá esto.
wait4([ ]) -> [ ]; wait4(List) -> receive {reply, Pid, Price} -> [Price] ++ wait4(List -- [Pid]) after 60000 -> [] end.
En la función
wait4
del ejemplo anterior,
wait4
sobre la lista de aquellos de quienes todavía estamos esperando respuestas. Si usando el método de
receive
recibimos un mensaje de ese proceso, lo escribimos en la lista. Si la lista ha terminado, devolvemos todo lo que estaba y acumulamos la lista. Pedimos al mismo tiempo tres objetos para que nos condujeran los datos. Si no se las arreglaron juntos en 60 segundos, y al menos uno de ellos no respondió OK, tendremos una lista vacía. Pero es importante que hagamos un tiempo de espera general para una solicitud de inmediato a un montón de objetos.
Alguien podría decir: "Piensa, libcurl tiene lo mismo". Pero aquí es importante que, por otro lado, pueda haber no solo un viaje HTTP, sino también un viaje DB, así como algunos cálculos, por ejemplo, calcular algún tipo de número óptimo para el cliente.
Manejo de errores
Los errores han pasado de la secuencia al objeto, que ahora son uno y el mismo . Ahora el error en sí se adjunta no al subproceso, sino al objeto donde se ejecutó.
Esto es mucho más lógico. Por lo general, cuando dibujamos todo tipo de pequeños cuadrados y círculos en el tablero con la esperanza de que cobren vida y comiencen a darnos resultados y dinero, generalmente dibujamos objetos, no los flujos en los que se ejecutarán estos objetos. Por ejemplo, en el momento de la entrega podemos recibir un
mensaje automático
sobre la muerte de otro objeto .
Introspección o depuración en producción
Qué podría ser mejor que ir a la producción y el débito, especialmente si el error ocurre solo bajo carga durante las horas pico. En hora punta decimos:
- ¡Vamos, reiniciaré ahora!- ¡Sal por la puerta y alguien más se reiniciará!Aquí podemos entrar en un sistema vivo que se está ejecutando en este momento y no está especialmente preparado para esto. Para hacer esto, no necesita reiniciarlo con el generador de perfiles, con el depurador, reconstruir.
Sin ninguna pérdida de rendimiento en un sistema de producción en vivo, podemos ver una lista de procesos: lo que hay dentro de ellos, cómo funciona todo, desecharlos, verificar lo que les sucede. Todo esto es gratis fuera de la caja.
Bonos
El código es súper confiable. Por ejemplo, Python tiene fragilidad con el
old vs async
, y permanecerá durante cinco años, nada menos. Teniendo en cuenta la velocidad con la que se implementó Python 3, no debe esperar que sea rápido.
Leer y rastrear mensajes es más fácil que depurar devoluciones de llamada . Esto es importante Parece que si todavía tenemos devoluciones de llamada para procesar mensajes que podemos ver, ¿qué es mejor? Por el hecho de que los mensajes son un dato en la memoria. Puedes mirarlo con ojos y entender lo que ha venido aquí. Se puede agregar al marcador, obtener una lista de mensajes en un archivo de texto. Esto es más conveniente que las devoluciones de llamada.
Magnífico multinúcleo , gestión de memoria e
introspección dentro de un sistema de producción en
vivo .
Los problemas
Naturalmente, Erlang también tiene problemas.
Pérdida de rendimiento máximo debido al hecho de que ya no podemos hacer referencia a datos en otro proceso u objeto. Tenemos que moverlos, pero esto no es gratis.
La sobrecarga de copiar datos entre procesos. Podemos escribir un programa en C que se ejecutará en los 80 núcleos y procesará una matriz de datos, y asumiremos que lo hace correcta y correctamente. En Erlang, no puede hacer esto: necesita cortar cuidadosamente los datos, distribuirlos en un montón de procesos, realizar un seguimiento de todo. Esta comunicación cuesta recursos: ciclos de procesador.
¿Qué tan rápido o lento es? Llevamos 10 años escribiendo código Erlang. El único competidor que ha sobrevivido estos 10 años está escrito en Java. Con él, tenemos una paridad de rendimiento casi completa: alguien dice que somos peores, alguien que son. Pero tienen Java con todos sus problemas, comenzando con JIT.
Estamos escribiendo un programa que sirve a decenas de miles de sockets y bombea decenas de GB de datos a través de sí mismo. De repente, resulta que en este caso la
corrección de los algoritmos y la capacidad de depurar todo esto en la producción es más importante que los posibles bollos de Java . Se han invertido miles de millones de dólares en esto, pero esto no le da al Java JIT ninguna ventaja mágica.
Pero si queremos medir puntos de referencia estúpidos y sin sentido, como "calcular los números de Fibonacci", entonces Erlang probablemente será aún peor que Python o comparable.
La sobrecarga de la asignación de mensajes. A veces duele. Por ejemplo, tenemos algunas piezas en C en el código, y en estos lugares no funcionó en absoluto con Erlang. , , .
Erlang
, , . , ,
receive
send receive
. — , .
, , .
Python
. . , Python - .
,
. - Python, , 20 , 40.
,
. - , , Elixir, , .
Moscow Python Conf++ . , 6 4 . , , ) ) . Call for Papers 13 , 27 .