Cómo explicar por qué el subprocesamiento múltiple es difícil

84

Soy un programador bastante bueno, mi jefe también es un programador bastante bueno. Aunque parece subestimar algunas tareas, como el subprocesamiento múltiple y lo difícil que puede ser (me resulta muy difícil para algo más que ejecutar algunos subprocesos, esperar a que todos terminen y luego devolver los resultados).

En el momento en que comienzas a tener que preocuparte por los puntos muertos y las condiciones de carrera, me resulta muy difícil, pero el jefe no parece apreciar esto, no creo que haya encontrado esto. Solo golpearlo es más o menos la actitud.

Entonces, ¿cómo puedo presentarlo o explicar por qué podría estar subestimando las complejidades de la concurrencia, el paralelismo y el subprocesamiento múltiple? O tal vez estoy equivocado?

Editar: solo un poco sobre lo que ha hecho: recorra una lista, para cada elemento de esa lista, cree un hilo que ejecute un comando de actualización de la base de datos basado en la información de ese elemento. No estoy seguro de cómo controlaba cuántos hilos ejecutados a la vez, supongo que debe haberlos agregado a una cola si hubiera demasiados en ejecución (no habría utilizado un semáforo).

Señor shoubs
fuente
17
Multi-threading es fácil. La sincronización correcta es difícil.
Vineet Reynolds
33
Traiga a tres personas a la sala, preferiblemente con diferentes acentos, y pídales que expliquen partes diferentes y superpuestas del problema de la concurrencia ... al mismo tiempo.
greyfade
El subprocesamiento múltiple puede ser muy difícil o muy fácil, dependiendo del problema en cuestión y del soporte del idioma. Clojure lo hace fácil clojure.org/concurrent_programming
Job
44
@Job La programación concurrente siempre es difícil (en proyectos del mundo real), sin importar el idioma que esté utilizando. Scala, Clojure o Erlang lo hacen un poco sensato cuando quieres compararlo con idiomas que usan y fomentan estados mutables.
Chiron
44
Mi metáfora favorita para esto es: "¿Tomarías una pastilla para dormir y un laxante al mismo tiempo?" Incluso usando colas de mensajes complejas, el orden es el fruto de la concurrencia bien hecha . Eso, a menos que tenga mucha experiencia con él, es difícil para muchas personas.
Tim Post

Respuestas:

29
  1. Si puede contar con alguna experiencia matemática, ilustre cómo un flujo de ejecución normal que es esencialmente determinista se convierte no solo en no determinista con varios subprocesos, sino también en exponencialmente complejo, porque debe asegurarse de que cada posible entrelazado de instrucciones de máquina siga haciendo lo correcto. Un ejemplo simple de una actualización perdida o una situación de lectura sucia es a menudo una revelación.

  2. "Abofetearlo" es la solución trivial ... resuelve todos sus problemas si no le preocupa el rendimiento. ¡Intenta ilustrar cuánto éxito sería si, por ejemplo, Amazon tuviera que bloquear toda la costa este cada vez que alguien en Atlanta ordena un libro!

Kilian Foth
fuente
1
+1 para la discusión de la complejidad matemática: así es como llegué a comprender la dificultad en la concurrencia de estado compartido, y es el argumento que generalmente hago al defender las arquitecturas de transmisión de mensajes. -1 para "abofetearlo" ... La frase connota un enfoque irreflexivo para el uso de bloqueos, lo que muy probablemente conducirá a un punto muerto o un comportamiento inconsistente (ya que los clientes de su código que viven en diferentes hilos hacen conflictos solicitudes, pero no se sincronizan entre sí, los clientes tendrán modelos incompatibles del estado de su biblioteca).
Aidan Cully
2
Amazon no tiene que bloquear el inventario de un artículo individual en un almacén brevemente al procesar un pedido. Si hay una carrera repentina y enorme en un artículo en particular, el rendimiento del pedido de ese artículo se verá afectado hasta que se agote el suministro y el acceso al inventario sea de solo lectura (y, por lo tanto, 100% compartible). Una cosa que Amazon está haciendo es que otros programas no lo hagan es la capacidad de poner en cola los pedidos hasta que ocurra un reabastecimiento y la opción de atender los pedidos en cola antes de que un reabastecimiento esté disponible para nuevos pedidos.
Blrfl
@Blrfl: los programas pueden hacer eso si están escritos para usar mensajes que pasan a través de colas. No es necesario que todos los mensajes a un hilo en particular pasen por una sola cola ...
Donal Fellows
44
@Donal Fellows: si hay 1M de widgets en stock en un almacén y los pedidos de 1M llegan en el mismo instante, todas esas solicitudes se serializan en algún nivel al tiempo que hacen coincidir los artículos con los pedidos, sin importar cómo se manejen. La realidad práctica es que Amazon probablemente nunca tenga tantos widgets en stock que la latencia en el procesamiento de una avalancha de pedidos sea inaceptablemente alta antes de que se agote el inventario y se pueda decir a todos los demás en línea (en paralelo), "estamos fuera". " Las colas de mensajes son una excelente manera de evitar puntos muertos, pero no resuelven el problema de la alta contención de un recurso limitado.
Blrfl
79

