Rust: [Estabilización] asíncrono / espera MVP

Creado en 26 jun. 2019  ·  58Comentarios  ·  Fuente: rust-lang/rust

Objetivo de estabilización: 1.38.0 (beta cut 2019-08-15)

Resumen ejecutivo

Esta es una propuesta para estabilizar una función asincrónica / en espera mínima viable, que incluye:

  • async anotaciones en funciones y bloques, lo que hace que se retrasen en la evaluación y, en cambio, se evalúen a un futuro.
  • Un operador await , válido solo dentro de un contexto async , que toma un futuro como argumento y hace que el futuro exterior en el que se encuentra ceda el control hasta que se complete el futuro que se espera.

Discusiones anteriores relacionadas

RFC:

Problemas de seguimiento:

Estabilizaciones:

Decisiones importantes alcanzadas

  • El futuro al que se evalúa una expresión asíncrona se construye a partir de su estado inicial, sin ejecutar nada del código del cuerpo antes de ceder.
  • La sintaxis de las funciones asíncronas usa el tipo de retorno "interno" (el tipo que coincide con la expresión return interna) en lugar del tipo de retorno "externo" (el tipo futuro al que se evalúa una llamada a la función)
  • La sintaxis para el operador await es la "sintaxis de punto postfix", expression.await , a diferencia de la más común await expression u otra sintaxis alternativa.

Trabajos de implementación que bloquean la estabilización

  • [x] async fns debería poder aceptar varias vidas útiles # 56238
  • [x] el tamaño de los generadores no debería crecer exponencialmente # 52924
  • [] Documentación mínima viable para la función async / await
  • [] Pruebas de compilación suficientes del comportamiento

Trabajo futuro

  • Async / await en contextos no estándar: async y await actualmente dependen de TLS para funcionar. Este es un problema de implementación que no forma parte del diseño y, aunque no bloquea la estabilización, se espera que se resuelva eventualmente.
  • Funciones asincrónicas de orden superior: async como modificador de literales de cierre no se estabiliza aquí. Se necesita más trabajo de diseño con respecto a la captura y abstracción sobre cierres asíncronos con vidas útiles.
  • Métodos de rasgo asíncrono: esto implica un trabajo de diseño e implementación significativo, pero es una característica muy deseable.
  • Procesamiento de flujo: el par del rasgo Futuro en la biblioteca de futuros es el rasgo Stream, un iterador asincrónico. La integración del soporte para la manipulación de transmisiones en std y el lenguaje es una característica deseable a largo plazo.
  • Optimización de las representaciones del generador: se puede trabajar más para optimizar la representación de los generadores para que tengan un tamaño más perfecto. Nos hemos asegurado de que esto es estrictamente un problema de optimización y no es semánticamente significativo.

Fondo

El manejo de E / S sin bloqueo es muy importante para desarrollar servicios de red de alto rendimiento, un caso de uso objetivo para Rust con gran interés por parte de los usuarios de producción. Por esta razón, una solución para hacer que sea ergonómico y factible escribir servicios utilizando E / S sin bloqueo ha sido durante mucho tiempo un objetivo de Rust. La función async / await es la culminación de ese esfuerzo.

Antes de 1.0, Rust tenía un sistema de subprocesamiento verde, en el que Rust proporcionaba una primitiva alternativa de subprocesos a nivel de lenguaje construida sobre IO sin bloqueo. Sin embargo, este sistema causó varios problemas: lo más importante, la introducción de un tiempo de ejecución del lenguaje que afectó el rendimiento incluso de los programas que no lo usaban, lo que aumentó significativamente la sobrecarga de FFI y tuvo varios problemas de diseño importantes sin resolver relacionados con la implementación de pilas de hilos verdes. .

Después de la eliminación de los hilos verdes, los miembros del proyecto Rust comenzaron a trabajar en una solución alternativa basada en la abstracción de futuros. A veces también llamados promesas, los futuros habían tenido mucho éxito en otros lenguajes como una abstracción basada en bibliotecas para IO sin bloqueo, y se sabía que a largo plazo se asignaban bien a una sintaxis asíncrona / espera que podía hacerlos solo menos convenientes que un sistema de lectura verde completamente invisible.

El mayor avance en el desarrollo de la abstracción del Futuro fue la introducción de un modelo de futuros basado en encuestas. Mientras que otros lenguajes usan un modelo basado en devolución de llamada, en el que el futuro mismo es responsable de programar la devolución de llamada para que se ejecute cuando esté completa, Rust usa un modelo basado en encuestas, en el que un ejecutor es responsable de sondear el futuro hasta su finalización, y el futuro simplemente informando al ejecutor que está listo para seguir avanzando utilizando la abstracción de Waker. Este modelo funcionó bien por varias razones:

  • Permitió a rustc compilar futuros a las máquinas de estado que tenían la sobrecarga de memoria más mínima, tanto en términos de tamaño como de direccionamiento indirecto. Esto tiene importantes beneficios de rendimiento sobre el enfoque basado en la devolución de llamada.
  • Permite que componentes como el ejecutor y el reactor existan como API de biblioteca, en lugar de como parte del tiempo de ejecución del lenguaje. Esto evita la introducción de costos globales que afectan a los usuarios que no utilizan esta función y permite a los usuarios reemplazar componentes individuales de su sistema de ejecución fácilmente, en lugar de requerir que tomemos una decisión de caja negra por ellos a nivel de idioma.
  • También crea todas las bibliotecas primitivas de concurrencia, en lugar de introducir la concurrencia en el lenguaje mediante la semántica de los operadores async y await. Esto hace que la concurrencia sea más clara y más visible a través del texto fuente, que debe utilizar una primitiva de concurrencia identificable para introducir la concurrencia.
  • Permite la cancelación sin gastos generales, al permitir que la ejecución de futuros se elimine antes de que se completen. Hacer que todos los futuros sean cancelables de forma gratuita tiene beneficios de rendimiento y claridad de código para los ejecutores y las primitivas de concurrencia.

(Los dos últimos puntos también se han identificado como una fuente de confusión para los usuarios que provienen de otros idiomas en los que no son ciertos y traen consigo expectativas de esos idiomas. Sin embargo, estas propiedades son propiedades inevitables del modelo basado en encuestas que tiene otras ventajas claras y son, en nuestra opinión, propiedades beneficiosas una vez que los usuarios las entienden).

Sin embargo, el modelo basado en encuestas adolecía de serios problemas ergonómicos cuando interactuaba con las referencias; esencialmente, las referencias a través de los puntos de rendimiento introdujeron errores de compilación irresolubles, aunque deberían ser seguros. Esto resultó en un código complejo y ruidoso lleno de arcos, mutex y cierres de movimiento, ninguno de los cuales era estrictamente necesario. Incluso dejando este problema a un lado, sin el nivel de lenguaje primitivo, los futuros sufrieron al forzar a los usuarios a un estilo de escritura de devoluciones de llamada altamente anidadas.

Por esta razón, buscamos azúcar sintáctico async / await con soporte para el uso normal de referencias en los puntos de rendimiento. Después de introducir la abstracción Pin que hizo que las referencias a través de los puntos de rendimiento fueran seguras de admitir, hemos desarrollado una sintaxis nativa asíncrona / espera que compila funciones en nuestros futuros basados ​​en encuestas, lo que permite a los usuarios obtener las ventajas de rendimiento de la E / S asíncrona con futuros al escribir código que es muy similar al código imperativo estándar. Esa última característica es el tema de este informe de estabilización.

descripción de la función async / await

El modificador async

La palabra clave async se puede aplicar en dos lugares:

  • Antes de una expresión de bloque.
  • Antes de una función libre o una función asociada en un impl inherente.

