Una mejor historia de concurrencia es uno de los objetivos principales del proyecto Rust, por lo que se deben esperar mejoras, siempre que confiemos en que el proyecto logre sus objetivos. Descargo de responsabilidad completo: tengo una gran opinión de Rust y estoy involucrado en ello. Según lo solicitado, intentaré evitar los juicios de valor y describir las diferencias en lugar de las mejoras (en mi humilde opinión) .
Moho seguro e inseguro
"Rust" se compone de dos lenguajes: uno que se esfuerza por aislarlo de los peligros de la programación de sistemas, y otro más poderoso sin tales aspiraciones.
El óxido inseguro es un lenguaje desagradable y brutal que se parece mucho a C ++. Le permite hacer cosas arbitrariamente peligrosas, hablar con el hardware, (mal) administrar la memoria manualmente, pegarse un tiro en el pie, etc. Es muy parecido a C y C ++ en que la corrección del programa está en sus manos. y las manos de todos los demás programadores involucrados en él. Opta por este lenguaje con la palabra clave unsafe
y, como en C y C ++, un solo error en una sola ubicación puede hacer que todo el proyecto se bloquee.
Safe Rust es el "predeterminado", la gran mayoría del código Rust es seguro, y si nunca menciona la palabra clave unsafe
en su código, nunca abandona el lenguaje seguro. El resto de la publicación se ocupará principalmente de ese lenguaje, porque el unsafe
código puede romper todas y cada una de las garantías de que Safe Rust trabaja tan duro para brindarle. Por otro lado, el unsafe
código no es malo y la comunidad no lo trata como tal (sin embargo, se desaconseja cuando no es necesario).
Es peligroso, sí, pero también importante, porque permite construir las abstracciones que usa el código seguro. Un buen código inseguro usa el sistema de tipos para evitar que otros lo usen mal y, por lo tanto, la presencia de un código inseguro en un programa Rust no necesita alterar el código seguro. Las siguientes diferencias existen porque los sistemas de tipos de Rust tienen herramientas que C ++ no tiene, y porque el código inseguro que implementa las abstracciones de concurrencia usa estas herramientas de manera efectiva.
No diferencia: memoria compartida / mutable
Aunque Rust pone más énfasis en el paso de mensajes y controla muy estrictamente la memoria compartida, no descarta la concurrencia de memoria compartida y admite explícitamente las abstracciones comunes (bloqueos, operaciones atómicas, variables de condición, colecciones concurrentes).
Además, al igual que C ++ y a diferencia de los lenguajes funcionales, a Rust realmente le gustan las estructuras de datos imperativas tradicionales. No hay una lista vinculada persistente / inmutable en la biblioteca estándar. Hay std::collections::LinkedList
pero es como std::list
en C ++ y se desaconseja por las mismas razones que std::list
(mal uso de caché).
Sin embargo, con referencia al título de esta sección ("memoria compartida / mutable"), Rust tiene una diferencia con C ++: alienta encarecidamente que la memoria sea "compartida XOR mutable", es decir, que la memoria nunca sea compartida y mutable al mismo hora. Mute la memoria como desee "en la privacidad de su propio hilo", por así decirlo. Compare esto con C ++, donde la memoria mutable compartida es la opción predeterminada y ampliamente utilizada.
Si bien el paradigma de xor-mutable compartido es muy importante para las diferencias a continuación, también es un paradigma de programación bastante diferente al que lleva un tiempo acostumbrarse y que impone restricciones significativas. Ocasionalmente, uno tiene que optar por este paradigma, por ejemplo, con tipos atómicos ( AtomicUsize
es la esencia de la memoria mutable compartida). Tenga en cuenta que los bloqueos también obedecen la regla de xor-mutable compartida, ya que descarta lecturas y escrituras concurrentes (mientras que un hilo escribe, ningún otro hilo puede leer o escribir).
No diferencia: las carreras de datos son comportamientos indefinidos (UB)
Si desencadena una carrera de datos en el código Rust, se acabó el juego, como en C ++. Todas las apuestas están apagadas y el compilador puede hacer lo que le plazca.
Sin embargo, es una garantía difícil de que el código Rust seguro no tenga carreras de datos (o cualquier UB para el caso). Esto se extiende tanto al lenguaje central como a la biblioteca estándar. Si puede escribir un programa Rust que no utiliza unsafe
(incluso en bibliotecas de terceros pero excluye la biblioteca estándar) que desencadena UB, entonces eso se considera un error y se solucionará (esto ya ha sucedido varias veces). Esto, por supuesto, en marcado contraste con C ++, donde es trivial escribir programas con UB.
Diferencia: estricta disciplina de bloqueo
A diferencia de C ++, una cerradura en Rust ( std::sync::Mutex
, std::sync::RwLock
, etc.) posee los datos que está protegiendo. En lugar de tomar un bloqueo y luego manipular parte de la memoria compartida que está asociada al bloqueo solo en la documentación, los datos compartidos son inaccesibles mientras no mantiene el bloqueo. Un protector RAII mantiene el bloqueo y simultáneamente da acceso a los datos bloqueados (esto podría implementarse mediante C ++, pero no mediante los std::
bloqueos). El sistema de por vida garantiza que no pueda seguir accediendo a los datos después de liberar el bloqueo (suelte la protección RAII).
Por supuesto, puede tener un bloqueo que no contenga datos útiles ( Mutex<()>
) y simplemente compartir algo de memoria sin asociarlo explícitamente con ese bloqueo. Sin embargo, tener memoria compartida potencialmente no sincronizada requiere unsafe
.
Diferencia: Prevención de compartir accidentalmente
Aunque puede haber compartido memoria, solo comparte cuando la solicita explícitamente. Por ejemplo, cuando usa el paso de mensajes (por ejemplo, los canales de std::sync
), el sistema de por vida asegura que no mantenga ninguna referencia a los datos después de enviarlos a otro hilo. Para compartir datos detrás de un bloqueo, explícitamente construyes el bloqueo y se lo das a otro hilo. Para compartir memoria no sincronizada con unsafe
usted, bueno, tiene que usar unsafe
.
Esto se relaciona con el siguiente punto:
Diferencia: seguimiento de seguridad de hilos
El sistema de tipo Rust rastrea alguna noción de seguridad del hilo. Específicamente, el Sync
rasgo denota tipos que pueden ser compartidos por varios hilos sin riesgo de carreras de datos, mientras que Send
marca aquellos que se pueden mover de un hilo a otro. El compilador aplica esto en todo el programa y, por lo tanto, los diseñadores de bibliotecas se atreven a hacer optimizaciones que serían estúpidamente peligrosas sin estas comprobaciones estáticas. Por ejemplo, C ++ std::shared_ptr
que siempre usa operaciones atómicas para manipular su recuento de referencia, para evitar UB si shared_ptr
sucede que varios hilos lo usan. Rust tiene Rc
y Arc
, que difieren solo en el Rc
uso de operaciones de recuento no atómicas y no es seguro (es decir, no se implementa Sync
o Send
) mientras que Arc
es muy parecido ashared_ptr
(e implementa ambos rasgos).
Tenga en cuenta que si un tipo no se usa unsafe
para implementar manualmente la sincronización, la presencia o ausencia de los rasgos se infiere correctamente.
Diferencia: reglas muy estrictas
Si el compilador no puede estar absolutamente seguro de que algún código esté libre de carreras de datos y otros UB, no compilará, punto . Las reglas antes mencionadas y otras herramientas pueden llevarte bastante lejos, pero tarde o temprano querrás hacer algo correcto, pero por razones sutiles que escapan al aviso del compilador. Podría ser una estructura de datos complicada sin bloqueo, pero también podría ser algo tan mundano como "Escribo en ubicaciones aleatorias en una matriz compartida, pero los índices se calculan de modo que cada ubicación se escriba en un solo hilo".
En ese momento, puede morder la viñeta y agregar un poco de sincronización innecesaria, o puede volver a redactar el código de modo que el compilador pueda ver su corrección (a menudo factible, a veces bastante difícil, a veces imposible), o puede caer en el unsafe
código. Aún así, es una sobrecarga mental adicional, y Rust no le da ninguna garantía para la exactitud del unsafe
código.
Diferencia: menos herramientas
Debido a las diferencias antes mencionadas, en Rust es mucho más raro que uno escriba código que pueda tener una carrera de datos (o un uso después de gratis, o un doble gratis, o ...). Si bien esto es bueno, tiene el desafortunado efecto secundario de que el ecosistema para rastrear tales errores está aún más subdesarrollado de lo que cabría esperar dada la juventud y el pequeño tamaño de la comunidad.
Si bien las herramientas como valgrind y el desinfectante de hilos de LLVM podrían aplicarse en principio al código Rust, si esto realmente funciona varía de una herramienta a otra (e incluso las que funcionan pueden ser difíciles de configurar, especialmente porque es posible que no encuentre ninguna -Fuente recursos sobre cómo hacerlo). Realmente no ayuda que Rust carezca actualmente de una especificación real y, en particular, de un modelo de memoria formal.
En resumen, escribir unsafe
código Rust correctamente es más difícil que escribir código C ++ correctamente, a pesar de que ambos lenguajes son más o menos comparables en términos de capacidades y riesgos. Por supuesto, esto debe compararse con el hecho de que un programa Rust típico contendrá solo una fracción relativamente pequeña de unsafe
código, mientras que un programa C ++ es, bueno, completamente C ++.
Note that if a type doesn't use unsafe to manually implement synchronization, the presence or absence of the traits are inferred correctly.
en realidad todavía lo hace incluso conunsafe
elementos. Solo los punteros en bruto no sonSync
ni loShare
que significa que, por defecto, la estructura que los contiene no tendrá ninguno.Send
oSync
aunque realmente no debería.Rust también se parece mucho a Erlang and Go. Se comunica utilizando canales que tienen buffers y espera condicional. Al igual que Go, relaja las restricciones de Erlang al permitirle hacer memoria compartida, admitir el recuento de referencias atómicas y bloqueos, y al permitirle pasar canales de hilo a hilo.
Sin embargo, Rust va un paso más allá. Mientras Go confía en que hagas lo correcto, Rust asigna un mentor que se sienta contigo y se queja si intentas hacer lo incorrecto. El mentor de Rust es el compilador. Realiza un análisis sofisticado para determinar la propiedad de los valores que se pasan alrededor de los subprocesos y proporciona errores de compilación si hay problemas potenciales.
A continuación hay una cita de RUST docs.
Las reglas de propiedad juegan un papel vital en el envío de mensajes porque nos ayudan a escribir código seguro y concurrente. La prevención de errores en la programación concurrente es la ventaja que obtenemos al compensar tener que pensar en la propiedad en todos nuestros programas Rust. - Mensaje que pasa con la propiedad de los valores.
Si Erlang es draconiano y Go es un estado libre, entonces Rust es un estado de niñera.
Puede encontrar más información en las ideologías de concurrencia de lenguajes de programación: Java, C #, C, C +, Go y Rust
fuente