El subprocesamiento múltiple es simple. Codificar una aplicación para subprocesos múltiples es muy, muy fácil.

Hay un truco simple, y es usar una cola de mensajes bien diseñada ( no la suyas) para pasar datos entre hilos.

La parte difícil es tratar de que múltiples hilos actualicen mágicamente un objeto compartido de alguna manera. Es entonces cuando se vuelve propenso a errores porque la gente no presta atención a las condiciones de carrera que están presentes.

Muchas personas no usan colas de mensajes e intentan actualizar objetos compartidos y crear problemas para sí mismos.

Lo que se vuelve difícil es diseñar un algoritmo que funcione bien al pasar datos entre varias colas. Eso es difícil. Pero la mecánica de los hilos coexistentes (a través de colas compartidas) es fácil.

Además, tenga en cuenta que los hilos comparten recursos de E / S. Es poco probable que un programa enlazado de E / S (es decir, conexiones de red, operaciones de archivo u operaciones de base de datos) vaya más rápido con muchos subprocesos.

Si desea ilustrar el problema de actualización de objetos compartidos, eso es simple. Siéntate al otro lado de la mesa con un montón de tarjetas de papel. Escriba un conjunto simple de cálculos, 4 o 6 fórmulas simples, con mucho espacio en la página.

Aquí está el juego. Cada uno lee una fórmula, escribe una respuesta y coloca una tarjeta con la respuesta.

Cada uno de ustedes hará la mitad del trabajo, ¿verdad? Has terminado en la mitad del tiempo, ¿verdad?

Si su jefe no piensa mucho y simplemente comienza, terminará en conflicto de alguna manera y ambos escriben respuestas a la misma fórmula. Eso no funcionó porque hay una condición de carrera inherente entre los dos que leen antes de escribir. Nada les impide leer la misma fórmula y sobrescribir las respuestas de los demás.

Hay muchas, muchas formas de crear condiciones de carrera con recursos mal o desbloqueados.

Si desea evitar todos los conflictos, corte el papel en una pila de fórmulas. Sacas uno de la cola, escribes la respuesta y publicas las respuestas. No hay conflictos porque ambos leyeron de una cola de mensajes de un solo lector.

S.Lott
fuente
Incluso cortar el papel en una pila no soluciona totalmente las cosas: todavía tienes la situación en la que tú y tu jefe buscan una nueva fórmula al mismo tiempo y golpeas los nudillos con los suyos. De hecho, diría que esto es representativo del tipo más común de problema de subprocesamiento. Los errores realmente graves se encuentran temprano. Los errores realmente inusuales permanecen para siempre porque nadie puede reproducirlos. Las condiciones de carrera plausibles, como esta, siguen apareciendo en las pruebas y, finalmente, todos (o más probablemente la mayoría) se resuelven.
Airsource Ltd
@AirsourceLtd ¿Qué estás diciendo exactamente al "golpear tus nudillos con los suyos"? Siempre que tenga una cola de mensajes que evite que dos hilos diferentes tomen el mismo mensaje, no sería un problema. A menos que no entienda lo que quieres decir.
Zack
25

La programación multiproceso es probablemente la solución más difícil para la concurrencia. Básicamente es una abstracción de nivel bastante bajo de lo que realmente hace la máquina.

Hay varios enfoques, como el modelo de actor o la memoria transaccional (de software) , que son mucho más fáciles. O trabajar con estructuras de datos inmutables (como listas y árboles).

En general, una separación adecuada de las preocupaciones facilita el subprocesamiento múltiple. Algo, que a menudo se olvida, cuando las personas generan 20 hilos, todos tratando de procesar el mismo búfer. Use reactores donde necesite sincronización y generalmente pase datos entre diferentes trabajadores con colas de mensajes.
Si tiene un bloqueo en la lógica de su aplicación, hizo algo mal.

