Aprender sobre Pin en Rust
(without.boats)Pinde Rust es un elemento base introducido para manejar de forma segura el estado autorreferencial dentro de los Future creados por async/await- Un Future async guarda estado en cada punto de await, por lo que puede convertirse en un tipo autorreferencial, donde un campo dentro de un objeto referencia a otro campo del mismo objeto
- Los diseños con move constructor, offset pointer y
?Moveno fueron adoptados por problemas de costo de seguimiento en runtime, viabilidad de compilación y compatibilidad hacia atrás con las API existentes, respectivamente - El diseño final es
Pin, que envuelve un puntero y pone el destino en un pinned typestate; gracias al auto traitUnpin, la mayoría de los tipos pueden moverse como antes - La dificultad de
Pinno viene tanto del concepto de inmutabilidad en sí, sino de las limitaciones de ser un tipo de biblioteca; reborrowing,Pin::set, pinned projection y la interacción conDropreducen mucho su usabilidad
El problema que hace necesario a Pin
- En el ecosistema async de Rust,
Piny pinning son piezas fundamentales, pero siguen siendo un área difícil y con muchos malentendidos para quienes aprenden async Rust - El objetivo de
Pinno es permitir que los usuarios creen directamente tipos autorreferenciales usando solo Rust seguro- Su propósito es permitir manipular de forma segura los Future autorreferenciales generados por el compilador a partir de funciones async, o los tipos autorreferenciales creados con código unsafe por runtimes como tokio
- En el ejemplo
async fn bar, en el puntofoo(&mut z).await, deben almacenarse juntos dentro del mismo estado del Future tantozcomo el FutureFooque referencia az- En ese momento, un campo dentro del objeto Future referencia a otro campo dentro del mismo objeto
- Ese tipo de Future se convierte en un tipo autorreferencial
- Si el objeto se mueve después de entrar en ese estado, la referencia interna apuntará a la ubicación de memoria anterior, que podría ser memoria muerta o reutilizarse para otro valor
- Antes de
Pin, en Rust se podía mover un objeto si se tenía ownership o una mutable reference, por lo que hacía falta una forma de expresar que no debía moverse después de cierto punto
Enfoques que no lograron ser la solución
-
move constructor
- Un move constructor es una forma de ejecutar código, como un destructor, cuando un valor se mueve, para corregir los punteros autorreferenciales a la nueva ubicación
- En Rust, los punteros no necesariamente están solo “dentro” del valor que se mueve; por ejemplo, también pueden estar dentro de un vector de punteros que apuntan a su propio estado
- Para rastrear todos esos punteros, al final haría falta una administración de memoria en runtime parecida a garbage collection
- Rust decidió desde temprano no tener move constructors, y mucho código unsafe depende de la suposición de que los valores pueden moverse solo mediante copia de memoria
- Agregar move constructors después sería un breaking change
-
offset pointer
- Un offset pointer compila las autorreferencias no como referencias normales, sino como offsets respecto de la dirección del objeto autorreferencial
- En tiempo de compilación no siempre se puede determinar qué referencia es autorreferencial
- Según la rama, el mismo valor podría apuntar hacia dentro de su propio objeto o hacia fuera
- Para manejar esto, habría que compilar las referencias en una forma similar a un enum de offset y reference, lo que en la época del trabajo de async/await se consideró poco realista
Requisitos del pinned typestate
- Un Future autorreferencial no tiene que ser siempre inamovible desde el inicio; durante su ciclo de vida puede moverse libremente hasta cierto punto, y a partir de un punto específico ya no debe moverse
- Mientras se combina el Future con otros Future, debe poder moverse
- Una vez colocado en la ubicación donde vivirá durante el polling, ya no debe moverse
- El modelo de Ralf Jung agrega, después de los typestates existentes “owned” y “shared”, un tercer estado para Future autorreferenciales: pinned typestate
- Cuando un objeto entra en pinned typestate, nunca más debe moverse
- Más precisamente, no se debe invalidar la memoria de ese objeto sin ejecutar primero su destructor
- En la práctica, puede verse como el requisito de no trasladar el objeto a una nueva ubicación
- La mayoría de los tipos no puede contener autorreferencias, así que pinned typestate no tiene un significado especial para ellos
- Para esos tipos, lo deseable es poder liberarlos de la restricción de pinning y volver a moverlos
- El modelo formal detallado de pinned typestate está explicado en A Formal Look at Pinning, de Ralf Jung
Por qué falló el diseño de ?Move
- Antes de
Pin, se intentó un diseño basado en un nuevo trait llamadoMove- La mayoría de los tipos implementaría
Move - Los tipos que pudieran contener autorreferencias no implementarían
Move - Al crear una referencia a un valor cuyo tipo no implementara
Move, ese valor entraría en pinned typestate y ya no podría moverse
- La mayoría de los tipos implementaría
- Este enfoque era intuitivo porque garantizaba seguridad vinculando el momento de creación de una referencia con la transición a pinning
- De hecho, llegó a implementarse en una rama del compilador
- La limitación fundamental es que hay casos en los que se quiere referenciar temporalmente un valor que más adelante será autorreferencial, pero todavía no se quiere hacer pinning
- Por ejemplo, podría quererse guardar un valor temporalmente en
Optiony luego extraerlo conOption::take
- Por ejemplo, podría quererse guardar un valor temporalmente en
- El problema más grande era la compatibilidad hacia atrás
Moveno podía convertirse en un auto trait- Esto se debía a que ya existían API estables, como
mem::swap, que asumen que siempre se puede mover un valor desde una mutable reference
- Agregarlo como
?Movetampoco era compatible hacia atrás debido a los associated types- El lugar donde se agrega un bound
?Traita un associated type es la definición del trait - Si se relaja el bound de un associated type en un trait existente, puede romperse código que dependía de ese bound
- Muchas operaciones básicas están ligadas a associated types: el associated future type de
IntoFuture, elTargetdeDerefMut, tipos de retorno de funciones, items de iterators, valores de retorno del index operator, valores de retorno de operadores aritméticos, etc.
- El lugar donde se agrega un bound
- Tampoco era fácil resolverlo con editions
- Para que crates de distintas editions puedan combinarse entre sí, la interfaz de los traits debe mantenerse igual
Diseño de Pin
- El diseño final expresa el pinned typestate no como una propiedad del tipo del objeto, sino como un estado creado por un puntero especial
Pines un wrapper type que envuelve un puntero- También puede envolver tipos de referencia built-in
- También puede envolver smart pointers definidos en bibliotecas, como
Box
Pinpone el destino al que apunta ese puntero en pinned typestate, y ese destino ya no debe moverse- Para minimizar los cambios, este diseño se implementa como una API de biblioteca, no como una funcionalidad del compilador
- El código que realmente necesite modificar un objeto pinned debe acceder mediante una API unsafe
- En ese momento, debe ofrecer la garantía de que el objeto no se moverá a través de una mutable reference normal
- Como para la mayoría de los tipos no hay una diferencia significativa entre el estado pinned y el estado normal, se agregó el auto trait
Unpin- Si un tipo no puede ser autorreferencial, se puede obtener una mutable reference desde un pinned pointer sin usar unsafe
- Un objeto que implementa
Unpinpuede moverse fuera dePinde forma segura
- Como el pinning solo se aplica a pinned pointers, las referencias normales unpinned siguen funcionando incluso con tipos que no son
Unpin - Hay más explicaciones en la documentación estándar del tipo
Piny del módulopin - La mayor ventaja de este diseño fue que se pudo agregar sin romper código existente
- Las API que pueden mover datos referenciados, como
swap, necesitan una mutable reference - Si un objeto se fija con
Pin, ya no se pueden llamar esas API sobre ese objeto - Como el pinned typestate solo se aplica a una pinned reference especial, no rompe las garantías de compatibilidad hacia atrás de todo el lenguaje Rust
- Las API que pueden mover datos referenciados, como
Problemas de usabilidad de Pin
Pincumplió los requisitos de forma compatible hacia atrás, pero en cuanto el usuario lo manipula directamente aparece un precipicio de complejidad- Una explicación es que modificar un objeto pinned requiere código unsafe
- Sin embargo, no conviene exagerar este problema
- Con
Pin::setse puede asignar de forma segura a un objeto pinned - En la práctica, el código que necesita modificar objetos pinned suele ser código generado por el compilador al bajar funciones async a Future, y rara vez lo escribe directamente el usuario
- La explicación de que
Pines difícil porque se comporta de forma condicional tampoco es la causa principal- Rust tiene funcionalidades que se comportan distinto según condiciones y aun así facilitan la comprensión
- Los non-lexical lifetimes son un caso donde el lifetime puede terminar en puntos distintos según la rama condicional
- El problema central es que
Pines un tipo puramente de biblioteca, mientras que los tipos de referencias normales son tipos incorporados en el lenguaje y reciben mucho soporte sintáctico y sugar- Funcionalidades que funcionaban naturalmente con referencias normales desaparecen con pinned references
- El mental model que el usuario construyó basándose en el comportamiento de referencias aceptado por el compilador se rompe con pinned references
reborrowing y Pin::as_mut
- Una mutable reference normal
&mut Tno implementaCopy, pero puede pasarse varias veces como el mismo argumento- Esto se debe a que el compilador realiza implícitamente reborrowing, como si pusiera
&mut *xen lugar dex
- Esto se debe a que el compilador realiza implícitamente reborrowing, como si pusiera
Pin<&mut T>es un tipo normal de biblioteca y no implementaCopy, por lo que no tiene esa comodidad- Si se usa
Pin<&mut T>más de una vez, puede aparecer un error de uso del valor después de un move, o un error de lifetime más difícil de entender - Hay que llamar explícitamente a
Pin::as_mutpara hacer reborrow
- Si se usa
- Con una mutable reference normal se puede asignar directamente mediante dereference y el assignment operator, pero con
Pinhay que aprender el métodoset- La razón por la que proliferan estas API especiales es que
Pines un tipo de biblioteca sin soporte sintáctico del lenguaje
- La razón por la que proliferan estas API especiales es que
pinned projection y Drop
- Pinned projection es el problema de obtener, desde una pinned reference a un objeto, una pinned reference a un campo de ese objeto
- Projection significa acceder desde un objeto hacia un campo
- Como es mucho más difícil que el acceso a campos con referencias normales, se usan crates de terceros como pin-project-lite
- Estos crates obligan a aprender nuevas API complejas, incluidas macros
- La peor interacción ocurre entre pinned projection y el trait
DropDrop::droprecibe una mutable reference normal- Si un tipo tiene un campo autorreferencial, se hace pin project sobre ese campo y se lo pollea, y luego el destructor mueve ese campo, puede romperse la garantía de pinning
- Por ejemplo, si dentro del destructor se fija ese future en el stack y se lo pollea, se viola la garantía de pinning existente
- Crates como
pin-project-litemanejan este problema restringiendo la capacidad de definir destructors- En la práctica funciona, pero agrega complejidad que hay que documentar al explicar las garantías de pinning
- Como
Dropya era stable antes dePin, hizo falta una solución indirecta
Evaluación actual y próxima línea de mejora
Pinpermitió compilar funciones async que contienen referencias arbitrarias como objetos autorreferenciales seguros- Las referencias son una parte importante de la forma básica en que los usuarios de Rust escriben código; sin esto, la usabilidad de async/await habría sido mucho menor
- Al mismo tiempo,
Pinse agregó de una forma completamente compatible hacia atrás con Rust existente Pinse volvió un bloque fundamental del ecosistema que sostiene servicios de red de alto rendimiento y otros casos de uso de programación asíncrona- Pero trabajar con pinned references es mucho más difícil que trabajar con ordinary references, y
Pinefectivamente crea un precipicio de complejidad - El concepto clave de la próxima línea de mejora es pinned places
1 comentarios
Opiniones en Hacker News
Siempre he pensado que Pin es difícil de entender porque no está explicado con claridad en la documentación oficial.
En particular, abundan explicaciones como “Pin garantiza que un objeto nunca se moverá”, pero eso no es cierto.
Solo es válido cuando el objeto no es
Unpin; la mayoría de los objetos comunes sonUnpin, así que normalmente Pin no hace nada.Me tomó muchísimo tiempo entender esto, y creo que el conjunto de tipos
Tpara los que Pin realmente tiene significado es bastante específico y extraño, pero la documentación no lo enfatiza lo suficiente.Claro que los tipos que realmente se tratarán como fijados, futures y streams, tienen mucha más probabilidad de ser ese tipo de objetos especiales.
Aun así, creo que la documentación ha mejorado mucho en los últimos años.
Cuando la revisé mientras escribía este artículo, me sorprendió que se enfocara en puntos bastante adecuados; recuerdo que alrededor de 2019 estaba mucho más inclinada hacia una especificación de contrato que habría encajado mejor en la documentación de referencia de Rust que en la documentación de la API de
std.Creo que la razón por la que a los usuarios les cuesta Pin es que Pin por sí solo no tiene significado.
Es distinto de otros wrappers del lenguaje, y la excepción sería algo como
AssertUnwindSafe, que casi nadie usa para su propósito original.Cuando tienes
Pin<&mut InnerType>, nada en Pin dentro del lenguaje o de la biblioteca estándar te dice qué puedes o no puedes hacer.Solo si
InnerTypese declara comoUnpin, eso significa que puedes hacer todo lo que podrías hacer con un puntero normal.En cambio, Pin funciona con un enfoque de “trae tú mismo el significado”: el proveedor de
InnerTypeagrega métodos y APIs internamenteunsafepara manipular de forma segura objetos fijados.El propósito de Pin en sí es ofrecer un puntero con menos capacidades intrínsecas, como reemplazar mediante
&muto sacar y mover desde unBox, para que el tipo interno pueda permitir de forma segura capacidades adicionales sobre esa base.Creo que esa ambigüedad de significado es lo que más confunde a la gente, y a mí también me tomó bastante tiempo entenderlo.
Los conceptos de campos estructurales y no estructurales son solo un mecanismo para permitir patrones de acceso comunes, como “este campo es datos normales, pero aquel campo contiene un objeto que quiere estar fijado por sí mismo”.
Unpin, significa que el destino de este puntero no puede volver a moverse jamás.Más precisamente, significa que no puedes invalidar el destino sin ejecutar su destructor, y esa es la razón por la que moverlo es problemático.
Si renuncias a ciertos derechos, obtienes otros, como el derecho a almacenar valores autorreferenciales.
Los contratos entre componentes suelen funcionar así.
Del mismo modo, si renuncias al derecho de modificar a través de una referencia, al mismo tiempo puedes hacer que esa referencia tenga alias.
Cada vez que pienso en esto, aunque es un tema completamente distinto y mucho más pesado, me viene a la mente una frase de la película Lincoln: “Si obedecemos la ley, Alex, hasta el punto de perder la libertad —por ejemplo, la libertad de oprimir— quizá descubramos otras libertades que antes no conocíamos”.
Dicho eso, estoy de acuerdo en que, desde el punto de vista educativo, es un problema que en código seguro no puedas usar directamente esos derechos.
Porque es difícil mostrar con facilidad qué se puede hacer con una referencia fijada, aparte de “llamar al método
pollque generó el compilador”.Llevo varios años desarrollando profesionalmente en Rust, pero sinceramente no entiendo tan bien Pin.
Conozco la teoría, pero no tengo mucha intuición sobre cuándo debería usarlo.
En la práctica, usar Pin se parece más a “probé algo, el compilador se quejó, fijé unas cuantas cosas y entonces compila”.
En la programación diaria todavía no ha sido un obstáculo que realmente me obligue a sentarme a entenderlo en profundidad.
unsafey agradece que la gente inteligente del compilador ya lo resolvió todo”.En cambio, en C++ solía caminar a menudo por la orilla de las “cosas que no entiendo pero que obligatoriamente tengo que usar”, hasta que me devoraba un cocodrilo.
Al enseñar, para dejar claro que los elementos
Unpinno se ven afectados porPin, sería bueno usar una analogía de la vida real con una herramienta creada para mantener algo en su lugar, pero que aun así no lo afecta.Los ganchos de velcro no se adhieren a una superficie lisa:
Pin→ velcro,Unpin→ superficie lisa.Un imán no afecta a los materiales no magnéticos:
Pin→ imán,Unpin→ no magnético/vidrio/latón.El pegamento no se adhiere a una superficie antiadherente:
Pin→ pegamento,Unpin→ antiadherente.Así queda claro que el “velcro” fija un objeto en su lugar, pero si el objeto es “liso”, el mecanismo del velcro no lo afecta.
Pensando en el estilo de nombres del ecosistema Rust, habría sido hermoso que hubieran nombrado el trait en torno a imanes y materiales no magnéticos.
Me parece que
Unpinsignifica que el objeto está listo para ser fijado en cualquier momento.Leí el artículo anoche, pero ya olvidé si la fijación requiere un paso de ajuste.
Por eso veo
T: Pin + !Unpincomo algo parecido a una hoja de papel que solo se puede fijar con grapas, yT: Pin + Unpincomo un cuadro con argolla, que puedes colgar en un clavo y luego volver a bajar sin romper la argolla.El término “identidad de valor” no está definido en ninguna parte de este artículo y tampoco lo pude encontrar en la documentación de Mojo, así que no queda claro en qué se basa Modular para decir que Mojo resuelve el problema que Pin intenta resolver.
Tampoco digo que yo sepa la respuesta, pero me viene a la mente una excelente charla de Dave Abrahams, quien trabajó con Chris Lattner en la semántica de valores de Swift.
El título de la charla es “Value Semantics: Safety, Independence, Projection, & Future of Programming”.
[0] https://www.youtube.com/watch?v=QthAU-t3PQ4
Rust tiene referencias como tipos de primera clase, mientras que Swift y, según veo, Mojo solo permiten referencias como mecanismo de paso de parámetros.
Mojo parece haber extendido los parámetros
inoutde Swift para incluir también un mecanismo de paso por referencia inmutable.Si no se permite almacenar referencias dentro de objetos, entonces no se puede implementar el tipo de código que Rust compila, así que eso sí resuelve el problema de las “estructuras autorreferenciales”.
Pero lo que el párrafo citado dice sobre Mojo no habla en absoluto de eso, así que resulta bastante confuso qué quiere decir.
A mi parecer, el problema es que si tienes una referencia
&muta un valor, puedes mover ese valor con cosas comomem::swap/replace.Pero en la práctica rara vez hace falta hacerlo.
Si eso no estuviera permitido, creo que tener una referencia
&muta valores autorreferenciales habría sido completamente seguro.Podría haber existido una forma de optar explícitamente por mover a través de una referencia solo cuando fuera necesario, y quizá si
swapyreplacese hubieran hechounsafe, se habría podido evitar todo este problema.Me gustaría que alguien explorara este espacio de diseño.
&mutcomo demasiado poderoso.Si
&mutno otorgara permiso para mover el valor que contiene, todo el diseño habría sido mucho más simple.Planeo tratar esto en el siguiente artículo.
Rust debe mantener la compatibilidad hacia atrás y ya se decidió que se pueden mover valores desde
&mut, pero si no estuviéramos atados a decisiones pasadas, claramente sería posible un diseño mucho más limpio.mem::swapes solo una de las formas de mover valores a través de una referencia mutable, y hay muchísimas más.Option::takees un ejemplo que uso con bastante frecuencia, y sería realmente raro que eso fueraunsafe.Me gusta ver esta historia de contexto. WithoutBoats ya ha generado muchas discusiones muy oportunas sobre iteradores asíncronos,
pollypin.https://news.ycombinator.com/from?site=without.boats
No creo que haya muchas comunidades que profundicen públicamente con tanto detalle en las entrañas de un lenguaje, y es muy entretenido de ver.
Async todavía está a medio cocinar y es muy complejo.
Lo digo como alguien que ha escrito código Rust 40 horas por semana durante los últimos 3 años.
Podemos imaginar un lenguaje parecido a Rust que tenga constructores de movimiento, donde todos los subtipos de
Futureque se generan sean opacos y se asignen automáticamente en el heapEntonces el usuario no tendría forma de destruirlos y, como son opacos y están en otro lugar del heap, tampoco habría forma de moverlos, así que Pin podría volverse innecesario
Que existan constructores de movimiento significa que, conceptualmente, mover es destruir y luego volver a crear
Esto tiene el buen efecto de permitir fusionar e inlinear
Futures antes de ejecutarlosTambién se parece a la inmutabilidad de Rust. No es que exista memoria inmutable, sino referencias inmutables
Pero entonces cada llamada a una función asíncrona generaría una asignación separada, y eso es muy malo para la localidad de memoria
Alguna forma de stack virtual sería mucho mejor que eso, pero para optimizar el stack de modo que sea pequeño por defecto, al final haría falta recolección de basura
stdun traitMoveintegrado al lenguaje a un nivel similar aCopyMovedefiniría una función para mover un valor de una dirección de memoria a otra, y haría que las estructuras sinimpl Moveno pudieran moverseCasi todos los tipos llevarían
#[derive(Move)], que simplemente implementaría una función de movimiento que copia bytesPero esto abriría el camino para tipos autorreferenciales, futures y muchas otras cosas que necesitan comportamientos de movimiento más complejos
En la práctica, quizá tendría más sentido separarlo en dos traits, reflejando la diferencia entre
CopyyCloneUno sería un trait marcador que le dice al compilador que se pueden mover los bytes sin más, y el otro permitiría una implementación personalizada de un “constructor de movimiento”
Pin es tan difícil de entender que me gustaría tener Move
Un concepto complejo está envuelto en una doble negación, y a veces en una triple negación. Cuando veo algo como
fn(...), pienso “¿qué es esto?”, y para cuando se llega a una proyección de pinunsafe, ya me pierdoNo sé cuándo es seguro y cuándo no, y simplemente termino alejándome
Migrar de un Rust sin
Movea un Rust conMovesería incómodoHabría que agregar
#[derive(Move)]a casi todas las estructuras escritas hasta ahora, y lo mismo astdPara todos los tipos no fijados en ediciones existentes, el compilador tendría que inferir la implementación del trait
MoveSería mecánicamente posible, pero simplemente mucho trabajo
El Rust asíncrono es horrible. Sobre todo comparado con los futures/promises de casi cualquier otro lenguaje
Algún día alguien mejorará el modelo de seguridad de memoria de Rust y creará un nuevo lenguaje de sistemas parecido a Rust, con un trait
Movey mejores futuresPersonalmente, también me gustaría que hubiera ejecución en tiempo de compilación en lugar del sistema de macros de Rust
Me gusta Rust, y también todo el trabajo que el equipo ha hecho durante años
Pero el lenguaje que realmente espero es el que venga después de Rust
Un lenguaje con las mismas ideas, pero que haya aprendido de los errores de Rust; cada vez está más claro cómo podría verse ese mejor lenguaje estilo Rust
Tengo muchas ganas de verlo
Otro excelente artículo de WithoutBoats
Sinceramente, este es uno de esos aspectos que me alegra que en Rust quede abstraído y enterrado dentro del runtime asíncrono
Aun así, me da curiosidad dónde se usa Pin en la práctica, aparte de implementaciones personalizadas de
FuturePor cómo está planteado, parece que podría usarse dentro de FFI
Por ejemplo, si una función
externdevuelve un*mut Ty recibe un puntero, parece que envolverlo enPin<&mut T>podría darle una semántica mejorPero el artículo dice: “otro hecho sobre el estado de tipo fijado es que es completamente irrelevante para la mayoría de los tipos. Si el valor de un tipo nunca puede contener autorreferencias, fijarlo no sirve de nada”
Todavía soy muy principiante en FFI, así que quiero entender cuál es la mejor forma de envolverlo en Rust seguro
También aplica al interactuar con tipos del sistema que dependen de la dirección
Por ejemplo, en algunos mutex/futex de ciertos sistemas operativos, la documentación del kernel dice que el objeto de bloqueo en espacio de usuario no debe cambiar de dirección después de inicializarse, así que entiendo que
stdusa algo equivalente a PinLo particular es que la dirección no debe cambiar ni siquiera cuando no está bloqueado
Normalmente la condición solo aplica mientras está bloqueado y, en ese caso, como no se puede mover el objeto al que apunta una referencia activa, no hace falta Pin
Parece que están haciendo un trabajo enorme para no arreglar el verdadero problema: la ineficiencia de los hilos.
Todo código asíncrono, sin excepción, es un hack que implementa hilos ligeros con mucho azúcar sintáctico para la gestión de estado.
En lenguajes como Rust, agrega una enorme complejidad que originalmente no tendría por qué existir.
Si se corrigieran los problemas de eficiencia y escalabilidad de los hilos, todo esto desaparecería.
Desaparecería como por arte de magia.
Es parecido a cuando
nullen lenguajes como Java fue un “error de costo billonario”.Una sola decisión de diseño, o en este caso la ausencia de diseño, genera una complejidad enorme.
La cancelación es muy útil en aplicaciones de red y GUI.
Los hilos dificultan aprovechar bien tanto la CPU como la red sin acaparar en exceso ninguna de las dos.
Cuando empiezas a pasar trabajos entre pools de hilos, ya entraste en el camino de volver a implementar futuros.
O terminas trabajando con callbacks/eventos, lo que fragmenta el código, y eso era precisamente lo que
async/awaitpretendía ser como azúcar sintáctico.La alternativa para la cancelación y los timeouts es entretejer un objeto
Contextpor todo el código, como en Go, pero entonces aparece el problema de que el código de más bajo nivel llama ingenuamente a funciones que no respetan bien elContext.Eso apenas es un poco mejor que el problema de las funciones no asíncronas dentro de código asíncrono.
Así que me pregunto qué, en concreto, se supone que debieron haber hecho.
¿Simplemente levantar las manos y decir “algún día alguien quizá arregle Linux para que los hilos sean mágicamente rápidos, así que no vamos a agregar asincronía a nuestro lenguaje”?
Además, si se convierte al sistema operativo en el planificador de todas las tareas asíncronas, todos los runtimes tendrían que usar el planificador del sistema operativo, lo que imposibilita distintos diseños de planificadores.
Parece que no vuelven a evaluar el costo/beneficio ni siquiera después de que queda claro que el “enfoque preferido” no es viable.
“Quiero la función X, no me importan las consecuencias” rara vez es una jugada ganadora en el diseño de lenguajes.