_ (Otras ubicaciones para funciones asíncronas: literales de cierre y métodos de rasgos, por ejemplo, se desarrollarán más y se estabilizarán en el futuro). _

El modificador asíncrono ajusta el elemento que modifica "convirtiéndolo en un futuro". En el caso de un bloque, el bloque se evalúa a un futuro de su resultado, en lugar de su resultado. En el caso de una función, las llamadas a esa función devuelven un futuro de su valor de retorno, en lugar de su valor de retorno. El código dentro de un elemento modificado por un modificador asíncrono se conoce como un contexto asíncrono.

El modificador async realiza esta modificación haciendo que el elemento sea evaluado como un constructor puro de un futuro, tomando argumentos y capturas como campos del futuro. Cada punto de espera se trata como una variante separada de esta máquina de estado, y el método de "encuesta" del futuro avanza el futuro a través de estos estados basándose en una transformación del código que escribió el usuario, hasta que finalmente alcanza su estado final.

El modificador async move

De manera similar a los cierres, los bloques asíncronos pueden capturar variables en el alcance circundante en el estado del futuro. Al igual que los cierres, estas variables se capturan por defecto por referencia. Sin embargo, en su lugar, se pueden capturar por valor, utilizando el modificador move (al igual que los cierres). async viene antes de move , lo que hace que estos bloques sean async move { } .

El operador await

Dentro de un contexto asincrónico, se puede formar una nueva expresión combinando una expresión con el operador await , usando esta sintaxis:

expression.await

El operador await solo se puede usar dentro de un contexto asincrónico, y el tipo de expresión a la que se aplica debe implementar el rasgo Future . La expresión de espera evalúa el valor de salida del futuro al que se aplica.

El operador de espera cede el control del futuro al que se evalúa el contexto asíncrono hasta que se completa el futuro al que se aplica. Esta operación de ceder el control no se puede escribir en la sintaxis de la superficie, pero si pudiera (usando la sintaxis YIELD_CONTROL! en este ejemplo), la eliminación de aguardar se vería más o menos así:

loop {
    match $future.poll(&waker) {
        Poll::Ready(value)  => break value,
        Poll::Pending       => YIELD_CONTROL!,
    }
}

Esto le permite esperar a que los futuros terminen de evaluarse en un contexto asincrónico, reenviando el rendimiento del control a través de Poll::Pending hacia afuera al contexto asincrónico más externo, en última instancia, al ejecutor en el que se ha generado el futuro.

Principales puntos de decisión

Ceder inmediatamente

Nuestras funciones y bloques asíncronos "ceden inmediatamente"; construirlos es una función pura que los pone en un estado inicial antes de ejecutar código en el cuerpo del contexto asíncrono. Ninguno de los códigos corporales se ejecuta hasta que comienzas a sondear ese futuro.

Esto es diferente de muchos otros lenguajes, en los que las llamadas a una función asíncrona activan el trabajo para comenzar de inmediato. En estos otros lenguajes, async es una construcción inherentemente concurrente: cuando llama a una función async, activa otra tarea para comenzar a ejecutarse simultáneamente con su tarea actual. En Rust, sin embargo, los futuros no se ejecutan de manera inherente de manera concurrente.

Podríamos hacer que los elementos asíncronos se ejecuten hasta el primer punto de espera cuando se construyen, en lugar de hacerlos puros. Sin embargo, decidimos que esto era más confuso: si el código se ejecuta durante la construcción del futuro o durante el sondeo, dependería de la ubicación de la primera espera en el cuerpo. Es más sencillo razonar para que todo el código se ejecute durante el sondeo y nunca durante la construcción.

Referencia:

Sintaxis de tipo de retorno

La sintaxis de nuestras funciones asíncronas usa el tipo de retorno "interno", en lugar del tipo de retorno "externo". Es decir, dicen que devuelven el tipo que finalmente evalúan, en lugar de decir que devuelven un futuro de ese tipo.

En un nivel, esta es una decisión sobre qué tipo de claridad se prefiere: debido a que la firma también incluye la anotación async , el hecho de que devuelvan un futuro se hace explícito en la firma. Sin embargo, puede ser útil para los usuarios ver que la función devuelve un futuro sin tener que notar también la palabra clave async. Pero esto también parece repetitivo, ya que la información también se transmite mediante la palabra clave async .

Lo que realmente nos inclinó la balanza fue la cuestión de la elisión de por vida. El tipo de retorno "externo" de cualquier función asíncrona es impl Future<Output = T> , donde T es el tipo de retorno interno. Sin embargo, ese futuro también captura la vida útil de cualquier argumento de entrada en sí mismo: esto es lo opuesto al valor predeterminado para impl Trait, que no se supone que capture ninguna vida útil de entrada a menos que los especifique. En otras palabras, usar el tipo de retorno externo significaría que las funciones asíncronas nunca se beneficiaron de la elisión de por vida (a menos que hiciéramos algo aún más inusual, como que las reglas de elisión de por vida funcionen de manera diferente para las funciones asíncronas y otras funciones).

Decidimos que, dado lo detallado y francamente confuso que sería escribir el tipo de retorno externo, no valía la pena señalar que esto devuelve un futuro para requerir que los usuarios lo escriban.

Orden del destructor

El orden de los destructores en contextos asíncronos es el mismo que en contextos no asíncronos. Las reglas exactas son un poco complicadas y están fuera de alcance aquí, pero en general, los valores se destruyen cuando salen de su alcance. Sin embargo, esto significa que continúan existiendo durante algún tiempo después de que se usan hasta que se limpian. Si ese tiempo incluye declaraciones de espera, esos elementos deben conservarse en el estado del futuro para que sus destructores se puedan ejecutar en el momento adecuado.

Podríamos, como una optimización del tamaño de los estados futuros, reordenar los destructores para que sean anteriores en algunos o en todos los contextos (por ejemplo, los argumentos de función no utilizados podrían descartarse inmediatamente, en lugar de almacenarse en el estado del futuro). Sin embargo, decidimos no hacer esto. El orden de los destructores puede ser un tema espinoso y confuso para los usuarios y, a veces, es muy importante para la semántica del programa. Hemos optado por renunciar a esta optimización a favor de garantizar un orden de destructor lo más sencillo posible: el mismo orden de destructor si se eliminan todas las palabras clave async y await.

(Algún día, es posible que estemos interesados ​​en buscar formas de marcar destructores como puros y reordenados. Ese es el trabajo de diseño futuro que también tiene implicaciones no relacionadas con async / await).

Referencia:

Espere la sintaxis del operador

Una desviación importante de las funciones async / await de otros idiomas es la sintaxis de nuestro operador await. Este ha sido objeto de una enorme discusión, más que cualquier otra decisión que hayamos tomado en el diseño de Rust.

Desde 2015, Rust ha tenido un operador de sufijo ? para el manejo ergonómico de errores. Desde mucho antes de 1.0, Rust también ha tenido un operador de sufijo . para el acceso a campos y llamadas a métodos. Debido a que el caso de uso principal para los futuros es realizar algún tipo de IO, la gran mayoría de los futuros se evalúan a Result con algunos
especie de error. Esto significa que, en la práctica, casi todas las operaciones de espera se secuencian con ? o con una llamada a un método después. Dada la precedencia estándar para los operadores de prefijo y sufijo, esto habría provocado que casi todos los operadores de espera se escribieran (await future)? , lo que consideramos muy poco ergonómico.

Por lo tanto, decidimos utilizar una sintaxis de sufijo, que se compone muy bien con los operadores ? y . . Después de considerar muchas opciones sintácticas diferentes, elegimos usar el operador . seguido de la palabra clave await.

