En los primeros días de FORTRAN y BASIC, esencialmente todos los programas fueron escritos con declaraciones GOTO. El resultado fue un código de espagueti y la solución fue una programación estructurada.
Del mismo modo, los punteros pueden tener características difíciles de controlar en nuestros programas. C ++ comenzó con muchos punteros, pero se recomienda el uso de referencias. Las bibliotecas como STL pueden reducir parte de nuestra dependencia. También hay modismos para crear punteros inteligentes que tienen mejores características, y algunas versiones de C ++ permiten referencias y código administrado.
Las prácticas de programación como la herencia y el polimorfismo utilizan muchos punteros detrás de escena (al igual que, mientras que la programación estructurada genera código lleno de instrucciones de ramificación). Los lenguajes como Java eliminan los punteros y usan la recolección de elementos no utilizados para administrar datos asignados dinámicamente en lugar de depender de los programadores para que coincidan con todas sus declaraciones nuevas y eliminadas.
En mi lectura, he visto ejemplos de programación multiproceso y multiproceso que no parecen utilizar semáforos. ¿Utilizan lo mismo con nombres diferentes o tienen nuevas formas de estructurar la protección de los recursos contra el uso concurrente?
Por ejemplo, un ejemplo específico de un sistema para programación multiproceso con procesadores multinúcleo es OpenMP. Representa una región crítica de la siguiente manera, sin el uso de semáforos, que parecen no estar incluidos en el entorno.
th_id = omp_get_thread_num();
#pragma omp critical
{
cout << "Hello World from thread " << th_id << '\n';
}
Este ejemplo es un extracto de: http://en.wikipedia.org/wiki/OpenMP
Alternativamente, una protección similar de los hilos entre sí utilizando semáforos con las funciones wait () y signal () podría verse así:
wait(sem);
th_id = get_thread_num();
cout << "Hello World from thread " << th_id << '\n';
signal(sem);
En este ejemplo, las cosas son bastante simples, y una simple revisión es suficiente para mostrar que las llamadas wait () y signal () coinciden e incluso con mucha concurrencia, se proporciona seguridad de subprocesos. Pero otros algoritmos son más complicados y usan múltiples semáforos (tanto binarios como de conteo) distribuidos en múltiples funciones con condiciones complejas que pueden ser llamadas por muchos hilos. Las consecuencias de crear un punto muerto o no hacer que las cosas sean seguras pueden ser difíciles de manejar.
¿Estos sistemas como OpenMP eliminan los problemas con los semáforos?
¿Mueven el problema a otro lugar?
¿Cómo transformo mi semáforo favorito usando un algoritmo para no usar más semáforos?
fuente
Respuestas:
¿Existen técnicas y prácticas de programación concurrentes que ya no se deberían usar? Yo diría que sí .
Una de las primeras técnicas de programación concurrente que parece rara hoy en día es la programación dirigida por interrupciones . Así funcionaba UNIX en la década de 1970. Consulte el Comentario de los Leones sobre UNIX o el diseño de Bach del sistema operativo UNIX . Brevemente, la técnica es suspender las interrupciones temporalmente mientras se manipula una estructura de datos, y luego restaurar las interrupciones después. La página del comando man BSD spl (9)tiene un ejemplo de este estilo de codificación. Tenga en cuenta que las interrupciones están orientadas al hardware, y el código incorpora una relación implícita entre el tipo de interrupción de hardware y las estructuras de datos asociadas con ese hardware. Por ejemplo, el código que manipula las memorias intermedias de E / S de disco debe suspender las interrupciones del hardware del controlador de disco mientras se trabaja con esas memorias intermedias.
Este estilo de programación fue empleado por los sistemas operativos en hardware uniprocesador. Era mucho más raro que las aplicaciones manejaran las interrupciones. Algunos sistemas operativos tuvieron interrupciones de software, y creo que la gente trató de construir sistemas de subprocesos o de rutina sobre ellos, pero esto no fue muy generalizado. (Ciertamente no en el mundo UNIX.) Sospecho que la programación de estilo de interrupción se limita hoy a pequeños sistemas integrados o sistemas en tiempo real.
Los semáforos son un avance sobre las interrupciones porque son construcciones de software (no relacionadas con el hardware), proporcionan abstracciones sobre las instalaciones de hardware y permiten el multiprocesamiento y el multiprocesamiento. El principal problema es que no están estructurados. El programador es responsable de mantener la relación entre cada semáforo y las estructuras de datos que protege, globalmente en todo el programa. Por esta razón, creo que los semáforos desnudos raramente se usan hoy en día.
Otro pequeño paso adelante es un monitor , que encapsula los mecanismos de control de concurrencia (bloqueos y condiciones) con los datos que se protegen. Esto se transfirió al sistema Mesa (enlace alternativo) y de allí a Java. (Si lee este documento de Mesa, puede ver que los bloqueos y las condiciones del monitor de Java se copian casi al pie de la letra de Mesa). Los monitores son útiles porque un programador lo suficientemente cuidadoso y diligente puede escribir programas concurrentes de forma segura utilizando solo el razonamiento local sobre el código y los datos. dentro del monitor.
Hay construcciones de biblioteca adicionales, como las del
java.util.concurrent
paquete de Java , que incluye una variedad de estructuras de datos altamente concurrentes y construcciones de agrupación de subprocesos. Estos se pueden combinar con técnicas adicionales como el confinamiento de hilos y la inmutabilidad efectiva. Ver Concurrencia Java en la práctica de Goetz et. Alabama. para mayor discusión. Desafortunadamente, muchos programadores todavía están desarrollando sus propias estructuras de datos con bloqueos y condiciones, cuando realmente deberían estar usando algo como ConcurrentHashMap donde los autores de la biblioteca ya han realizado el trabajo pesado.Todo lo anterior comparte algunas características importantes: tienen múltiples hilos de control que interactúan sobre un estado mutable compartido globalmente . El problema es que la programación en este estilo sigue siendo muy propensa a errores. Es bastante fácil que un pequeño error pase desapercibido, lo que resulta en un mal comportamiento que es difícil de reproducir y diagnosticar. Puede ser que ningún programador sea "suficientemente cuidadoso y diligente" para desarrollar sistemas grandes de esta manera. Al menos, muy pocos lo son. Por lo tanto, diría que , si es posible, se debe evitar la programación multiproceso con estado compartido y mutable.
Lamentablemente, no está del todo claro si se puede evitar en todos los casos. Todavía se realiza mucha programación de esta manera. Sería bueno ver esto suplantado por otra cosa. Las respuestas de Jarrod Roberson y davidk01 apuntan a técnicas tales como datos inmutables, programación funcional, STM y transmisión de mensajes. Hay mucho para recomendarlos, y todos se están desarrollando activamente. Pero no creo que hayan reemplazado completamente el buen estado mutable pasado de moda por el momento.
EDITAR: aquí está mi respuesta a las preguntas específicas al final.
No sé mucho sobre OpenMP. Mi impresión es que puede ser muy efectivo para problemas altamente paralelos como las simulaciones numéricas. Pero no parece de uso general. Las construcciones de semáforos parecen de nivel bastante bajo y requieren que el programador mantenga la relación entre semáforos y estructuras de datos compartidas, con todos los problemas que describí anteriormente.
Si tiene un algoritmo paralelo que utiliza semáforos, no conozco ninguna técnica general para transformarlo. Es posible que pueda refactorizarlo en objetos y luego construir algunas abstracciones a su alrededor. Pero si quieres usar algo como pasar mensajes, creo que realmente necesitas reconceptualizar todo el problema.
fuente
Respuesta a la pregunta
El consenso general es que el estado mutable compartido es Bad ™, y el estado inmutable es Good ™, que está demostrado que es preciso y verdadero una y otra vez por lenguajes funcionales y lenguajes imperativos también.
El problema es que los idiomas imperativos convencionales simplemente no están diseñados para manejar esta forma de trabajo, las cosas no van a cambiar para esos idiomas durante la noche. Aquí es donde la comparación
GOTO
es defectuosa. El estado inmutable y el paso de mensajes es una gran solución, pero tampoco es una panacea.Premisa defectuosa
Esta pregunta se basa en comparaciones con una premisa defectuosa; ¡Ese
GOTO
era el problema real y la Junta Universal Intergalatic de Diseñadores de Lenguaje y Uniones de Ingeniería de Software lo desaprobó universalmente ! Sin unGOTO
mecanismo, ASM no funcionaría en absoluto. Lo mismo ocurre con la premisa de que los punteros en bruto son el problema con C o C ++ y, de alguna manera, los punteros inteligentes son una panacea, no lo son.GOTO
no era el problema, los programadores eran el problema. Lo mismo ocurre con el estado mutable compartido . El problema en sí mismo no es el problema , sino los programadores que lo usan. Si hubiera una forma de generar código que usara un estado mutable compartido de una manera que nunca tuviera condiciones de carrera o errores, entonces no sería un problema. Al igual que si nunca escribes código de espaguetiGOTO
o construcciones equitativas, tampoco es un problema.La educación es la panacea
Idiot programadores son lo que eran
deprecated
, cada lenguaje popular aún tiene laGOTO
construcción, ya sea directa o indirectamente, y es unabest practice
cuando se utiliza correctamente en todos los idiomas que tiene este tipo de construcciones.EJEMPLO: Java tiene etiquetas y
try/catch/finally
ambas funcionan directamente comoGOTO
declaraciones.La mayoría de los programadores de Java con los que hablo ni siquiera saben lo que
immutable
realmente significa fuera de ellos repitiendothe String class is immutable
con una mirada de zombie en sus ojos. Definitivamente no saben cómo usar lafinal
palabra clave correctamente para crear unaimmutable
clase. Así que estoy bastante seguro de que no tienen idea de por qué la transmisión de mensajes mediante mensajes inmutables es tan buena y por qué el estado mutable compartido no es tan bueno.fuente
La última furia en los círculos académicos parece ser la Memoria Transaccional de Software (STM) y promete quitar todos los detalles meticulosos de la programación multiproceso de las manos de los programadores mediante el uso de tecnología de compilación suficientemente inteligente. Detrás de escena todavía hay bloqueos y semáforos, pero usted, como programador, no tiene que preocuparse por eso. Los beneficios de ese enfoque aún no están claros y no hay contendientes obvios.
Erlang utiliza el paso de mensajes y agentes para la concurrencia y ese es un modelo más simple para trabajar que STM. Al pasar el mensaje, no tiene absolutamente ningún bloqueo ni semáforo de qué preocuparse porque cada agente opera en su propio mini universo, por lo que no hay condiciones de carrera relacionadas con los datos. Todavía tienes algunos casos extraños, pero no son tan complicados como los bloqueos en vivo y los puntos muertos. Los lenguajes JVM pueden hacer uso de Akka y obtener todos los beneficios de pasar mensajes y actores, pero a diferencia de Erlang, JVM no tiene soporte incorporado para actores, por lo que al final del día Akka todavía usa hilos y bloqueos, pero usted como el programador no tiene que preocuparse por eso.
El otro modelo que conozco que no usa bloqueos e hilos es el uso de futuros, que en realidad es solo otra forma de programación asíncrona.
No estoy seguro de qué cantidad de esta tecnología está disponible en C ++, pero es probable que si está viendo algo que no utiliza explícitamente subprocesos y bloqueos, será una de las técnicas anteriores para administrar la concurrencia.
fuente
Creo que esto se trata principalmente de niveles de abstracción. Muy a menudo en la programación, es útil abstraer algunos detalles de una manera que sea más segura o más legible o algo así.
Esto se aplica a las estructuras de control:
if
s,for
s, e inclusotry
-catch
bloques son sólo abstracciones más degoto
s. Estas abstracciones son casi siempre útiles, porque hacen que su código sea más legible. Pero hay casos en los que aún necesitará usargoto
(por ejemplo, si está escribiendo el ensamblaje a mano).Esto también se aplica a la gestión de memoria: los punteros inteligentes C ++ y GC son abstracciones sobre punteros sin formato y desasignación / memoria manual de memoria. Y a veces, estas abstracciones no son apropiadas, por ejemplo, cuando realmente necesita el máximo rendimiento.
Y lo mismo se aplica a los subprocesos múltiples: cosas como futuros y actores son solo abstracciones sobre hilos, semáforos, mutexes e instrucciones CAS. Tales abstracciones pueden ayudarlo a hacer que su código sea mucho más legible y también lo ayudan a evitar errores. Pero a veces, simplemente no son apropiados.
Debe saber qué herramientas tiene disponibles y cuáles son sus ventajas y desventajas. Luego puede elegir la abstracción correcta para su tarea (si corresponde). Los niveles más altos de abstracción no desprecian los niveles más bajos, siempre habrá algunos casos en los que la abstracción no sea apropiada y la mejor opción es usar la "forma antigua".
fuente
Sí, pero no es probable que te encuentres con algunos de ellos.
En los viejos tiempos, era común usar métodos de bloqueo (sincronización de barrera) porque escribir buenos mutexes era difícil de hacer bien. Todavía puede ver rastros de esto en cosas recientes. El uso de bibliotecas de concurrencia modernas le brinda un conjunto de herramientas mucho más rico y probado para la paralelización y la coordinación entre procesos.
Del mismo modo, una práctica más antigua era escribir código tortuoso de modo que pudieras descubrir cómo paralelizarlo manualmente. Esta forma de optimización (potencialmente dañina, si se equivoca) también ha desaparecido en gran medida con el advenimiento de compiladores que hacen esto por usted, desenrollando bucles si es necesario, siguiendo las ramas de forma predictiva, etc. Sin embargo, esta no es una tecnología nueva. , estar al menos 15 años en el mercado. Aprovechar cosas como los grupos de subprocesos también evita algún código realmente engañoso de antaño.
Entonces, tal vez la práctica obsoleta es escribir el código de concurrencia usted mismo, en lugar de usar bibliotecas modernas y bien probadas.
fuente
Grand Central Dispatch de Apple es una elegante abstracción que cambió mi forma de pensar sobre la concurrencia. Su enfoque en las colas hace que la implementación de la lógica asincrónica sea un orden de magnitud más simple, en mi humilde experiencia.
Cuando programo en entornos donde está disponible, ha reemplazado la mayoría de mis usos de hilos, bloqueos y comunicación entre hilos.
fuente
Uno de los principales cambios en la programación paralela es que las CPU son tremendamente más rápidas que antes, pero para lograr ese rendimiento, se requiere un caché bien lleno. Si intenta ejecutar varios subprocesos al mismo tiempo intercambiando entre ellos continuamente, casi siempre estará invalidando la memoria caché para cada subproceso (es decir, cada subproceso requiere datos diferentes para operar) y termina matando el rendimiento mucho más de lo que usted acostumbrado con CPUs más lentas.
Esta es una de las razones por las que los marcos asíncronos o basados en tareas (por ejemplo, Grand Central Dispatch o Intel's TBB) son más populares, ejecutan la tarea de código 1 a la vez, terminando antes de pasar a la siguiente; sin embargo, debe codificar cada uno cada tarea tomará poco tiempo a menos que desee atornillar el diseño (es decir, sus tareas paralelas están realmente en cola). Las tareas intensivas en CPU se pasan a un núcleo de CPU alternativo en lugar de procesarse en el único subproceso que procesa todas las tareas. También es más fácil de administrar si no hay realmente un procesamiento de subprocesos múltiples.
fuente