¿Existen prácticas obsoletas para la programación multiprocesador y multiproceso que ya no debería usar?

36

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?

DesarrolladorDon
fuente
¿De qué estás hablando exactamente? ¿Qué viste?
svick
44
No significa ser grosero, pero podrías haber eliminado los primeros tres párrafos. Realmente no tienen en cuenta su pregunta, y sobrepasan sus conclusiones y solo generarán muchos argumentos.
dbracey
1
Whoa, gran edición. He apuñalado una respuesta. La pregunta todavía vaga por GOTO, punteros, herencia y polimorfismo, pero en mi respuesta he dejado de lado estos problemas y me he centrado en la pregunta de "prácticas obsoletas".
Stuart Marks

Respuestas:

15

¿Existen técnicas y prácticas de programación concurrentes que ya no se deberían usar? Yo diría que .

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.concurrentpaquete 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.

Stuart Marks
fuente
Gracias, esta es una gran información. Revisaré las referencias y profundizaré en los conceptos que usted menciona que son nuevos para mí.
DesarrolladorDon
+1 para java.util.concurrent y acordó el comentario: ha estado en el JDK desde 1.5 y rara vez, si es que alguna vez, lo veo usado.
MebAlone
1
Deseo que resaltes lo importante que es no rodar tus propias estructuras cuando ya existen. Tantos, tantos errores ...
corsiKa
No creo que sea exacto decir: "Los semáforos son un avance sobre las interrupciones porque son construcciones de software (no relacionadas con el hardware) ". Los semáforos dependen de la CPU para implementar la instrucción Compare-and-Swap , o sus variantes multi-core .
Josh Pearce
@JoshPearce de semáforos curso son implementado utilizando construcciones de hardware, pero son una abstracción que es independiente de cualquier constructo de hardware en particular, como CAS, prueba-y-set, cmpxchng, etc.
Stuart Marcas
28

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 GOTOes 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 GOTOera el problema real y la Junta Universal Intergalatic de Diseñadores de Lenguaje y Uniones de Ingeniería de Software lo desaprobó universalmente ! Sin un GOTOmecanismo, 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.

GOTOno 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 espagueti GOTOo 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 la GOTOconstrucción, ya sea directa o indirectamente, y es una best practicecuando se utiliza correctamente en todos los idiomas que tiene este tipo de construcciones.

EJEMPLO: Java tiene etiquetas y try/catch/finallyambas funcionan directamente como GOTOdeclaraciones.

La mayoría de los programadores de Java con los que hablo ni siquiera saben lo que immutablerealmente significa fuera de ellos repitiendo the String class is immutablecon una mirada de zombie en sus ojos. Definitivamente no saben cómo usar la finalpalabra clave correctamente para crear una immutableclase. 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.

Comunidad
fuente
3
+1 Gran respuesta, claramente escrita y señalando el patrón subyacente de estado mutable. IUBLDSEU debería convertirse en un meme :)
Dibbeke
2
GOTO es una palabra en clave para "por favor, no, por favor, comience una guerra de llamas aquí, doble perro te reto". Esta pregunta apaga las llamas pero en realidad no da una buena respuesta. Las menciones honoríficas de la programación funcional y la inmutabilidad son geniales, pero esas afirmaciones no tienen sentido.
Evan Plaice
1
Esto parece ser una respuesta contradictoria. Primero, dices "A es malo, B es bueno", luego dices "Los idiotas quedaron en desuso". ¿No se aplica lo mismo al primer párrafo? ¿No puedo tomar esa última parte de su respuesta y decir "El estado mutable compartido es una mejor práctica cuando se usa correctamente en todos los idiomas". Además, "prueba" es una palabra muy fuerte. No debe usarlo a menos que tenga evidencia realmente sólida.
luiscubal
2
No era mi intención iniciar una guerra de llamas. Hasta que Jarrod reaccionó a mi comentario, había pensado que GOTO no era controvertido y que funcionaría bien en una analogía. Cuando escribí la pregunta, no se me ocurrió, pero Dijkstra estaba en la zona cero tanto en GOTO como en semáforos. Edsger Dijkstra me parece un gigante, y se le atribuye la invención de los semáforos (1965) y el trabajo académico temprano (1968) sobre GOTO. El método de defensa de Dijkstra fue a menudo crujiente y conflictivo. La controversia / confrontación funcionó para él, pero solo quiero ideas sobre posibles alternativas a los semáforos.
DesarrolladorDon
1
Se supone que muchos programas modelan cosas que, en el mundo real, son mutables. Si a las 5:37 am, el objeto # 451 mantiene el estado de algo en el mundo real en ese momento (5:37 am), y el estado de la cosa del mundo real cambia posteriormente, es posible que la identidad del objeto que representa el estado de la cosa del mundo real como inmutable (es decir, la cosa siempre estará representada por el objeto # 451), o para que el objeto # 451 sea inmutable, pero no ambos. En muchos casos, tener la identidad inmutable será más útil que tener el objeto # 451 inmutable.
supercat
27

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.