Referencia:

Compatible con ejecutores de uno o varios subprocesos

Rust está diseñado para facilitar la escritura de programas simultáneos y paralelos sin imponer costos a las personas que escriben programas que se ejecutan en un solo hilo. Es importante poder ejecutar funciones asíncronas tanto en ejecutores de un solo subproceso como en ejecutores de múltiples subprocesos. La diferencia clave entre estos dos casos de uso es que los ejecutores multiproceso vincularán los futuros que pueden generar por Send , y los ejecutores de un solo hilo no lo harán.

Similar al comportamiento existente de la sintaxis impl Trait , las funciones asíncronas "filtran" los rasgos automáticos del futuro que devuelven. Es decir, además de observar que el tipo de retorno externo es un futuro, la persona que llama también puede observar si ese tipo es Enviar o Sincronizar, basándose en un examen de su cuerpo. Esto significa que cuando el tipo de retorno de un fn asincrónico se programa en un ejecutor multiproceso, puede verificar si esto es seguro o no. Sin embargo, no es necesario que el tipo sea Enviar, por lo que los usuarios de ejecutores de un solo subproceso pueden aprovechar las primitivas de un solo subproceso más eficaces.

Hubo cierta preocupación de que esto no funcionaría bien cuando las funciones asíncronas se expandieran a métodos, pero después de algunas discusiones se determinó que la situación no sería significativamente diferente.

Referencia:

Bloqueadores de estabilización conocidos

Tamaño del estado

Problema: # 52924

La forma en que la transformación asíncrona a una máquina de estado se implementa actualmente no es del todo óptima, lo que hace que el estado sea mucho más grande de lo necesario. Es posible, debido a que el tamaño del estado en realidad crece superlinealmente, desencadenar desbordamientos de pila en la pila real a medida que el tamaño del estado crece más que el tamaño de un subproceso normal del sistema. Mejorar este código genético para que el tamaño sea más razonable, al menos no lo suficientemente malo como para causar desbordamientos de pila en el uso normal, es una corrección de error de bloqueo.

Varias vidas en funciones asincrónicas

Problema: # 56238

Las funciones asíncronas deberían poder tener varias vidas útiles en su firma, todas las cuales se "capturan" en el futuro en el que se evalúa la función cuando se llama. Sin embargo, la reducción actual a impl Future dentro del compilador no admite múltiples vidas de entrada; se necesita una refactorización más profunda para hacer
este trabajo. Debido a que es muy probable que los usuarios escriban funciones con múltiples vidas de entrada (probablemente todas omitidas), esta es una corrección de error de bloqueo.

Otros problemas de bloqueo:

Etiqueta

Trabajo futuro

Todas estas son extensiones conocidas y de muy alta prioridad del MVP en las que pretendemos comenzar a trabajar tan pronto como hayamos enviado la versión inicial de async / await.

Cierres asincrónicos

En el RFC inicial, también admitimos el modificador asíncrono como modificador en literales de cierre, creando funciones asíncronas anónimas. Sin embargo, la experiencia en el uso de esta función ha demostrado que todavía hay una serie de cuestiones de diseño que resolver antes de que nos sintamos cómodos estabilizando este caso de uso:

  1. La naturaleza de la captura de variables se vuelve más complicada en cierres asíncronos y requiere cierto soporte sintáctico.
  2. Actualmente no es posible abstraer funciones asíncronas con vida útil de entrada y puede requerir algún lenguaje adicional o soporte de biblioteca.

Soporte sin STD

La implementación actual del operador await requiere que TLS pase el waker hacia abajo mientras sondea el futuro interior. Esto es esencialmente un "truco" para hacer que la sintaxis funcione en sistemas con TLS lo antes posible. A largo plazo, no tenemos la intención de comprometernos con este uso de TLS y preferiríamos pasar el waker como un argumento de función normal. Sin embargo, esto requiere cambios más profundos en el código de generación de la máquina de estado para que pueda manejar la toma de argumentos.

Aunque no estamos bloqueando la implementación de este cambio, lo consideramos de alta prioridad, ya que evita el uso de async / await en sistemas sin soporte TLS. Este es un problema de implementación pura: nada en el diseño del sistema requiere el uso de TLS.

Métodos de rasgo asíncrono

Actualmente no permitimos funciones o métodos asociados asíncronos en rasgos; este es el único lugar en el que puede escribir fn pero no async fn . Los métodos asincrónicos serían claramente una abstracción poderosa y queremos apoyarlos.

Un método asincrónico se trataría funcionalmente como un método que devuelve un tipo asociado que implementaría el futuro; cada método asincrónico generaría un tipo futuro único para la máquina de estado a la que se traduce ese método.

Sin embargo, debido a que ese futuro capturaría todas las entradas, cualquier tiempo de vida de entrada o parámetros de tipo también deberían capturarse en ese estado. Esto es equivalente a un concepto llamado tipos asociados genéricos , una característica que hemos querido durante mucho tiempo pero que aún no hemos implementado correctamente. Por tanto, la resolución de los métodos asíncronos está ligada a la resolución de los tipos asociados genéricos.

También hay problemas de diseño pendientes. Por ejemplo, ¿los métodos asíncronos son intercambiables con los métodos que devuelven tipos futuros que tendrían la misma firma? Además, los métodos asíncronos presentan problemas adicionales en torno a los rasgos automáticos, ya que es posible que deba requerir que el futuro devuelto por algún método asíncrono implemente un rasgo automático cuando abstrae un rasgo con un método asíncrono.

Una vez que tengamos incluso este soporte mínimo, hay otras consideraciones de diseño para futuras extensiones, como la posibilidad de hacer que los métodos asíncronos sean "seguros para los objetos".

Generadores y generadores asincrónicos

Tenemos una función de generador inestable que utiliza la misma transformación de máquina de estado de rutina para tomar funciones que producen múltiples valores y convertirlas en máquinas de estado. El caso de uso más obvio para esta característica es crear funciones que se compilen en "iteradores", al igual que las funciones asíncronas se compilan para
futuros. De manera similar, podríamos componer estas dos características para crear generadores asíncronos , funciones que se compilan en "flujos", el equivalente asíncrono de los iteradores. Hay casos de uso realmente claros para esto en la programación de redes, que a menudo implica el envío de flujos de mensajes entre sistemas.

Los generadores tienen muchas preguntas de diseño abiertas porque son una característica muy flexible con muchas opciones posibles. El diseño final de los generadores en Rust en términos de sintaxis y API de biblioteca aún está en el aire y es incierto.

A-async-await AsyncAwait-Focus F-async_await I-nominated T-lang disposition-merge finished-final-comment-period

Comentario más útil

El período de comentarios final, con una disposición para fusionarse , según la revisión anterior , ahora está completo .

Como representante automatizado del proceso de gobernanza, me gustaría agradecer al autor por su trabajo y a todos los que contribuyeron.

El RFC se fusionará pronto.

Todos 58 comentarios

@rfcbot fcp fusionar

El miembro del equipo @withoutboats ha propuesto fusionar esto. El siguiente paso es la revisión por el resto de los miembros del equipo etiquetados:

  • [x] @Centril
  • [x] @cramertj
  • [x] @eddyb
  • [x] @joshtriplett
  • [x] @nikomatsakis
  • [] @pnkfelix
  • [x] @scottmcm
  • [x] @withoutboats

Preocupaciones:

Una vez que la mayoría de los revisores aprueben (y como máximo 2 aprobaciones estén pendientes), entrará en su período de comentarios final. Si detecta un problema importante que no se ha planteado en ningún momento de este proceso, ¡hable!