Entonces sí, técnicamente, el subprocesamiento múltiple es difícil.
"Abofetearlo" es prácticamente la solución menos escalable para los problemas de concurrencia, y en realidad derrota todo el propósito del subprocesamiento múltiple. Lo que hace es revertir un problema a un modelo de ejecución no concurrente. Cuanto más lo hagas, más probable es que tengas solo un hilo ejecutándose a la vez (o 0 en un punto muerto). Derrota todo el propósito.
Esto es como decir: "Resolver los problemas del tercer mundo es fácil. Simplemente arroje una bomba sobre él". Solo porque hay una solución trivial, esto no hace que el problema sea trivial, ya que te importa la calidad del resultado.

Pero en la práctica, resolver estos problemas es tan difícil como cualquier otro problema de programación y se hace mejor con abstracciones apropiadas. Lo que lo hace bastante fácil de hecho.

back2dos
fuente
14

Creo que hay un ángulo no técnico para esta pregunta: la OMI es una cuestión de confianza. Por lo general, se nos pide que reproduzcamos aplicaciones complejas como, por ejemplo, Facebook, no sé. He llegado a la conclusión de que si tiene que explicar la complejidad de una tarea a los no iniciados / a la administración, entonces algo está podrido en Dinamarca.

Incluso si otros programadores ninja pudieran hacer la tarea en 5 minutos, sus estimaciones se basan en su capacidad personal. Su interlocutor debe aprender a confiar en su opinión sobre el asunto o contratar a alguien cuya palabra esté dispuesto a aceptar.

El desafío no es transmitir las implicaciones técnicas, que las personas tienden a ignorar o no pueden comprender a través de la conversación, sino establecer una relación de respeto mutuo.

sunwukung
fuente
1
Respuesta interesante, aunque es una pregunta técnica. Sin embargo, estoy de acuerdo con lo que dices ... sin embargo, en este caso, mi gerente es un programador bastante bueno, sin embargo, solo creo que porque no se ha encontrado con las complejidades de las aplicaciones multiproceso, las subestima.
Sr. Shoubs
6

Un simple experimento mental para comprender los callejones sin salida es el problema del " filósofo comedor ". Uno de los ejemplos que suelo usar para describir cuán malas pueden ser las condiciones de carrera es la situación de Therac 25 .

"Simplemente abofetearlo" es la mentalidad de alguien que no ha encontrado errores difíciles con subprocesos múltiples. Y es posible que él piense que está exagerando la gravedad de la situación (yo no, es posible hacer explotar cosas o matar personas con errores de condición de carrera, especialmente con un software integrado que termina en los automóviles).

Tangurena
fuente
1
es decir, el problema del sándwich: haces un montón de sándwiches, pero solo hay 1 plato de mantequilla y 1 cuchillo. En general, todo está bien, pero eventualmente alguien agarrará la mantequilla mientras que otra persona agarra el cuchillo ... y luego ambos se quedarán allí esperando que el otro suelte sus recursos.
gbjbaanb
¿Podrían resolverse problemas de punto muerto como ese siempre adquiriendo recursos en un orden establecido?
compman
@compman, no. Debido a que es posible que 2 subprocesos intenten obtener el mismo recurso en el mismo momento, y esos subprocesos no necesariamente necesitan el mismo conjunto de recursos, solo una superposición suficiente para causar problemas. Un esquema es poner el recurso "atrás" y luego esperar un período aleatorio antes de recuperarlo nuevamente. Este período de retroceso ocurre en varios protocolos, el primero de los cuales fue Aloha. en.wikipedia.org/wiki/ALOHAnet
Tangurena
1
¿Qué sucede si cada recurso en el programa tiene un número y cuando un hilo / proceso necesita un conjunto de recursos, siempre bloquea los recursos en orden numérico creciente? No creo que ese punto muerto pueda suceder.
compman
1
@compman: Esa es de hecho una forma de evitar un punto muerto. Es posible diseñar herramientas que le permitan verificar esto automáticamente; por lo tanto, si nunca se encuentra que su aplicación bloquea recursos que no sean en orden numérico creciente, entonces nunca tuvo un punto muerto potencial . (Tenga en cuenta que los puntos muertos potenciales solo se convierten en puntos muertos reales cuando su código se ejecuta en la computadora de un cliente).
gnasher729
3

Las aplicaciones concurrentes no son deterministas. Con la cantidad excepcionalmente pequeña de código general que el programador ha reconocido como vulnerable, usted no controla cuándo una parte de un hilo / proceso se ejecuta en relación con cualquier parte de otro hilo. La prueba es más difícil, lleva más tiempo y es poco probable que encuentre todos los defectos relacionados con la concurrencia. Los defectos, si se encuentran, son a menudo sutiles y no pueden reproducirse de manera consistente, por lo tanto, la reparación es difícil.

