Java y C # proporcionan seguridad de memoria al verificar los límites de la matriz y las desreferencias de puntero.
¿Qué mecanismos podrían implementarse en un lenguaje de programación para evitar la posibilidad de condiciones de carrera y puntos muertos?
Respuestas:
Las carreras ocurren cuando tienes un alias simultáneo de un objeto y, al menos, uno de los alias está mutando.
Por lo tanto, para evitar carreras, debe hacer que una o más de estas condiciones no sean ciertas.
Diversos enfoques abordan diversos aspectos. La programación funcional enfatiza la inmutabilidad que elimina la mutabilidad. El bloqueo / atómica elimina la simultaneidad. Los tipos afines eliminan el alias (Rust elimina el alias mutable). Los modelos de actor generalmente eliminan los alias.
Puede restringir los objetos que pueden tener alias para que sea más fácil asegurarse de evitar las condiciones anteriores. Ahí es donde entran los canales y / o los estilos de paso de mensajes. No se puede alias la memoria arbitraria, solo el final de un canal o cola que está dispuesta para estar libre de carreras. Por lo general, evitando la simultaneidad, es decir, bloqueos o atómicos.
La desventaja de estos diversos mecanismos es que restringen los programas que puede escribir. Cuanto más contundente es la restricción, menos son los programas. Por lo tanto, no funcionan los alias ni la mutabilidad, y son fáciles de razonar, pero son muy limitantes.
Es por eso que Rust está causando tanto revuelo. Es un lenguaje de ingeniería (en comparación con el académico) que admite aliasing y mutabilidad, pero hace que el compilador verifique que no ocurran simultáneamente. Aunque no es lo ideal, permite que una clase más grande de programas se escriba de forma segura que muchos de sus predecesores.
fuente
Es importante pensar primero en cómo C # y Java hacen esto. Lo hacen al convertir lo que es un comportamiento indefinido en C o C ++ en un comportamiento definido: bloquear el programa . Las desreferencias nulas y las excepciones de índice de matriz nunca deben detectarse en un programa Java o C # correcto; no deberían suceder en primer lugar porque el programa no debería tener ese error.
¡Pero eso es lo que creo que no quieres decir con tu pregunta! Podríamos escribir fácilmente un tiempo de ejecución de "punto muerto seguro" que verifica periódicamente para ver si hay n hilos que se esperan mutuamente y finalizar el programa si eso sucede, pero no creo que eso lo satisfaga.
El siguiente problema que enfrentamos con su pregunta es que las "condiciones de carrera", a diferencia de los puntos muertos, son difíciles de detectar. Recuerde, lo que buscamos en seguridad de hilos no es eliminar carreras . ¡Lo que buscamos es corregir el programa sin importar quién gane la carrera ! El problema con las condiciones de carrera no es que dos hilos estén funcionando en un orden indefinido y no sabemos quién va a terminar primero. El problema con las condiciones de carrera es que los desarrolladores olvidan que algunas órdenes de acabado de hilos son posibles y no tienen en cuenta esa posibilidad.
Entonces, su pregunta básicamente se reduce a "¿hay alguna forma en que un lenguaje de programación pueda garantizar que mi programa sea correcto?" y la respuesta a esa pregunta es, en la práctica, no.
Hasta ahora solo he criticado tu pregunta. Permítanme intentar cambiar de marcha aquí y abordar el espíritu de su pregunta. ¿Hay opciones que los diseñadores de idiomas podrían hacer que mitigarían la horrible situación en la que estamos con los subprocesos múltiples?
¡La situación es realmente horrible! Obtener el código multiproceso correcto, particularmente en arquitecturas de modelo de memoria débil, es muy, muy difícil. Es instructivo pensar por qué es difícil:
Entonces, hay una forma obvia de que los diseñadores de idiomas pueden mejorar las cosas. Abandone las victorias de rendimiento de los procesadores modernos . Haga que todos los programas, incluso los de subprocesos múltiples, tengan un modelo de memoria extremadamente fuerte. Esto hará que los programas multiproceso sean mucho, muchas veces más lentos, lo que funciona directamente en contra de la razón de tener programas multiproceso en primer lugar: para mejorar el rendimiento.
Incluso dejando de lado el modelo de memoria, hay otras razones por las que el subprocesamiento múltiple es difícil:
Ese último punto lleva más explicaciones. Por "composable" quiero decir lo siguiente:
Supongamos que deseamos calcular un int dado un doble. Escribimos una implementación correcta de la computación:
Supongamos que deseamos calcular una cadena dada un int:
Ahora si queremos calcular una cadena dada un doble:
G y F pueden componerse en una solución correcta para el problema más complejo.
Pero las cerraduras no tienen esta propiedad debido a los puntos muertos. Un método correcto M1 que toma bloqueos en el orden L1, L2, y un método correcto M2 que toma bloqueos en el orden L2, L1, no pueden usarse en el mismo programa sin crear un programa incorrecto. Los bloqueos hacen que no se pueda decir "cada método individual es correcto, por lo que todo es correcto".
Entonces, ¿qué podemos hacer, como diseñadores de idiomas?
Primero, no vayas allí. Múltiples hilos de control en un programa es una mala idea, y compartir memoria entre hilos es una mala idea, así que no lo pongas en el lenguaje o tiempo de ejecución en primer lugar.
Esto aparentemente no es un iniciador.
Volvamos nuestra atención a la pregunta más fundamental: ¿por qué tenemos múltiples hilos en primer lugar? Hay dos razones principales, y se combinan en la misma cosa con frecuencia, aunque son muy diferentes. Están combinados porque ambos tratan sobre la gestión de la latencia.
Mala idea. En su lugar, use la asincronía de un solo subproceso a través de las rutinas. C # hace esto maravillosamente. Java, no tan bien. Pero esta es la forma principal en que la actual cosecha de diseñadores de idiomas está ayudando a resolver el problema de la creación de hilos. El
await
operador en C # (inspirado en los flujos de trabajo asíncronos de F # y otras técnicas anteriores) se está incorporando a más y más lenguajes.Los diseñadores de idiomas pueden ayudar creando características de lenguaje que funcionen bien con paralelismo. Piense en cómo LINQ se extiende tan naturalmente a PLINQ, por ejemplo. Si es una persona sensata y limita sus operaciones TPL a operaciones vinculadas a la CPU que son altamente paralelas y no comparten memoria, puede obtener grandes ganancias aquí.
qué más podemos hacer?
C # no te permite esperar en un candado, porque esa es una receta para los callejones sin salida. C # no le permite bloquear un tipo de valor porque eso siempre es lo incorrecto; bloqueas la caja, no el valor. C # le advierte si alias un volátil, porque el alias no impone semántica de adquirir / liberar. Hay muchas más formas en que el compilador podría detectar problemas comunes y prevenirlos.
C # y Java cometieron un gran error de diseño al permitirle usar cualquier objeto de referencia como monitor. Eso fomenta todo tipo de malas prácticas que hacen que sea más difícil rastrear puntos muertos y más difícil evitarlos estáticamente. Y desperdicia bytes en cada encabezado de objeto. Se debe requerir que los monitores se deriven de una clase de monitor.
STM es una idea hermosa, y he jugado con implementaciones de juguetes en Haskell; le permite componer soluciones correctas de manera mucho más elegante a partir de piezas correctas que las soluciones basadas en cerraduras. Sin embargo, no sé lo suficiente sobre los detalles para decir por qué no se pudo hacer que funcione a escala; pregúntale a Joe Duffy la próxima vez que lo veas.
Se ha investigado mucho sobre lenguajes basados en cálculo de procesos y no entiendo muy bien ese espacio; intente leer algunos documentos sobre usted mismo y vea si tiene alguna idea.
Después de trabajar en Microsoft en Roslyn, trabajé en Coverity, y una de las cosas que hice fue conseguir que el analizador utilizara Roslyn. Al tener un análisis léxico, sintáctico y semántico preciso proporcionado por Microsoft, podríamos concentrarnos en el arduo trabajo de escribir detectores que encontraron problemas comunes de subprocesos múltiples.
Una razón fundamental por la que tenemos carreras y puntos muertos y todo eso es porque estamos escribiendo programas que dicen qué hacer , y resulta que todos somos basura en escribir programas imperativos; la computadora hace lo que le dices y le decimos que haga las cosas mal. Muchos lenguajes de programación modernos se centran cada vez más en la programación declarativa: diga qué resultados desea y deje que el compilador descubra la forma eficiente, segura y correcta de lograr ese resultado. Nuevamente, piense en LINQ; queremos que digas
from c in customers select c.FirstName
, lo que expresa una intención . Deje que el compilador descubra cómo escribir el código.Los algoritmos de aprendizaje automático son mucho mejores en algunas tareas que los algoritmos codificados a mano, aunque, por supuesto, hay muchas compensaciones que incluyen corrección, tiempo necesario para entrenar, sesgos introducidos por un mal entrenamiento, etc. Pero es probable que una gran cantidad de tareas que actualmente codificamos "a mano" pronto sean susceptibles de soluciones generadas por máquina. Si los humanos no escriben el código, no escriben errores.
Lamento que haya estado divagando un poco allí; Este es un tema enorme y difícil y no ha surgido un consenso claro en la comunidad de PL en los 20 años que llevo siguiendo el progreso en este espacio problemático.
fuente