Consulte este documento para obtener información sobre los comandos que pueden darme los miembros del equipo etiquetados.

(Simplemente registre los bloqueadores existentes en el informe anterior para asegurarse de que no se resbalen)

@rfcbot se refiere a la implementación-trabajo-bloqueo-estabilización

Miembro del equipo ... ha propuesto fusionar esto

¿Cómo se puede fusionar un problema de Github (no una solicitud de extracción)?

@vi El bot es un poco tonto y no comprueba si es un problema o relaciones públicas :) Puedes reemplazar "fusionar" con "aceptar" aquí.

¡Vaya, gracias por el resumen completo! Solo lo he estado siguiendo tangencialmente, pero estoy completamente seguro de que estás al tanto de todo.

@rfcbot revisado

¿Podría ser posible agregar explícitamente "Triage AsyncAwait-Problemas poco claros" a los bloqueadores de estabilización (y / o registrar una preocupación por eso)?

Tengo https://github.com/rust-lang/rust/issues/60414 que creo que es importante (obviamente, es mi error: p), y me gustaría al menos diferirlo explícitamente antes de la estabilización :)

¡Solo me gustaría expresar el agradecimiento de la comunidad por el esfuerzo que los equipos de Rust han puesto en esta función! Ha habido mucho diseño, discusión y algunas fallas en la comunicación, pero al menos yo, y con suerte muchos otros, estamos seguros de que a través de todo esto hemos encontrado la mejor solución posible para Rust. : tada:

(Dicho esto, me gustaría ver una mención de los problemas con el puente a las API del sistema de cancelación asincrónica y basada en finalización en futuras posibilidades. TL; DR todavía tienen que pasar los búferes de propiedad. Es un problema de biblioteca, pero uno con mencionar.)

También me gustaría ver una mención de los problemas con las API basadas en finalización. (vea este hilo interno para el contexto) Teniendo en cuenta IOCP y la introducción de io_uring , que puede convertirse en el camino para IO asíncrono en Linux, creo que es importante tener una forma clara de avanzar para manejarlos. Las ideas hipotéticas de caída asíncrona de IIUC no se pueden implementar de forma segura, y pasar los búferes propios será menos conveniente y potencialmente menos eficaz (por ejemplo, debido a una localidad peor o debido a copias adicionales).

@newpavlov He implementado cosas similares para Fuchsia, y es completamente posible hacerlo sin caída asíncrona. Hay algunas rutas diferentes para hacer esto, como usar la agrupación de recursos donde la adquisición de un recurso potencialmente tiene que esperar a que finalice un trabajo de limpieza en los recursos antiguos. La API de futuros actual puede y se ha utilizado para resolver estos problemas de manera efectiva en los sistemas de producción.

Sin embargo, este problema se trata de la estabilización de async / await, que es ortogonal al diseño de la API de futuros, que ya se ha estabilizado. No dude en hacer más preguntas o abrir un tema para debatir sobre el repositorio de futuros-rs.

@Ekleog

¿Podría ser posible agregar explícitamente "Triage AsyncAwait-Problemas poco claros" a los bloqueadores de estabilización (y / o registrar una preocupación por eso)?

Sí, eso es algo que hemos estado haciendo todas las semanas. WRT ese problema específico (# 60414), creo que es importante y me encantaría verlo arreglado, pero aún no hemos podido decidir si debería o no bloquear la estabilización, especialmente porque ya es observable en -> impl Trait funciones.

@cramertj ¡ Gracias! Creo que el problema de # 60414 es básicamente "el error puede surgir muy rápido ahora", mientras que con -> impl Trait parece que nadie lo había notado antes, entonces está bien si se aplaza de todos modos, algunos problemas tendrá que :) (FWIW surgió en código natural en una función en la que devuelvo () en un lugar y T::Assoc en otro, lo que IIRC me impidió compilar - Sin embargo, no he revisado el código desde que abrí # 60414, así que tal vez mi recuerdo sea incorrecto)

@Ekleog ¡ Sí, eso tiene sentido! Definitivamente puedo ver por qué sería un dolor: he creado un flujo zulip para sumergirme más en ese problema específico.

EDITAR: no importa, me perdí el objetivo 1.38 .

@cramertj

Hay algunas rutas diferentes para hacer esto, como usar la agrupación de recursos donde la adquisición de un recurso potencialmente tiene que esperar a que finalice un trabajo de limpieza en los recursos antiguos.

¿No son menos eficientes en comparación con mantener amortiguadores como parte del estado futuro? Mi principal preocupación es que el diseño actual no será de costo cero (en el sentido de que podrá crear un código más eficiente eliminando async abstracción) y menos ergonómico en las API basadas en finalización, y hay no hay forma clara de solucionarlo. No es un obstáculo de ninguna manera, pero creo que es importante no olvidarse de tales deficiencias en el diseño, de ahí la solicitud de mencionarlo en OP.

@el duque

Por supuesto, el equipo de lang puede juzgar esto mejor que yo, pero retrasar hasta 1.38 para garantizar una implementación estable parecería mucho más sensato.

Este problema apunta a 1.38, vea la primera línea de descripción.

@huxi gracias, me perdí eso. Editó mi comentario.

@nuevopavlov

¿No son menos eficientes en comparación con mantener amortiguadores como parte del estado futuro? Mi principal preocupación es que el diseño actual no será de costo cero (en el sentido de que podrá crear un código más eficiente eliminando la abstracción asincrónica) y menos ergonómico en las API basadas en finalización, y no hay una forma clara de solucionarlo. eso. No es un obstáculo de ninguna manera, pero creo que es importante no olvidarse de tales deficiencias en el diseño, de ahí la solicitud de mencionarlo en OP.

No, no necesariamente, pero pasemos esta discusión a un tema en un hilo separado, ya que no está relacionado con la estabilización de async / await.

(Dicho esto, me gustaría ver una mención de los problemas con el puente a las API del sistema de cancelación asincrónica y basada en finalización en futuras posibilidades. TL; DR todavía tienen que pasar los búferes de propiedad. Es un problema de biblioteca, pero uno con mencionar.)

También me gustaría ver una mención de los problemas con las API basadas en finalización. (vea este hilo interno para el contexto) Teniendo en cuenta IOCP y la introducción de io_uring, que puede convertirse en el Camino para IO asíncrono en Linux, creo que es importante tener una forma clara de avanzar para manejarlos.

Estoy de acuerdo con Taylor en que discutir los diseños de API en este espacio de problemas estaría fuera de tema, pero quiero abordar un aspecto específico de estos comentarios (y esta discusión sobre io_uring en general) que es relevante para la estabilización asíncrona / en espera: el problema de momento.

io_uring es una interfaz que llegará a Linux este año , 2019. El proyecto Rust ha estado trabajando en la abstracción de futuros desde 2015, hace cuatro años. La elección fundamental para favorecer una encuesta basada en una API basada en finalización ocurrió durante 2015 y 2016. En RustCamp en 2015, Carl Lerche habló sobre por qué hizo esa elección en mio, la abstracción de IO subyacente. En esta publicación de blog de 2016, Aaron Turon habló sobre los beneficios de crear abstracciones de mayor nivel. Estas decisiones se tomaron hace mucho tiempo y no podríamos haber llegado al punto en que estamos ahora sin ellas.

Las sugerencias de que deberíamos revisar nuestro modelo de futuros subyacente son sugerencias de que deberíamos volver al estado en el que estábamos hace 3 o 4 años y empezar de nuevo desde ese punto. ¿Qué tipo de abstracción podría cubrir un modelo IO basado en finalización sin introducir gastos generales para primitivas de nivel superior, como Aaron describió? ¿Cómo asignaremos ese modelo a una sintaxis que permita a los usuarios escribir "anotaciones normales de Rust + menores" como lo hace async / await? ¿Cómo podremos manejar la integración de eso en nuestro modelo de memoria, como lo hemos hecho con estas máquinas de estado con pin? Tratar de dar respuestas a estas preguntas no sería tema de este hilo; la cuestión es que responderlas y demostrar que las respuestas son correctas es trabajo. Lo que equivale a una sólida década de años laborales entre los diferentes contribuyentes hasta ahora tendría que rehacerse nuevamente.

El objetivo de Rust es enviar un producto que la gente pueda usar, y eso significa que tenemos que enviarlo . No siempre podemos detenernos a mirar hacia el futuro en lo que puede convertirse en un gran problema el próximo año y reiniciar nuestro proceso de diseño para incorporar eso. Hacemos lo mejor que podemos basándonos en la situación en la que nos encontramos. Obviamente, puede ser frustrante sentir que apenas nos perdimos algo importante, pero tal como está, tampoco tenemos una visión completa a) de cuál es el mejor resultado para el manejo de io_uring será, b) qué tan importante será io_uring en el ecosistema como un todo. No podemos revertir 4 años de trabajo basándonos en esto.

