1 puntos por GN⁺ 2024-07-22 | 1 comentarios | Compartir por WhatsApp
  • Pin de 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 ?Move no 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 trait Unpin, la mayoría de los tipos pueden moverse como antes
  • La dificultad de Pin no 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 con Drop reducen mucho su usabilidad

El problema que hace necesario a Pin

  • En el ecosistema async de Rust, Pin y pinning son piezas fundamentales, pero siguen siendo un área difícil y con muchos malentendidos para quienes aprenden async Rust
  • El objetivo de Pin no 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 punto foo(&mut z).await, deben almacenarse juntos dentro del mismo estado del Future tanto z como el Future Foo que referencia a z
    • 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 llamado Move
    • 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
  • 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 Option y luego extraerlo con Option::take
  • El problema más grande era la compatibilidad hacia atrás
    • Move no 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 ?Move tampoco era compatible hacia atrás debido a los associated types
    • El lugar donde se agrega un bound ?Trait a 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, el Target de DerefMut, tipos de retorno de funciones, items de iterators, valores de retorno del index operator, valores de retorno de operadores aritméticos, etc.
  • 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
  • Pin es 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
  • Pin pone 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 Unpin puede moverse fuera de Pin de 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 Pin y del módulo pin
  • 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

Problemas de usabilidad de Pin

  • Pin cumplió 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::set se 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 Pin es 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 Pin es 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 T no implementa Copy, pero puede pasarse varias veces como el mismo argumento
    • Esto se debe a que el compilador realiza implícitamente reborrowing, como si pusiera &mut *x en lugar de x
  • Pin<&mut T> es un tipo normal de biblioteca y no implementa Copy, 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_mut para hacer reborrow
  • Con una mutable reference normal se puede asignar directamente mediante dereference y el assignment operator, pero con Pin hay que aprender el método set
    • La razón por la que proliferan estas API especiales es que Pin es un tipo de biblioteca sin soporte sintáctico del lenguaje

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 Drop
    • Drop::drop recibe 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-lite manejan 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 Drop ya era stable antes de Pin, hizo falta una solución indirecta

Evaluación actual y próxima línea de mejora

  • Pin permitió 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, Pin se agregó de una forma completamente compatible hacia atrás con Rust existente
  • Pin se 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 Pin efectivamente crea un precipicio de complejidad
  • El concepto clave de la próxima línea de mejora es pinned places

1 comentarios

 
GN⁺ 2024-07-22
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 son Unpin, así que normalmente Pin no hace nada.
    Me tomó muchísimo tiempo entender esto, y creo que el conjunto de tipos T para los que Pin realmente tiene significado es bastante específico y extraño, pero la documentación no lo enfatiza lo suficiente.

    • Es buen feedback, y sería bueno que la documentación aclarara más esta parte.
      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 InnerType se declara como Unpin, 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 InnerType agrega métodos y APIs internamente unsafe para 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 &mut o sacar y mover desde un Box, 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”.

    • Pin sí tiene significado. Mientras el tipo de destino no implemente 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 poll que 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.

    • Me pasa lo mismo. Es uno de los casos más comunes de “simplemente evita unsafe y 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 Unpin no se ven afectados por Pin, 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.

    • Pero un objeto liso no se puede sujetar con velcro, y la madera no puede retener un imán.
      Me parece que Unpin significa 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 + !Unpin como algo parecido a una hoja de papel que solo se puede fijar con grapas, y T: Pin + Unpin como 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

    • Está claro que Mojo, en cierto sentido, heredó el concepto de semántica de valores de Swift, pero Rust también tiene semántica de valores en ese mismo sentido.
      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 inout de 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 &mut a un valor, puedes mover ese valor con cosas como mem::swap/replace.
    Pero en la práctica rara vez hace falta hacerlo.
    Si eso no estuviera permitido, creo que tener una referencia &mut a 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 swap y replace se hubieran hecho unsafe, se habría podido evitar todo este problema.
    Me gustaría que alguien explorara este espacio de diseño.

    • Correcto. Cuando se trabajaba en este problema, Aaron Turon describió &mut como demasiado poderoso.
      Si &mut no 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.
    • Eso es cierto, pero no escala. Habría sido difícil de sostener desde el principio porque rompería demasiado código existente.
      mem::swap es solo una de las formas de mover valores a través de una referencia mutable, y hay muchísimas más.
      Option::take es un ejemplo que uso con bastante frecuencia, y sería realmente raro que eso fuera unsafe.
  • Me gusta ver esta historia de contexto. WithoutBoats ya ha generado muchas discusiones muy oportunas sobre iteradores asíncronos, poll y pin.
    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.

    • Es genial, pero también significa que el desarrollo del lenguaje es muy lento.
      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 Future que se generan sean opacos y se asignen automáticamente en el heap
    Entonces 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

    • Pin es más un estado que una propiedad de los datos en sí
      Esto tiene el buen efecto de permitir fusionar e inlinear Futures antes de ejecutarlos
      También se parece a la inmutabilidad de Rust. No es que exista memoria inmutable, sino referencias inmutables
    • Si todos los futures se asignaran en el heap, no harían falta constructores de movimiento
      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
    • Puedo imaginar lo disruptivo que sería, pero realmente me gustaría que Rust lo encarara de frente y agregara a std un trait Move integrado al lenguaje a un nivel similar a Copy
      Move definiría una función para mover un valor de una dirección de memoria a otra, y haría que las estructuras sin impl Move no pudieran moverse
      Casi todos los tipos llevarían #[derive(Move)], que simplemente implementaría una función de movimiento que copia bytes
      Pero 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 Copy y Clone
      Uno 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 pin unsafe, ya me pierdo
      No sé cuándo es seguro y cuándo no, y simplemente termino alejándome
      Migrar de un Rust sin Move a un Rust con Move sería incómodo
      Habría que agregar #[derive(Move)] a casi todas las estructuras escritas hasta ahora, y lo mismo a std
      Para todos los tipos no fijados en ediciones existentes, el compilador tendría que inferir la implementación del trait Move
      Serí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 Move y mejores futures
      Personalmente, 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 Future

    • A mí también me da curiosidad
      Por cómo está planteado, parece que podría usarse dentro de FFI
      Por ejemplo, si una función extern devuelve un *mut T y recibe un puntero, parece que envolverlo en Pin<&mut T> podría darle una semántica mejor
      Pero 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
    • En FFI hay casos en los que una API de C expone elementos como punteros, no como referencias, y por eso no deben moverse
      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 std usa algo equivalente a Pin
      Lo 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 null en 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.

    • Los hilos no admiten cancelación de una manera razonable.
      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/await pretendía ser como azúcar sintáctico.
      La alternativa para la cancelación y los timeouts es entretejer un objeto Context por 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 el Context.
      Eso apenas es un poco mejor que el problema de las funciones no asíncronas dentro de código asíncrono.
    • Dudo que sea posible reescribir el kernel de Linux para “arreglar los problemas de eficiencia y escalabilidad de los hilos”, y aunque lo fuera, probablemente el conjunto de expertos en Rust que hicieron funcionar Pin y el conjunto de expertos en kernel capaces de hacer eso no sean los mismos.
      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”?
    • Marcar de forma diferenciada las funciones que se sincronizan con procesos concurrentes y las que no, de hecho, es algo bueno.
    • Lamentablemente, cruzar el límite del espacio de usuario tiene un costo, por más ligero que sea un “hilo”.
      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.
    • Las decisiones dañinas que tomó Rust muestran una cultura profundamente arraigada de empujar errores anteriores a toda costa.
      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.