davidk01
fuente
+1 para el nuevo término "detalles peludos". Jajaja hombre. No puedo dejar de reírme de este nuevo término. Supongo que voy a usar "código peludo" de ahora en adelante.
Saeed Neamati
1
@Saeed: He escuchado esa expresión antes, no es tan raro. Aunque estoy de acuerdo en que es gracioso :-)
Cameron
1
Buena respuesta. Supuestamente, la CLI de .NET también es compatible con la señalización (a diferencia del bloqueo), pero todavía tengo que encontrar un ejemplo en el que reemplaza completamente el bloqueo. No estoy seguro si async cuenta. Si está hablando de plataformas como Javascript / NodeJs, en realidad son de un solo subproceso y solo son mejores con altas cargas de E / S porque son mucho menos susceptibles a maximizar los límites de recursos (es decir, en una tonelada de contextos desechables). En las cargas intensivas de CPU, el uso de la programación asíncrona es poco o nada beneficioso.
Evan Plaice
1
Respuesta interesante, no había encontrado futuros antes. También tenga en cuenta que aún puede tener un punto muerto y un bloqueo en vivo en los sistemas de transmisión de mensajes como Erlang . CSP le permite razonar formalmente sobre el punto muerto y el bloqueo directo, pero no lo impide por sí mismo.
Mark Booth
1
Agregaría las estructuras de datos Lock free y wait free a esa lista.
stonemetal
3

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: ifs, fors, e incluso try- catchbloques son sólo abstracciones más de gotos. 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á usar goto(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".

svick
fuente
Gracias, está entendiendo la analogía, y no tengo una idea preconcebida ni siquiera una hacha para analizar si la respuesta de los semáforos de WRT es que están o no en desuso. Las preguntas más importantes para mí son si hay mejores formas y en sistemas que no parecen tener semáforos que faltan algo importante y serían incapaces de hacer la gama completa de algoritmos de subprocesos múltiples.
DesarrolladorDon
2

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.

Alex Feinman
fuente
Gracias. Parece que hay un gran potencial para el uso de la programación concurrente, pero podría ser una caja de Pandora si no se usa de manera disciplinada.
DesarrolladorDon
2

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.

orip
fuente
1

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.

gbjbaanb
fuente
Genial, gracias por las referencias a la tecnología de Apple e Intel. ¿Su respuesta señala los desafíos de administrar el hilo hasta la afinidad central? Algunos problemas de rendimiento de caché se alivian porque los procesadores multinúcleo pueden repetir cachés L1 por núcleo. Por ejemplo: software.intel.com/en-us/articles/… El caché de alta velocidad para cuatro núcleos con más aciertos de caché puede ser más de 4 veces más rápido que un núcleo con más errores de caché en los mismos datos. La multiplicación de matrices puede. La programación aleatoria de 32 hilos en 4 núcleos no puede. Usemos afinidad y obtengamos 32 núcleos.
DesarrolladorDon
en realidad no es el mismo problema: la afinidad central solo se refiere al problema en el que una tarea rebota de núcleo a núcleo. Es el mismo problema si una tarea se interrumpe, se reemplaza por una nueva, luego la tarea original continúa en el mismo núcleo. Intel dice allí: aciertos de caché = rápido, errores de caché = lento, independientemente de la cantidad de núcleos. Creo que están tratando de persuadirte para que compres sus chips en lugar de AMD :)
gbjbaanb