Ya existen limitaciones similares, probablemente incluso más graves, de Rust en otros espacios. Quiero destacar uno que miré con Nick Fitzgerald el otoño pasado: la integración de wasm GC. El plan para manejar objetos administrados en wasm es esencialmente segmentar el espacio de memoria, de modo que existan en un espacio de direcciones separado de los objetos no administrados (de hecho, algún día en muchos espacios de direcciones separados). El modelo de memoria de Rust simplemente no está diseñado para manejar espacios de direcciones separados, y cualquier código inseguro que se ocupe de la memoria de pila hoy asume que solo hay 1 espacio de direcciones. Si bien hemos esbozado soluciones técnicas innovadoras y técnicamente no disruptivas pero extremadamente disruptivas, el camino más probable a seguir es aceptar que nuestra historia de wasm GC puede no ser perfectamente óptima , porque estamos lidiando con las limitaciones de Rust como existe.

Un aspecto interesante que estamos estabilizando aquí es que estamos haciendo que las estructuras autorreferenciales estén disponibles a partir de código seguro. Lo que hace que esto sea interesante es que en un Pin<&mut SelfReferentialGenerator> , tenemos una referencia mutable (almacenada como un campo en el Pin ) que apunta a todo el estado del generador, y tenemos un puntero dentro de ese estado que apunta a otra parte del estado. ¡Ese puntero interno se alias con la referencia mutable!

La referencia mutable, que yo sepa, no se acostumbra a acceder realmente a la parte de la memoria a la que apunta el puntero a otro campo. (En particular, no hay clone método

Probablemente hay poco que podamos hacer al respecto en este punto, en particular porque Pin ya es estable, pero creo que vale la pena señalar que esto complicará significativamente las reglas que terminan siendo para las que se permite el alias y lo cual no es. Si pensaba que Stacked Borrows era complicado, prepárese para que las cosas empeoren.

Cc https://github.com/rust-lang/unsafe-code-guidelines/issues/148

La referencia mutable, que yo sepa, no se acostumbra a acceder realmente a la parte de la memoria a la que apunta el puntero a otro campo.

La gente ha hablado de hacer que todos estos tipos de corrutinas implementen Debug , parece que la conversación también debería integrar pautas de código inseguro para asegurarse de qué es seguro depurar la impresión.

La gente ha hablado de hacer que todos estos tipos de corrutinas implementen Debug, parece que la conversación también debería integrar pautas de código inseguro para asegurarse de qué es seguro depurar la impresión.

En efecto. Tal implementación Debug , si imprime los campos auto-referenciados, probablemente prohibiría las optimizaciones basadas en referencias de nivel MIR dentro de los generadores.

Actualización sobre bloqueadores:

Los dos bloqueadores de alto nivel han logrado un gran progreso y, en realidad, ambos podrían estar terminados (?). Más información de @cramertj @tmandry y @nikomatsakis sobre esto sería genial:

  • El problema de la vida útil múltiple debería haber sido solucionado por # 61775
  • La cuestión del tamaño es más ambigua; siempre habrá más optimizaciones que hacer, pero creo que el fruto más fácil de evitar los obvios aumentos exponenciales de las pistolas se ha resuelto en su mayoría.

Esto deja la documentación y las pruebas como los principales bloqueadores para estabilizar esta función. @Centril ha expresado constantemente su preocupación de que la función no esté lo suficientemente probada o pulida; @Centril, ¿hay

No estoy seguro de si alguien tiene documentación para conducir. ¡Cualquiera que quiera centrarse en mejorar la documentación en árbol en el libro, la referencia, etc. estaría haciendo un gran servicio! La documentación fuera del árbol como en el repositorio de futuros o areweasyncyet tiene un poco de tiempo extra.

A partir de hoy tenemos 6 semanas hasta que se elimine la versión beta, así que digamos que tenemos 4 semanas (hasta el 1 de agosto) para hacer estas cosas y estar seguros de que no resbalaremos 1.38.

La cuestión del tamaño es más ambigua; siempre habrá más optimizaciones que hacer, pero creo que el fruto más fácil de evitar los obvios aumentos exponenciales de las pistolas se ha resuelto en su mayoría.

Creo que sí, y algunos otros también se cerraron recientemente; pero hay otros problemas de bloqueo .

@Centril, ¿hay

Hay un papel de Dropbox con una lista de cosas que queríamos probar y hay https://github.com/rust-lang/rust/issues/62121. Aparte de eso, intentaré volver a revisar las áreas que creo que están subprobadas lo antes posible. Dicho esto, algunas áreas ahora están bastante bien probadas.

¡Cualquiera que quiera centrarse en mejorar la documentación en árbol en el libro, la referencia, etc. estaría haciendo un gran servicio!

En efecto; Me complacería revisar los RP a la referencia. También cc @ehuss.


También me gustaría mover async unsafe fn del MVP a su propia puerta de función porque creo que a) ha tenido poco uso, b) no está particularmente bien probado, c) aparentemente se comporta de manera extraña porque el .await punto no es donde se escribe unsafe { ... } y esto es comprensible desde el "punto de vista de implementación con fugas" pero no tanto desde un punto de vista de efectos, d) ha tenido poca discusión y no se incluyó en el RFC ni este informe, ye) hicimos esto con const fn y funcionó bien. (Puedo escribir la función de puerta de enlace de relaciones públicas)

Estoy bien con desestabilizar async unsafe fn , aunque soy escéptico de que terminemos con un diseño diferente al actual. ¡Pero parece prudente darnos tiempo para averiguarlo!

Creé https://github.com/rust-lang/rust/issues/62500 para mover async unsafe fn a una puerta de función distinta y la enumeré como un bloqueador. Probablemente también deberíamos crear un problema de seguimiento adecuado, supongo.

Soy muy escéptico de que logremos un diseño diferente por async unsafe fn y estoy sorprendido por la decisión de no incluirlo en la ronda inicial de estabilización. He escrito varios async fn s que no son seguros y los convertirán en async fn really_this_function_is_unsafe() o algo así, supongo. Esto parece una regresión en una expectativa básica que tienen los usuarios de Rust en términos de poder definir funciones que requieren unsafe { ... } para llamar. Sin embargo, otra puerta de función contribuirá a la impresión de que async / await está terminado.

¡@cramertj parece que deberíamos discutir! Creé un tema de Zulip para él , para tratar de evitar que este problema de seguimiento se sobrecargue demasiado.

