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).
fuente
Respuestas:
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.
"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!
fuente
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.
fuente
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.
fuente
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.
fuente
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).
fuente
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.
fuente
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:
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.
fuente
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:
Obtienes algo más como esto:
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".
fuente
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. :)
fuente
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