¿Cómo difiere Rust de las facilidades de concurrencia de C ++?

35

Preguntas

Estoy tratando de entender si Rust mejora fundamental y suficientemente las facilidades de concurrencia de C ++ para decidir si debo pasar el tiempo para aprender Rust.

Específicamente, ¿cómo mejora Rust idiomático, o en cualquier caso, diverge de las facilidades de concurrencia de C ++ idiomático?

¿Es la mejora (o divergencia) mayormente sintáctica, o es sustancialmente una mejora (divergencia) en el paradigma? ¿O es otra cosa? ¿O no es realmente una mejora (divergencia) en absoluto?


Razón fundamental

Recientemente he estado tratando de enseñarme a mí mismo las facilidades de concurrencia de C ++ 14, y algo no se siente del todo bien. Algo se siente mal. ¿Qué se siente mal? Difícil de decir.

Se siente casi como si el compilador realmente no estuviera tratando de ayudarme a escribir programas correctos cuando se trata de concurrencia. Se siente casi como si estuviera usando un ensamblador en lugar de un compilador.

Es cierto que es muy probable que aún sufra un concepto sutil y defectuoso cuando se trata de concurrencia. Quizás todavía no asimile la tensión de Bartosz Milewski entre la programación con estado y las carreras de datos. Tal vez no entiendo cuánto de la metodología concurrente de sonido está en el compilador y cuánto está en el sistema operativo.

thb
fuente

Respuestas:

56

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 unsafey, 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 unsafeen su código, nunca abandona el lenguaje seguro. El resto de la publicación se ocupará principalmente de ese lenguaje, porque el unsafecódigo puede romper todas y cada una de las garantías de que Safe Rust trabaja tan duro para brindarle. Por otro lado, el unsafecó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::LinkedListpero es como std::listen 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 ( AtomicUsizees 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 unsafeusted, 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 Syncrasgo denota tipos que pueden ser compartidos por varios hilos sin riesgo de carreras de datos, mientras que Sendmarca 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_ptrque siempre usa operaciones atómicas para manipular su recuento de referencia, para evitar UB si shared_ptrsucede que varios hilos lo usan. Rust tiene Rcy Arc, que difieren solo en el Rc uso de operaciones de recuento no atómicas y no es seguro (es decir, no se implementa Synco Send) mientras que Arces muy parecido ashared_ptr (e implementa ambos rasgos).

Tenga en cuenta que si un tipo no se usa unsafepara 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 unsafecódigo. Aún así, es una sobrecarga mental adicional, y Rust no le da ninguna garantía para la exactitud del unsafecó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 unsafecó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 unsafecódigo, mientras que un programa C ++ es, bueno, completamente C ++.


fuente
66
¿En qué parte de mi pantalla está el interruptor de votación positiva +25? No puedo encontrarlo! Esta respuesta informativa es muy apreciada. Me deja sin preguntas obvias sobre los puntos que cubre. Entonces, en otros puntos: si entiendo la documentación de Rust, Rust tiene [a] instalaciones de prueba integradas y [b] un sistema de construcción llamado Cargo. ¿Están razonablemente listos para producción en su opinión? Además, con respecto a Cargo, ¿es de buen humor dejarme agregar shell, Python y scripts de Perl, compilación de LaTeX, etc., al proceso de construcción?
THB
2
@thb El material de prueba es muy básico (por ejemplo, sin burlas) pero funcional. Cargo funciona bastante bien, aunque su enfoque en Rust y en la reproducibilidad significa que puede no ser la mejor opción para cubrir todos los pasos desde el código fuente hasta los artefactos finales. Puede escribir scripts de compilación, pero eso puede no ser apropiado para todas las cosas que menciona. (Sin embargo, las personas usan regularmente scripts de compilación para compilar bibliotecas C o encontrar versiones existentes de bibliotecas C, por lo que no es como que Cargo deje de funcionar cuando usa más que Rust puro)
2
Por cierto, por lo que vale, su respuesta parece bastante concluyente. Como me gusta C ++, dado que C ++ tiene instalaciones decentes para casi todo lo que he necesitado hacer, dado que C ++ es estable y ampliamente utilizado, hasta ahora he estado bastante satisfecho de usar C ++ para cada posible propósito no ligero (nunca he desarrollado un interés en Java , por ejemplo). Pero ahora tenemos concurrencia, y C ++ 14 me parece que está luchando con eso. No he probado voluntariamente un nuevo lenguaje de programación en una década, pero (a menos que Haskell parezca una mejor opción) creo que tendré que probar Rust.
2016
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 con unsafeelementos. Solo los punteros en bruto no son Syncni lo Shareque significa que, por defecto, la estructura que los contiene no tendrá ninguno.
Hauleth
@ ŁukaszNiemier Puede funcionar bien, pero hay mil millones de formas en las que un tipo de uso inseguro puede terminar Sendo Syncaunque realmente no debería.
-2

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

srinath_perera
fuente
2
¡Bienvenido a Stack Exchange! Tenga en cuenta que cada vez que se vincula a su propio blog, debe indicarlo explícitamente; ver el centro de ayuda .
Glorfindel