En cuanto a tamaños futuros, se optimizan los casos que afectan a cada await punto. El último problema restante que conozco es el # 59087, donde cualquier préstamo de un futuro antes de esperar puede duplicar el tamaño asignado para ese futuro. Esto es bastante desafortunado, pero aún un poco mejor que donde estábamos antes.

Tengo una idea de cómo solucionar ese problema, pero a menos que esto sea mucho más común de lo que creo, probablemente no debería ser un bloqueador para un MVP estable.

Dicho esto, todavía necesito ver el impacto de estas optimizaciones en Fuchsia (que ha estado bloqueado por un tiempo, pero debería aclararse hoy o mañana). Es muy posible que descubramos más casos y tendremos que decidir si alguno de ellos debería estar bloqueado.

@cramertj (Recordatorio: uso async / await y quiero que se estabilice lo antes posible) Su argumento suena como un argumento para retrasar la estabilización de async / await, no para estabilizar async unsafe ahora mismo sin la experimentación y el pensamiento adecuados.

Especialmente porque no estaba incluido en el RFC, y potencialmente desencadenará otra tormenta de mierda de "rasgo implícito en la posición del argumento" si fuera forzado a salir de esta manera.

[Nota al margen que realmente no merece una discusión aquí: para "Sin embargo, otra puerta de función contribuirá a la impresión de que async / await no está terminado", encontré un error cada pocas horas de uso de async / await, propagado por unos pocos meses legítimamente necesarios por el equipo de rustc para arreglarlos, y es lo que me hace decir que no está terminado. El último se solucionó hace unos días, y realmente espero no descubrir otro cuando vuelva a intentar compilar mi código con un rustc más nuevo, pero…]

Su argumento suena como un argumento para retrasar la estabilización de async / await, no para estabilizar async inseguro en este momento sin la experimentación y el pensamiento adecuados.

No, no es un argumento para eso. Creo que async unsafe está listo y no puedo imaginar ningún otro diseño para él. Creo que solo hay consecuencias negativas por no incluirlo en este lanzamiento inicial. No creo que retrasar async / await en su conjunto, ni async unsafe específicamente, produzca un mejor resultado.

no puedo imaginar ningún otro diseño para él

Un diseño alternativo, aunque definitivamente requiere extensiones complicadas: async unsafe fn es unsafe a .await , no a call() . El razonamiento detrás de esto es que _no se puede hacer nada inseguro_ en el punto donde se llama async fn y crea el impl Future . Todo lo que hace ese paso es introducir datos en una estructura (de hecho, todos los async fn son const para llamar). El punto real de inseguridad es hacer avanzar el futuro con poll .

(En mi humilde opinión, si el unsafe es inmediato, unsafe async fn tiene más sentido, y si el unsafe se retrasa, async unsafe fn tiene más sentido).

Por supuesto, si nunca tenemos una manera de decir, por ejemplo, unsafe Future donde todos los métodos de Future no son seguros para llamar, entonces "elevando" el unsafe a la creación del impl Future , y el contrato de ese unsafe es utilizar el futuro resultante de una manera segura. Pero esto también se puede hacer casi trivialmente sin unsafe async fn simplemente "desugaring" manualmente en un bloque async : unsafe fn os_stuff() -> impl Future { async { .. } } .

Sin embargo, además de eso, hay una cuestión de si realmente existe una forma de tener invariantes que deben mantenerse una vez que comienza poll ing que no necesitan mantenerse en la creación. Es un patrón común en Rust que use un constructor unsafe en un tipo seguro (por ejemplo, Vec::from_raw_parts ). Pero la clave es que después de la construcción, el tipo _no_ puede ser mal utilizado; el alcance unsafe ha terminado. Este alcance de la inseguridad es clave para las garantías de Rust. Si introduce un unsafe async fn que contiene un impl Future seguro con requisitos de cómo / cuándo se encuesta, luego páselo al código seguro, ese código seguro está repentinamente dentro de su barrera de inseguridad. Y esto es _muy_ probable que suceda tan pronto como use este futuro de cualquier otra manera que no sea aguardarlo inmediatamente, ya que probablemente pasará por _algún_ combinador externo.

Supongo que el TL; DR de esto es que definitivamente hay esquinas de async unsafe fn que deben discutirse adecuadamente antes de estabilizarlo, especialmente con la dirección de const Trait potencialmente introducida (tengo un borrador de blog post sobre la generalización de esto a un "sistema de 'efectos' débil" con cualquier fn -modificar la palabra clave). Sin embargo, unsafe async fn podría ser lo suficientemente claro sobre el "orden" / "posicionamiento" del unsafe para estabilizarlo.

Creo que un rasgo unsafe Future basado en efectos no solo está fuera del alcance de cualquier cosa que sepamos expresar en el lenguaje o el compilador hoy, sino que, en última instancia, sería un diseño peor debido al efecto adicional. polimorfismo que requeriría que tuvieran los combinadores.

no se puede hacer nada inseguro en el punto donde se llama al async fn y se crea el impl Future. Todo lo que hace ese paso es introducir datos en una estructura (de hecho, todos los fn asíncronos son constantes para llamar). El punto real de inseguridad es adelantar el futuro con encuestas.

Es cierto que, dado que un async fn no puede ejecutar ningún código de usuario antes de ser .await ed, es probable que cualquier comportamiento indefinido se retrase hasta que se llame a .await . Sin embargo, creo que hay una distinción importante entre el punto de UB y el punto de unsafe ty. El punto real de unsafe ty es cuando un autor de API decide que un usuario debe prometer que se cumple un conjunto de invariantes no verificables estáticamente, incluso si el resultado de la violación de esos invariantes no causaría UB hasta más tarde en algún otro código seguro. Un ejemplo común de esto es una función unsafe para crear un valor que implemente un rasgo con métodos seguros (exactamente lo que es esto). He visto esto usado para asegurar que, por ejemplo, los tipos de implementación de Visitor -trait cuyas implementaciones se basan en invariantes unsafe se puedan usar correctamente, requiriendo unsafe para construir el tipo. Otros ejemplos incluyen cosas como slice::from_raw_parts , que en sí mismo no causará UB (aparte de los invariantes de validez de tipo), pero los accesos al segmento resultante sí lo harán.

No creo que async unsafe fn represente un caso único o interesante aquí; sigue un patrón bien establecido para realizar comportamientos unsafe detrás de una interfaz segura al requerir un unsafe constructor.

@cramertj El hecho de que tengas que argumentar a favor de esto (y no estoy sugiriendo que creo que la solución actual es mala, o que tengo una mejor idea) significa, para mí, que este debate debería estar en un lugar que deben seguir las personas que se preocupan por el óxido: el repositorio de RFC.

Como recordatorio, una cita de su archivo Léame:

Debe seguir este proceso si [...]:

  • Cualquier cambio semántico o sintáctico en el lenguaje que no sea una corrección de errores.
  • [... y también cosas no citadas]

No estoy diciendo que ocurrirá ningún cambio en el diseño actual. En realidad, pensarlo unos minutos me hace pensar que probablemente sea el mejor diseño que se me ocurrió. Pero el proceso es lo que nos permite evitar que nuestras creencias se conviertan en un peligro para Rust, y estamos perdiendo la sabiduría de muchas personas que siguen el repositorio de RFC pero no leen todos los números al no seguir el proceso aquí.

A veces, no seguir el proceso puede tener sentido. Aquí no veo ninguna urgencia que justifique ignorar el proceso solo para evitar unas 2 semanas de retraso del FCP.

