- El
async/awaitde Rust apunta a la concurrencia a gran escala para manejar decenas de miles de conexiones, pero choca con los objetivos de Rust de control de bajo nivel y verificación estática de lifetimes, lo que crea una experiencia de desarrollo distinta al Rust habitual - Los hilos y canales son suficientes para mucho software, pero en escalas como C10K el costo del enfoque de una conexión por hilo se vuelve alto, por lo que hacen falta tareas en espacio de usuario y planificación desde el runtime
- En Rust
async, los datos deben moverse comoSendo tratarse como referencias'static, y por la naturaleza contagiosa deasyncestas restricciones se repiten en todo el código Arcpuede resolver problemas de compilación, pero también vuelve más difusa la vida útil de objetos y recursos, y siguen apareciendo trampas comoasyncrecursivo, la diferencia entre future y task, y llamadas bloqueantes que atascan hilos del runtime- En Haskell o Go, el “código async” funciona como código normal y el runtime junto con el GC ocultan esas diferencias, por lo que en este tipo de programación el control explícito de Rust puede no traducirse solo en ventajas
Por qué hacen falta concurrencia y paralelismo
- Los programas rápidos tienen dos exigencias al mismo tiempo
- Deben usar varios núcleos de CPU para aprovechar toda la computadora
- Deben seguir haciendo otras cosas mientras esperan operaciones lentas, como transmitir mensajes por internet o abrir archivos
- El paralelismo es el problema de ejecutar código al mismo tiempo en varias CPU
- La concurrencia es una forma de dividir un problema en partes independientes
- No son lo mismo, pero cuando un programa se divide en piezas concurrentes, esas piezas pueden ejecutarse en paralelo y mantener ocupados los núcleos
Procesos, hilos y canales
- Una forma simple de construir un sistema concurrente es dividir el código en varios procesos
- El scheduler del sistema operativo ejecuta porciones de tiempo de los procesos listos sobre los núcleos de CPU disponibles
- Este modelo también se usa cuando se conectan comandos de shell con pipes
- El enfoque basado en procesos tiene un costo alto de comunicación entre procesos
- En muchas implementaciones, los datos deben copiarse a memoria del SO y luego recuperarse
- Se puede reducir ese costo con memoria compartida, pero eso debilita la ventaja de que el SO aísle los procesos entre sí
- Los hilos evitan ese overhead porque comparten la misma memoria, pero si se usan mal herramientas de sincronización como mutex, condition variable o semaphore, aparecen carreras de datos y deadlocks
- El modelo de Communicating Sequential Processes de Tony Hoare conecta hilos mediante colas o canales
- Los hilos no comparten memoria, así que se obtiene un aislamiento parecido al de los procesos
- Las entradas y salidas de cada hilo quedan expuestas a través de canales, lo que facilita razonar y depurar
- El propio canal cumple el papel de sincronización: si está vacío, el receptor espera; si está lleno, el emisor espera
- La biblioteca estándar de Rust incluye std::sync::mpsc::sync_channel
- Para mucho software basta con una combinación de hilos y canales, junto con herramientas como Rayon para paralelizar loops intensivos en CPU
Concurrencia en espacio de usuario y Rust async
- En problemas C10K, como un servidor web con decenas de miles de usuarios conectados al mismo tiempo, el enfoque de asignar un hilo por conexión llega a su límite
- En Linux, cada hilo tiene un control block de 4 kB, y cambiar de hilo requiere un context switch que entra al scheduler del sistema operativo
- Para la concurrencia a gran escala, algunos lenguajes crean y administran tareas en espacio de usuario
- El runtime agenda esas tareas sobre un pool de hilos del SO
- Normalmente el pool se configura para tener un hilo por núcleo de CPU y así maximizar el paralelismo
- A este enfoque también se le llama green thread, lightweight thread, lightweight process, fiber o coroutine
- Rust usa el modelo
async/awaitque se ve en C# o Node.js- Una
async fnno devuelve el valor directamente, sino un future o promise cuyo resultado se obtiene con.await
- Una
- Los future de Rust son muy pequeños y rápidos gracias a la planificación cooperativa y a su diseño stackless
- Rust intenta ofrecer la abstracción de future y al mismo tiempo prometer control de bajo nivel al programador
- Intenta verificar estáticamente en tiempo de compilación la vida útil de todos los objetos y referencias
- Un future divide el código y los datos a los que hace referencia en miles de fragmentos, y permite que se ejecuten en cualquier momento y en cualquier hilo según condiciones que solo se conocen después de empezar la ejecución
- Un future que lee datos de un cliente debería ejecutarse solo cuando haya datos disponibles en ese socket, pero las lifetime annotations no indican ese momento
- Rust no incorpora un runtime de future dentro del lenguaje y lo deja en manos de bibliotecas como Tokio
- El usuario gana libertad para elegir alternativas según su entorno
- Pero incluso si imagináramos un mundo en el que Tokio estuviera integrado en el lenguaje, las mismas reglas seguirían aplicando, así que en este punto es un detalle secundario
La presión que generan Send, 'static y Arc
- Para convencer al compilador, los datos deben moverse marcados como
Sendo pasar mediante referencias con lifetime'static - En código
asynces común que varias tareas compartan estado, así que mover datos sin copiarlos muchas veces no encaja bien - Las referencias también son complicadas, y no existe un equivalente de
thread::scopeque permita limitar la vida de un future a algo menor que “para siempre” asynces contagioso, así que una función que llama a una funciónasynctambién debe serasync- Por eso estos problemas de lifetime y movilidad no se resuelven en unas cuantas funciones, sino una y otra vez a lo largo del código
- Se puede cortar la cadena esperando en tiempo de ejecución a que un future termine con
block_on, pero eso no es componible y, si se anida, el runtime puede hacer panic
- Arc es una herramienta para manejar lifetimes dinámicos entre varios hilos, y permite pasar el borrow check y hacer que el código compile
- Pero usar
Arcde forma extensa también vuelve más difusa la vida útil de objetos y recursos- Ya no queda claro cuándo se liberarán recursos como memoria, archivos o sockets
- Se sufren pérdidas parecidas a las de un GC, sin obtener ventajas reales de un GC como mayor allocation throughput, menor fragmentación o evitar cycle leaks
Trampas adicionales de async Rust
- Las coroutines de Rust son stackless, así que el compilador convierte cada coroutine en una máquina de estados que avanza hasta el siguiente punto
.await- Una función
asyncrecursiva se convierte en un tipo definido de forma recursiva - Quien solo quiere llamarse a sí mismo tiene que hacer boxing manualmente o usar un crate como async-recursion
- Una función
- Un future no hace nada hasta que se le aplica await
- Una task comienza a ejecutarse en el pool de hilos del runtime y devuelve un future que indica su finalización
- No hay ningún mecanismo que impida llamar código bloqueante dentro de un future
- Tampoco hay nada que impida que esa llamada bloquee el hilo del runtime en el que cayó
- Eso entra en conflicto con el propósito central de usar
async
Diferencias con Rust normal, Haskell y Go
asyncRust tiene un sabor muy distinto al Rust “normal”- Tiene más trampas
- Es más difícil de entender y de enseñar
- El usuario queda frente a dos opciones
- Entender a fondo cómo funciona realmente la abstracción y escribir código complejo
- Esparcir elementos como
Arc,Piny'staticpor todo el código y esperar que funcione
- Incluso un equipo de desarrolladores con experiencia puede intentar usar Rust en un proyecto nuevo y quedar frenado por estos detalles
- En Haskell o Go, el “código async” es código normal
- Ambos lenguajes esconden la diferencia entre código bloqueante y no bloqueante detrás de un runtime pesado
- Los problemas de lifetime se delegan al garbage collection
- En este tipo de software concurrente a gran escala en espacio de usuario, la forma en que el runtime y el GC ocultan esas diferencias funciona como una ventaja pura
- Puede que Rust no sea una buena herramienta para software concurrente a gran escala en espacio de usuario, y quizá convenga usarlo en proyectos que no tengan ese tipo de exigencia
1 comentarios
Opiniones de Hacker News
Estoy escribiendo un cliente de metaverso de alto rendimiento en Rust, y actualmente tiene unas 40 mil líneas
El video demo está en https://video.hardlimit.com/w/tp9mLAQoHaFR32YAVKVDrz
Un metaverso bien hecho tiene que procesar contenido creado por usuarios casi en tiempo real, así que necesita 2 a 3 veces más VRAM que juegos similares, cientos de Mbps de ancho de banda para cargar assets desde el servidor, varios CPU y Vulkan para hacer renderizado y subidas a la GPU en paralelo
Esto no es una estructura de concurrencia “a escala web”, con pequeños servidores corriendo por separado en el mismo espacio de direcciones, sino una en la que se mueven juntos un hilo de render de alta prioridad, un hilo de actualización de eventos de red, hilos de carga y descompresión de assets, y varios hilos encargados de objetos móviles, LOD, limpieza de caché, etc.
En Rust uso bastante bloqueo sin estado global aparte de constantes; los canales los uso donde encajan, y el árbol principal de objetos lo maneja sobre todo el hilo de actualización con propiedad única. Las conexiones de objetos gráficos se administran con conteo de referencias
Arc, y subo mallas y texturas a la GPU mediante Rend3/WGPU/VulkanSi lo hubiera hecho en C++, habría estado peleando con crashes todo el tiempo, pero en Rust tengo crashes relacionados con memoria más o menos una vez al año, y por lo general fueron por código
unsafede otros. En mi código prohibíunsafe; es difícil lograr que compile, pero una vez que lo hace, suele “simplemente funcionar”, lo cual me parece mucho mejor que depurar concurrenciaTambién tengo quejas. Rust es fuerte contra las carreras de datos, pero no puede impedir los interbloqueos, así que hace falta un analizador estático que rastree el orden de los bloqueos a lo largo de las rutas de llamada.
asyncno encaja con trabajos centrados en cómputo ni con varios hilos de distintas prioridades, pero aun así se va colando como dependencia. Las estructuras comunes con propiedad única y referencias inversas son demasiado difíciles sinRcyWeak, y el sistema de traits también es complejo, por lo que en partes de procesamiento de assets donde la orientación a objetos sería natural aparece código duplicadoLas crates centrales de gráficos tampoco están todavía muy maduras. Eso de que “Rust tiene 5 juegos y 50 motores de juegos” no es un problema del lenguaje, sino del ecosistema, y aun comparándolo con https://gamedev.rs/, parece que todavía falta desarrollo serio de juegos en Rust. Para desarrollo profesional de juegos con fechas de entrega, el ecosistema de juegos en Rust todavía no está listo; lo veo como algo que necesitaría aproximadamente 5 personas trabajando un año más
asynctienen problemas, en general las ventajas pesan mucho másComo
lockdepde Linux: analizar qué bloqueos se toman mientras ya se tiene otro, y avisar sobre combinaciones peligrosas incluso antes de que el programa se detenga de verdad. Con bloqueos complejos harían falta anotaciones como “esta clase de bloqueo siempre se toma en orden de direcciones”, pero parece implementableFuera del acceso a datos todavía pueden aparecer condiciones de carrera: https://news.ycombinator.com/item?id=23599598
Se pueden crear varios pools de hilos y enrutar los futures al lugar adecuado, o escribir un loop de eventos propio que tome trabajo de varias colas de eventos con distintas prioridades. El segundo enfoque, si el tiempo de ejecución de las tareas está acotado, puede dar garantías soft real-time a las tareas de alta prioridad mientras permite que avancen las de baja prioridad incluso con la CPU al 100%
Con
asyncRust estoy en un punto raroSi usas un montón de
Arc,RwLocky estado compartido, todo se vuelve sucio, y es cierto eso de que, sobre todo cuando'staticempieza a propagarse por todas partes, infecta todo como las funciones de colores. Antes intenté ponerArcy manejar préstamos con lifetime de forma inteligente, y terminó siendo un desastrePero Rust también tiene canales. El código que estoy escribiendo ahora consiste en su mayoría en unas cuantas tareas que atienden canales, miran los mensajes entrantes y, si hace falta, colocan mensajes para otras tareas en los canales correspondientes. No comparto objetos. Si varias tareas necesitan un objeto grande, lo pongo dentro de una tarea que envía por mensaje los resultados de las consultas relacionadas, o hago que cada tarea cree su propia copia dentro del flujo de mensajes
Aun así hay demasiados artículos sobre cómo usar
Arcy cómo manejar lifetimes. Si estás implementando un runtimeasync, será necesario, pero no entiendo bien por qué el usuario promedio de una librería debería concentrarse tanto en esoasyncno significa necesariamente multihilo, y si esasyncdentro del mismo hilo, no hay compartición, así que tampoco hace falta poner palabras clave mágicas a todo lo compartidoCuando se cruza entre hilos, se envían señales por canales en vez de tener mucho estado compartido. Si hay algún estado global realmente necesario, se crea una pequeña estructura que envuelva mecanismos de acceso exclusivo como
Arc/RwLock, y para quien la llama se ve como una simple llamada a funciónTampoco entiendo bien la preocupación por
Send+Sync. En mi experiencia, la mayoría de las cosas son fácilmenteSend+Sync, y las que no lo son eran cosas que no deberían o no podían serlo. A veces uno quiere escribir código sin pensar en los detalles, pero si necesitas concurrencia y paralelismo eficientes, los microsegundos y el throughput importan, y entonces hay que escribir código para computadoras reales de forma correctaPero codificar de esta forma es muy distinto de JavaScript
async, que se siente como código síncrono con green threads encima. La gente intenta escribir código de la forma a la que está acostumbrada, y por eso parece terminar yéndose por el camino deArcyRwLocken RustEstructurar el problema como datos que fluyen entre tareas, conectadas por colas y evitando estado compartido, es una mejor manera de manejar multihilo sin importar el lenguaje que uses
asynces, en la práctica, Rust mucho más difícil, y es una pena que se haya impuesto casi a todos cuando probablemente solo un 1% de los proyectos realmente lo necesita.Dicho eso, en ese 1% es realmente excelente. Para servicios que procesan como parte central una enorme cantidad de llamadas de red, como linkerd o nginx; para juegos que ejecutan cantidades enormes de tareas livianas; o para casos de embebidos donde se necesita concurrencia cooperativa, async Rust se vuelve un arma poderosa.
La mayoría del código a nivel de sistemas y aplicaciones no necesita E/S asíncrona. Para una app REST alcanza con un pool de hilos y, aun cuando se necesita
async, lo habitual es que lo correcto sea un modelo mixto: limitarlo a partes pequeñas como la red y conectar el resto con hilos y canales.La comunidad de Rust ha usado
asyncen todos lados de forma demasiado indiscriminada, al punto de que el Rust con E/S bloqueante, que ofrece una mejor experiencia de usuario, quedó como ciudadano de segunda en el ecosistema. También en frameworks web hay varios frameworks asíncronos bien diseñados como Axum y Warp, mientras que del lado bloqueante las opciones son mucho más limitadas, comotiny_http,rouilleyastra.Al elegir corrutinas sin stack aparecieron
async/awaity el problema de las funciones de colores, y surgió la fricción que menciona el artículo. Go no tiene este problema porque usa corrutinas con stack.Rust también consideró al principio las corrutinas con stack, pero concluyó que requerían un runtime preventivo de corrutinas y que el costo era alto, así que fue por el modelo sin stack. Pero la mayoría no usa
asyncRust sin runtime, sino Tokio, y Tokio hace en la práctica casi todo lo que hacía el runtime que se quería evitar.Por eso muchos usuarios de
asyncRust terminan con lo peor de ambos mundos. En embebidos algunos usanasyncRust con un runtime muy liviano, pero son pocos, e incluso ellos no están completamente convencidos.reqwest, eso traeh2, y eso a su vez traetokio.asynccuando la plataforma soporta hilos virtuales.Como usuario de Java, estoy intentando abandonar por completo el paradigma asíncrono y reescribir el código con un modelo bloqueante sobre hilos virtuales donde bloquear está bien.
asyncse ha extendido a demasiados crates, así que todo el programa termina siendoasynco, como mínimo, dependiendo de Tokio para muchas cosas.Si quieres un servidor web, la actitud parece ser “
async + tokioo vete”, y con los conectores SQL el ambiente es que, si no quieres asincronía, tienes que escribirlos tú. Cada quien resuelve los problemas que traeasyncde una manera distinta, y cosas como los closuresasyncse sienten como abrirle al compilador las puertas del infierno.Está bien que Rust como lenguaje y el compilador ayuden a resolver problemas, pero no alcanza con que el ecosistema esté cerca de “si no es
async, hazlo tú mismo”.futurestuvieran mejores primitivas asíncronas, se podría haber reducido mucho el dolor.Harían falta cosas como traits que deban implementar los ejecutores, o un ejecutor bloqueante básico para ejecutar código asíncrono desde código síncrono. Hoy, solo crear una biblioteca que soporte varios runtimes asíncronos ya es un calvario, así que al final se termina soportando solo Tokio o, con suerte, agregando
async-std.No soy especialista en Rust
async, pero este mes, tras escribir varios miles de líneas de Rust síncrono, lo que sentí es que cuandorustcvuelve difícil cierto enfoque, normalmente hay una buena razón para eso y existe una forma mejor de obtener un resultado parecido.Si estás aprendiendo el lenguaje, recomendaría primero acostumbrarte al código síncrono común, a los bucles y condicionales, y a las reglas de préstamo.
asynctodavía está evolucionando bastante, no solo en la implementación, sino también en un plano filosófico: “qué es la asincronía y cómo debería verse para el usuario”.El compilador depende mucho de los traits, pero la funcionalidad de los traits para manejar
asyncno está estabilizada. Por ejemplo, existe trabajo como https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-i....Si las capacidades asíncronas de los traits no están estabilizadas, atacar que el código asíncrono de Rust todavía no se vea bonito es parecido a criticar un borrador inicial de un libro que eventualmente estará terminado.
También me preocupa cómo evitar que lo asíncrono se propague por toda la base de código.
La idea actual es una arquitectura en la que un hilo de E/S divide los eventos del sistema de
liburingoepollen dos etapas, “submit” y “handle”, y los envía a otros componentes. Por ejemplo, si creas untcp-connection, puedes suscribirte a eventos asíncronos como “listo para escribir” o “listo para leer”, y el evento de listo para escribir toma datos de un búfer llenado mediante un mutex normal y los envía conEPOLLOUT/io_uring_prep_writev.Para transmitir eventos entre hilos se puede usar un búfer circular de múltiples productores y múltiples consumidores con el patrón LMAX Disruptor. Los hilos de aplicación o el pool de hilos tienen cada uno su propio event loop y procesan ese búfer circular.
También estoy trabajando en una sintaxis para expresar el orden de disparo de eventos asíncronos; se parece a un pipeline de Bash y la llamo
statelines:initialstate1 initialstate2 = state1 | {state1a state1b state1c} {state2a state2b state2d} | state3La vida útil de
Arcno es algo desconocido, sino que queda determinada por dónde y cómo se lo conservaLa desconexión de este artículo parece venir de que el autor intenta forzar sobre Rust un modelo mental previo, como el de la recolección de basura, en lugar de aprender Rust y trabajar según el lenguaje. Es una trampa común al aprender un lenguaje nuevo, pero Rust hace que uno tropiece con ella especialmente seguido
Pero eso es casi lo opuesto al objetivo del verificador de préstamos, que busca restringir estáticamente la vida útil de los objetos en tiempo de compilación
En la práctica, fue casi lo contrario. Después de hacer programación de sistemas con C, C++ y Rust durante unos 10 años, en mi trabajo actual empecé a usar mucho Haskell, y me abrió bastante los ojos ver que un runtime grande del lenguaje y la recolección de basura no son monstruos en ciertos dominios de problemas
asyncobstaculiza optimizaciones que el compilador puede hacer en código no asíncronoLa parte sobre lidiar con
Weakparece apuntar a construir una estructura de ownership compleja, algo que no es fácil en Rust en general. Yo uso punteros inteligentes débiles muy rara vezCasi no se mencionan los canales, que son la herramienta principal para hacer que distintas partes de un programa se comuniquen, ya sea en código asíncrono o al conectar código asíncrono y síncrono. También existen abstracciones de señalización como
Notifyy semáforosLos mutex son lentos y tienden a convertirse en cuellos de botella, y el estado compartido se vuelve complejo rápidamente. Esto se sabe desde hace mucho. El problema podría estar, para empezar, en una estructura como
BIG_GLOBAL_STATIC_REF_OR_SIMILAR_HORRORLa observación de que no se puede impedir llamar código bloqueante desde un contexto asíncrono es válida, pero si hace falta se puede manejar de forma relativamente razonable con algo como
tokio::spawn_blockingEs muy probable que el autor sepa qué es
Arcy cómo funciona; el punto se acerca más a que en Rustasyncse termina usandoArcmucho más seguido que RAII normal, en comparación con el código síncronoSi el 90% de los objetos del programa se gestionan con conteo de referencias, quizá convenga usar recolección de basura por trazado en lugar de pagar el costo de muchas asignaciones y liberaciones pequeñas en el heap y de operaciones atómicas. El ejemplo del tutorial de Tokio también apunta en una dirección similar: https://tokio.rs/tokio/tutorial/shared-state
Me da curiosidad si una recolección de basura por trazado real en Rust podría hacer significativamente más rápidas las aplicaciones asíncronas típicas, como servidores HTTP: https://manishearth.github.io/blog/2015/09/01/designing-a-gc...
Arcno es aleatoria, sino estáticamente desconocidaArcde Rust se puede mover o tomar prestado, y también se puede usar sin tocar el conteo de referenciasEn muchos casos es mucho más barato que los objetos de lenguajes con conteo de referencias implícito
Me gusta Rust, pero
asynces un desastre, y no se puede escribir código asíncrono como si fuera código síncronoCada vez estoy más convencido de que mezclar ambos es una mala idea, y quizá el enfoque de Go —dejar todo síncrono y ofrecer solo una primitiva de canales
async— sea el correctoAhora mismo estoy cableando lógica para llamar métodos síncronos desde una estructura que implementa
Future, y es un desafío bastante interesante. Se puede hacer que las abstracciones asíncronas de costo cero sean algo fáciles para el usuario, pero el dolor lo termina cargando el desarrollador de bibliotecasasyncsin duda también es doloroso para el usuario final, y se siente como usar un lenguaje aparte al que le faltan funciones centrales de Rust, como las vidas útiles y los tipos explícitos, conPinespolvoreado por todos ladosComo no se pueden ejecutar fibras con alcance, uno termina poniendo
Arcpor todas partes;Pines difícil de usar sinunsafe, y un cambio minúsculo en una función asíncrona puede convertir todos los futures de una base de código en!Sendasync. Habrá que ver cómo evolucionaAsync Everything es un mal lenguaje
async/awaitfue una idea terrible para solucionar el problema de que JavaScript no tenía hilos bloqueantes de verdad, y ahora se está agregando a todos los lenguajes. Va a partir en dos el lenguaje y el ecosistema de bibliotecas, y seguirá causando dolor durante mucho tiempoCualquiera que haya hecho multithreading fuera de JavaScript sabe que los actores o los procesos secuenciales comunicantes son la mejor forma de abordar el multithreading
En la tesis de Joe Armstrong también se explica que la única forma de entender programas multihilo es escribir código estrictamente secuencial para cada hilo y no mezclar en un solo lugar el código de varios hilos. Para minimizar la brecha conceptual, una actividad concurrente real del problema debe corresponder exactamente a un proceso concurrente del lenguaje de programación: https://erlang.org/download/armstrong_thesis_2003.pdf
También es buena la crítica a
async/awaitde Ron Pressler, quien implementó Project Loom de Java: https://www.youtube.com/watch?v=oNnITaBseYQÉl era bastante ambivalente respecto de JavaScript en sí; su objetivo principal era encontrar una abstracción para manejar el bucle de eventos de E/S de
epoll()sin querer sacarse los ojos. Antes había probado muchas otras formasasync/awaiten realidad no empezó en JavaScript, sino en C#Anders Hejlsberg, de C#, también creó TypeScript, y funcionalidades de TypeScript como las clases, las funciones flecha y
async/awaitterminaron entrando en ES6+Creo que fue una gran solución para JS/TS, con su event loop de un solo hilo. Pero cuanto más de bajo nivel es un lenguaje, peor funciona como abstracción, así que la mayoría de las críticas a
asyncRust que salen de ahí son válidasEl artículo explica bien la complejidad y las dificultades de
asyncRust, pero también es importante que una de las filosofías centrales de Rust es la seguridad de memoria sin sacrificar rendimientoLos patrones asíncronos de Rust, en particular la forma en que hacen que el compilador garantice la seguridad de los datos, muestran bien esta filosofía. Hay complejidad, pero tiene valor como un modelo de concurrencia más seguro que obliga a los desarrolladores a pensar a fondo en los datos y en el flujo de ejecución
Puede que Rust no sea la respuesta para todas las aplicaciones grandes de espacio de usuario con mucha concurrencia, pero en sistemas donde la robustez y la seguridad son la máxima prioridad, las concesiones pueden estar justificadas. A medida que evolucione el ecosistema, es probable que aparezcan más abstracciones y bibliotecas que reduzcan ese dolor
Escribo mucho Rust lock-free basado en
async. El problema principal es que los futures de Tokio son'static, y eso viene de un error de diseño profundamente incrustado en el ecosistema de Rust: la decisión de que las fugas de memoria son segurasPor eso no se puede garantizar estáticamente que un future se limpie correctamente. Cuando se crea una tarea asíncrona, si alguien se olvida del future con
std::mem::forget, el borrow checker no puede saber que las referencias que ese future pasó transitivamente siguen vivasEn vez de repartir
Arcpor todos lados, uso este crateunsafe: https://docs.rs/async-scoped/latest/async_scoped/Con esto se atrapa el 99% de los bugs que habría creado en C++, así que es una concesión razonable. También hay trabajo en curso para implementar futures no-
'staticde forma segura, y espero que tenga éxitoOtro gran problema es que
async traitactualmente exige futures boxeados, lo que agregamalloc/freeen cada frontera de llamada de función, pero está en la hoja de ruta para corregirse este añoEl consejo de “simplemente usa canales” también dispersa el flujo de control por todos lados en bases de código grandes. Los canales se sienten como un
GOTOmoderno; yo también los uso, pero no suelo usarlos cuando solo quiero ejecutar varias cosas en paralelo y esperar a que terminen'static, sino que los únicos futures que se puedenspawnpara aprovechar la concurrencia del runtime son los'staticPara que un future pueda ser
poll(), debe estarPin, y unT: !Unpinque estáPinfinalmente debe llamar aDrop: https://doc.rust-lang.org/std/pin/#drop-guaranteeLos futures generados por la funcionalidad
asyncdel compilador tienen esta propiedad, y también se puede ponerPhantomPinneden futures manuales. Gracias a esto, después de que fueronpoll(), las travesuras conmem::forgetpueden asumirse como comportamiento indefinido, y se vuelven posibles bibliotecas de futures intrusivos y autorreferenciales: https://docs.rs/futures-intrusive/latest/futures_intrusive/Un future puede seguir vivo y fugarse por culpa de un
Arc/Rc, pero desde el punto de vista de quien desarrolla una biblioteca, eso no se puede distinguir razonablemente del uso normal, o no hace falta preocuparse demasiado por elloRc, o bien poner límites de traitsunsafeinfecciosos