Por lo tanto, la única aplicación concurrente correcta es una que sea demostrablemente correcta, algo que no se practica a menudo en el desarrollo de software. Como resultado, la respuesta de S.Lot es el mejor consejo general, ya que la transmisión de mensajes es relativamente fácil de probar correcta.

Mattnz
fuente
3

Respuesta corta en dos palabras: NONDETERMINISMO OBSERVABLE

Respuesta larga: depende del enfoque de la programación concurrente que utilice dado su problema. En el libro Conceptos, técnicas y modelos de programación de computadoras , los autores explican claramente cuatro enfoques prácticos principales para escribir programas concurrentes:

  • Programación secuencial : un enfoque de línea de base que no tiene concurrencia;
  • Concurrencia declarativa : utilizable cuando no hay no determinismo observable;
  • Simultaneidad de paso de mensajes: mensaje concurrente que pasa entre muchas entidades, donde cada entidad procesa internamente el mensaje secuencialmente;
  • Concurrencia de estado compartido : actualización de subprocesos de objetos pasivos compartidos mediante acciones atómicas de grano grueso, por ejemplo, bloqueos, monitores y transacciones;

Ahora, el más fácil de estos cuatro enfoques, aparte de la programación secuencial obvia, es la concurrencia declarativa , porque los programas escritos con este enfoque no tienen un no determinismo observable . En otras palabras, no hay condiciones de carrera , ya que la condición de carrera es solo un comportamiento observable no determinista.

Pero la falta de no determinismo observable significa que hay algunos problemas que no podemos abordar utilizando la concurrencia declarativa. Aquí es donde entran en juego los dos últimos enfoques no tan fáciles. La parte no tan fácil es una consecuencia del no determinismo observable. Ahora ambos caen bajo un modelo concurrente con estado y también son equivalentes en expresividad. Pero debido al número cada vez mayor de núcleos por CPU, parece que la industria se ha interesado más recientemente en la concurrencia de envío de mensajes, como se puede ver en el aumento de las bibliotecas de envío de mensajes (por ejemplo, Akka para JVM) o lenguajes de programación (por ejemplo, Erlang ) .

La biblioteca de Akka mencionada anteriormente, que está respaldada por un modelo teórico de actor, simplifica la creación de aplicaciones concurrentes, ya que ya no tiene que lidiar con bloqueos, monitores o transacciones. Por otro lado, requiere un enfoque diferente para diseñar la solución, es decir, pensar en una forma de componer jerárquicamente a los actores. Se podría decir que requiere una mentalidad totalmente diferente, que al final puede ser aún más difícil que usar la concurrencia compartida de estado simple.

La programación concurrente es difícil debido al no determinismo observable, pero cuando se usa el enfoque correcto para el problema dado y la biblioteca correcta que admite ese enfoque, se pueden evitar muchos problemas.

Jernej Jerin
fuente
0

Primero me enseñaron que podría generar problemas al ver un programa simple que inició 2 hilos y los hizo imprimir a la consola al mismo tiempo de 1 a 100. En lugar de:

1
1
2
2
3
3
...

Obtienes algo más como esto:

1
2
1
3
2
3
...

Ejecútelo nuevamente y puede obtener resultados totalmente diferentes.

La mayoría de nosotros hemos sido entrenados para asumir que nuestro código se ejecutará secuencialmente. Con la mayoría de los subprocesos múltiples no podemos dar esto por sentado "fuera de la caja".

Morgan Herlocker
fuente
-3

Intente usar varios martillos para machacar en un puñado de clavos muy separados a la vez sin cierta comunicación entre quienes sostienen los martillos ... (suponga que tienen los ojos vendados).

Escale esto para construir una casa.

Ahora intenta dormir por la noche imaginando que eres el arquitecto. :)

Macke
fuente
-3

Parte fácil: use subprocesos múltiples con características contemporáneas de marcos, sistemas operativos y hardware, como semáforos, colas, contadores enclavados, tipos atómicos en caja, etc.

Parte difícil: implementar las funciones por sí mismo sin utilizar características en primer lugar, puede ser, excepto algunas capacidades muy limitadas de hardware, confiando, por ejemplo, en garantías de coherencia de reloj en múltiples núcleos.


fuente
3
La parte difícil es realmente más difícil, pero incluso esa parte fácil no es tan fácil.
PeterAllenWebb