Así que, por favor, permita que rust sea honesto con su comunidad sobre las promesas que ofrece en su propio archivo Léame, y simplemente mantenga esa función debajo de una puerta de función hasta que haya al menos un RFC aceptado y, con suerte, un poco más de uso en la naturaleza. Ya sea que se trate de toda la puerta de función async / await o simplemente una puerta de función async insegura, no me importa, pero no estabilizo algo que (AFAIK) ha tenido poco uso más allá de async-wg y apenas se conoce en el comunidad en general.

Estoy escribiendo un primer paso al material de referencia para el libro. En el camino, noté que el RFC async-await dice que el comportamiento del operador ? aún no se ha determinado. Y, sin embargo, parece funcionar bien en un bloque asincrónico (área de juegos ). ¿Deberíamos mover eso a una puerta de función separada? ¿O eso se resolvió en algún momento? No lo vi en el informe de estabilización, pero quizás me lo perdí.

(También hice esta pregunta en Zulip y preferiría respuestas allí, ya que es más fácil de administrar para mí).

Sí, se discutió y resolvió junto con el comportamiento de return , break , continue et. Alabama. que todos hacen "lo único posible" y se comportan como lo harían dentro de un cierre.

let f = unsafe { || {...} }; también es seguro para llamar y IIRC es equivalente a mover el unsafe al interior del cierre.
Lo mismo para unsafe fn foo() -> impl Fn() { || {...} } .

Esto, para mí, es un precedente suficiente para que "lo inseguro suceda después de dejar el alcance unsafe ".

Lo mismo vale para otros lugares. Como se señaló anteriormente, unsafe no siempre está donde estaría el UB potencial. Ejemplo:

    let mut vec: Vec<u32> = Vec::new();

    unsafe { vec.set_len(100); }      // <- unsafe

    let val = vec.get(5).unwrap();     // <- UB
    println!("{}", val);

Simplemente me parece un malentendido de inseguro - inseguro no indica que "una operación insegura ocurre aquí dentro" - marca "Estoy garantizando que mantengo las invariantes necesarias aquí". Si bien podría mantener las invariantes en el punto de espera, debido a que no involucra parámetros variables, no es un sitio muy obvio para verificar que mantiene las invariantes. Tiene mucho más sentido y es mucho más coherente con el funcionamiento de todas nuestras abstracciones inseguras, para garantizar que mantenga invariantes en el sitio de la llamada.

Esto está relacionado con por qué pensar en lo inseguro como un efecto conduce a intuiciones inexactas (como argumentó Ralf cuando se planteó esa idea por primera vez el año pasado). La inseguridad es específicamente, intencionalmente, no contagiosa. Si bien puede escribir funciones inseguras que llaman a otras funciones inseguras y simplemente reenviar sus invariantes a la pila de llamadas, esta no es la forma normal en que se usa inseguro en absoluto, y en realidad es un marcador sintáctico que se usa para definir contratos sobre valores y verificarlo manualmente los defiendes.

Por lo tanto, no es el caso de que cada decisión de diseño necesite un RFC completo, pero hemos estado trabajando para tratar de proporcionar más claridad y estructura sobre cómo se toman las decisiones. La lista de los principales puntos de decisión en la apertura de este número es un ejemplo de ello. Usando las herramientas disponibles para nosotros, me gustaría intentar un punto de consenso estructurado en torno a este problema de fns asíncronos inseguros, por lo que esta es una publicación de resumen con una encuesta.

async unsafe fn

async unsafe fns son funciones asincrónicas que solo se pueden llamar dentro de un bloque inseguro. El interior de su cuerpo se trata como un visor inseguro. El diseño alternativo principal sería hacer que los fns inseguros asincrónicos no sean seguros para esperar , en lugar de llamar. Hay una serie de razones sólidas para preferir el diseño en el que no es seguro llamar:

  1. Es coherente sintácticamente con el comportamiento de fns inseguros no asíncronos, que tampoco son seguros para llamar.
  2. Es más coherente con lo inseguro que funciona en general. Una función insegura es una abstracción que depende de que el llamante mantenga algunas invariantes. Es decir, no se trata de marcar "dónde ocurre la operación insegura" sino "dónde se garantiza que se mantendrá la invariante". Es mucho más sensato comprobar que los invariantes se mantienen en el sitio de la llamada, donde los argumentos se especifican realmente, que en el sitio de espera, separado de cuando se seleccionaron y verificaron los argumentos. Esto es muy normal para las funciones inseguras en general, que a menudo determinan algún estado en el que otras funciones seguras esperan que sean correctas.
  3. Es más consistente con la noción desagradable de firmas fn asíncronas, donde puede modelar la firma como equivalente a eliminar el modificador asíncrono y envolver el tipo de retorno en el futuro.
  4. La alternativa no es viable de implementar en el corto o mediano plazo (es decir, varios años). No hay forma de crear un futuro que no sea seguro para sondear en el lenguaje Rust actualmente diseñado. Algún tipo de "inseguro como un efecto" sería un gran cambio que tendría implicaciones de largo alcance y la necesidad de hacer frente a la forma en que es compatible hacia atrás con insegura, tal como existe hoy en día ya (como, funciones inseguras normales y bloques). Agregar fns inseguros asíncronos no cambia significativamente ese panorama, mientras que los fns inseguros asíncronos bajo la interpretación actual de inseguro tienen casos de uso prácticos reales a corto y mediano plazo.

@rfcbot ask lang "¿Aceptamos estabilizar async unsafe fn como un async fn que no es seguro llamar?"

No tengo idea de cómo hacer una encuesta con rfcbot, pero al menos lo he nominado.

El miembro del equipo @withoutboats ha pedido a los equipos: T-lang, un consenso sobre:

"¿Aceptamos estabilizar async unsafe fn como un async fn que no es seguro llamar?"

  • [x] @Centril
  • [x] @cramertj
  • [x] @eddyb
  • [] @joshtriplett
  • [x] @nikomatsakis
  • [] @pnkfelix
  • [] @scottmcm
  • [x] @withoutboats

@sin barcos

Me gustaría intentar un punto de consenso estructurado sobre este problema de fns asíncronos inseguros, por lo que esta es una publicación de resumen con una encuesta.

Gracias por la reseña. La discusión me ha convencido de que async unsafe fn tal como funciona hoy por la noche, se comporta bien. (Aunque probablemente se deberían agregar algunas pruebas, ya que parecía escaso). Además, ¿podría modificar el informe en la parte superior con partes de su informe + una descripción de cómo se comporta async unsafe fn ?

Es más coherente con lo inseguro que funciona en general. Una función insegura es una abstracción que depende de que el llamante mantenga algunas invariantes. Es decir, no se trata de marcar "dónde ocurre la operación insegura" sino "dónde se garantiza que se mantendrá la invariante". Es mucho más sensato comprobar que los invariantes se mantienen en el sitio de la llamada, donde los argumentos se especifican realmente, que en el sitio de espera, separado de cuando se seleccionaron y verificaron los argumentos. Esto es muy normal para las funciones inseguras en general, que a menudo determinan algún estado en el que otras funciones seguras esperan que sean correctas.

Como alguien que no presta demasiada atención, estaría de acuerdo y creo que la solución aquí es una buena documentación.

Podría estar fuera de lugar aquí, pero dado que

  • los futuros son combinatorios por naturaleza, es fundamental que sean componibles.
  • Los puntos de espera dentro de una implementación futura son generalmente un detalle de implementación invisible.
  • el futuro está muy lejos del contexto de ejecución, con el usuario real tal vez en el medio en lugar de en la raíz.

Me parece que los invariantes que dependen de un uso / comportamiento específico en espera están en algún lugar entre una mala idea e imposible de gobernar de manera segura.

Si hay casos en los que el valor de salida esperado es lo que está involucrado en mantener los invariantes, supongo que el futuro podría simplemente tener una salida que sea un contenedor que requiera acceso inseguro, como

struct UnsafeOutput<T>(T);
impl<T> UnsafeOutput<T> {
    unsafe fn unwrap(self) -> T { self.0 }
}

Dado que el unsafe ness es anterior al async ness en este "temprano inseguro", estaría mucho más feliz con el orden del modificador siendo unsafe async fn que async unsafe fn , porque unsafe (async fn) asigna mucho más obviamente a ese comportamiento que async (unsafe fn) .

Con mucho gusto aceptaré cualquiera de las dos, pero creo firmemente que el orden de envoltura expuesto aquí tiene el unsafe en el exterior, y el orden de los modificadores puede ayudar a aclarar esto. ( unsafe es el modificador de async fn , no async el modificador de unsafe fn .)

Con mucho gusto aceptaré cualquiera de las dos, pero creo firmemente que el orden de envoltura expuesto aquí tiene el unsafe en el exterior, y el orden de los modificadores puede ayudar a aclarar esto. ( unsafe es el modificador de async fn , no async el modificador de unsafe fn .)

Estuve contigo hasta tu último punto entre paréntesis. El artículo de @withoutboats me deja bastante claro que, si se trata la inseguridad en el sitio de la llamada, lo que realmente tiene es un unsafe fn (que se llama en un contexto asíncrono).

Yo diría que pintamos el cobertizo async unsafe fn bicicletas

Creo que async unsafe fn tiene más sentido, pero también creo que deberíamos aceptar gramaticalmente cualquier orden entre async, unsafe y const. Pero async unsafe fn tiene más sentido para mí con la noción de que elimines el async y modifiques el tipo de retorno a "desugar".

La alternativa no es viable de implementar en el corto o mediano plazo (es decir, varios años). No hay forma de crear un futuro que no sea seguro para sondear en el lenguaje Rust actualmente diseñado.

FWIW Me encontré con un problema similar al que mencioné en RFC2585 cuando se trata de cierres dentro de unsafe fn y los rasgos de función. No esperaba que unsafe async fn devolviera un Future con un método seguro poll , sino que devolviera un UnsafeFuture con un unsafe método de encuesta. (*) Entonces podríamos hacer que .await también funcione en UnsafeFuture s cuando se usa dentro de los bloques unsafe { } , pero no de otra manera.

Estos dos rasgos futuros serían un gran cambio con respecto a lo que tenemos hoy, y probablemente introducirían muchos problemas de componibilidad. Así que el barco para explorar alternativas probablemente ha zarpado. Particularmente dado que esto sería diferente a cómo funcionan los rasgos Fn hoy (por ejemplo, no tenemos un rasgo UnsafeFn o similar, y mi problema en RFC2585 fue que crear un cierre dentro de un unsafe fn devuelve un cierre que impls Fn() , es decir, que es seguro llamar, aunque este cierre puede llamar a funciones inseguras.

Crear el Futuro "inseguro" o el cierre no es el problema, el problema es llamarlos sin probar que hacerlo es seguro, particularmente cuando sus tipos no dicen que esto debe hacerse.

(*) Podemos proporcionar una implícita general de UnsafeFuture para todos los Future s, y también podemos proporcionar UnsafeFuture un método unsafe para "desenvolverse". como Future que es seguro para poll .

Aquí están mis dos centavos:

  • La explicación de @cramertj (https://github.com/rust-lang/rust/issues/62149#issuecomment-510166207) me convence de que las funciones asíncronas unsafe son el diseño correcto.
  • Prefiero un orden fijo de las palabras clave unsafe y async
  • Prefiero un poco el pedido unsafe async fn porque el pedido parece más lógico. Similar a "un coche eléctrico rápido" frente a "un coche eléctrico rápido". Principalmente porque un async fn convierte en un fn . Por lo tanto, tiene sentido que las dos palabras clave estén una al lado de la otra.

Creo que let f = unsafe { || { ... } } debería hacer que f seguro, un rasgo UnsafeFn nunca debería introducirse, y a priori .await ing y async unsafe fn deberían ser a salvo. ¡Cualquier UnsafeFuture necesita una fuerte justificación!

Todo esto se debe a que unsafe debería ser explícito, y Rust debería empujarlo de regreso a tierra segura. También con este token, f ... debería _no_ ser un bloque inseguro, https://github.com/rust-lang/rfcs/pull/2585 debería adoptarse y un async unsafe fn debe tener un cuerpo seguro.

Creo que este último punto podría resultar bastante crucial. Es posible que cada async unsafe fn emplee un bloque unsafe , pero de manera similar, la mayoría se beneficiaría de algún análisis de seguridad, y muchos suenan lo suficientemente complejos como para cometer errores fácilmente.

Nunca debemos pasar por alto el comprobador de préstamos al realizar capturas para cierres en particular.

Entonces, mi comentario aquí: https://github.com/rust-lang/rust/issues/62149#issuecomment -511116357 es una muy mala idea.

Un rasgo UnsafeFuture requeriría que la persona que llama escriba unsafe { } para sondear un futuro, pero la persona que llama no tiene idea de qué obligaciones deben probarse allí, por ejemplo, si obtiene un Box<dyn UnsafeFuture> ¿ unsafe { future.poll() } seguro? ¿Para todos los futuros? No puedes saberlo. Entonces esto sería completamente inútil como @rpjohnst señaló en discordia por un rasgo UnsafeFn similar.

Exigir que Future's siempre sea seguro para los políticos tiene sentido, y el proceso de construir un futuro que debe ser seguro para las encuestas puede ser inseguro; Supongo que eso es async unsafe fn . Pero en ese caso, el elemento fn puede documentar lo que se debe mantener para que el futuro devuelto sea seguro para sondear.

@rfcbot implementación-trabajo-bloqueo-estabilización

Que yo sepa, todavía hay 2 bloqueadores de implementación conocidos (https://github.com/rust-lang/rust/issues/61949, https://github.com/rust-lang/rust/issues/62517) y Aún sería bueno agregar algunas pruebas. Estoy resolviendo mi preocupación de hacer que rfcbot no sea nuestro bloqueador en cuanto al tiempo y luego bloquearemos las correcciones.

@rfcbot resolver implementación-trabajo-bloqueo-estabilización

: bell: Esto ahora está entrando en su período de comentarios final , según la revisión anterior . :campana:

PR de estabilización archivado en https://github.com/rust-lang/rust/pull/63209.

El período de comentarios final, con una disposición para fusionarse , según la revisión anterior , ahora está completo .

Como representante automatizado del proceso de gobernanza, me gustaría agradecer al autor por su trabajo y a todos los que contribuyeron.

El RFC se fusionará pronto.

Un aspecto interesante que estamos estabilizando aquí es que estamos haciendo que las estructuras autorreferenciales estén disponibles a partir de código seguro. Lo que hace que esto sea interesante es que en un Pin <& mut SelfReferentialGenerator>, tenemos una referencia mutable (almacenada como un campo en el Pin) que apunta a todo el estado del generador, y tenemos un puntero dentro de ese estado que apunta a otra parte del estado. . ¡Ese puntero interno se alias con la referencia mutable!

Como seguimiento a esto, @comex logró escribir algún código (seguro) asíncrono de Rust que viola las anotaciones noalias LLVM de la forma en que las emitimos actualmente. Sin embargo, parece que debido al uso de TLS, actualmente no hay errores de compilación.

¿Fue útil esta página
0 / 5 - 0 calificaciones