what threads thread que programming cooperative multithreading concurrency design-patterns

multithreading - threads - ¿Cuáles son los riesgos comunes de simultaneidad?



programming threads (21)

Estoy buscando educar a nuestro equipo en concurrencia. ¿Cuáles son los obstáculos más comunes que los desarrolladores caen en la concurrencia circundante? Por ejemplo, en .Net, la palabra clave static abre la puerta a una gran cantidad de problemas de concurrencia.

¿Hay otros patrones de diseño que no sean seguros para subprocesos?

Actualizar

Aquí hay tantas respuestas excelentes que es difícil seleccionar solo una como respuesta aceptada. Asegúrese de desplazarse por todas ellas para obtener excelentes consejos.



Algunas reglas generales:

(1) Vigile el contexto cuando declara una variable

  • Los atributos de escritura a clase ( estáticos ) deben sincronizarse
  • Escribir en atributos de instancia tiene que estar sincronizado
  • Mantenga todas las variables lo más localizadas posible (no las ponga en un contexto miembro a menos que tenga sentido)
  • Marcar variables que son de solo lectura inmutables

(2) Bloquee el acceso a clase mutable o atributos de instancia: las variables que son parte de la misma invariant deben estar protegidas por el mismo bloqueo.

(3) Evite el bloqueo controlado doble

(4) Mantenga bloqueos cuando ejecute una operación distribuida (subrutinas de llamada).

(5) Evitar la espera ocupada

(6) Mantenga la carga de trabajo baja en secciones sincronizadas

(7) No permita tomar el control del cliente mientras esté en un bloque sincronizado.

(8) Comentarios! Esto realmente ayuda a entender lo que el otro tipo tenía en mente al declarar esta sección sincronizada o esa variable inmutable.



Aquí hay un gran recurso sobre concurrencia, específicamente en Java: http://tech.puredanger.com/ Alex Miller enumera muchos problemas diferentes que uno puede encontrar al tratar con la concurrencia. Muy recomendable :)


Como se indica en otras respuestas, los dos problemas más probables son puntos muertos y condiciones de carrera . Sin embargo, mi principal consejo sería que si buscas formar a un equipo sobre el tema de la concurrencia, te recomiendo encarecidamente que te capacites . Obtenga un buen libro sobre el tema, no confíe en unos pocos párrafos de un sitio web. Un buen libro dependería del idioma que esté utilizando: "Java Concurrency in Practice" de Brian Goetz es bueno para ese idioma, pero hay muchos otros.


Compostabilidad En cualquier sistema no trivial, los enfoques ad-hoc para la sincronización dentro de diferentes subsistemas hacen que la interacción entre ellos sea generalmente propensa a errores y ocasionalmente imposible. Vea este video para ver un ejemplo de cómo incluso el código más trivial es propenso a estos problemas.

Personalmente, soy un converso al modelo Actor de computación concurrente (la variedad asincrónica).


El bloqueo verificado por duplicado está broken , al menos en Java. Comprender por qué esto es cierto y cómo puede solucionarlo lo lleva a comprender profundamente los problemas de concurrencia y el modelo de memoria de Java.


El escollo n. ° 1 que he visto es el intercambio de datos.

Creo que una de las mejores formas de manejar la concurrencia es con múltiples procesos en lugar de hilos. De esta manera, la comunicación entre hilos / procesos está estrictamente limitada a la tubería elegida, la cola de mensajes u otro método de comunicación.


En mi experiencia, muchos desarrolladores (capacitados) carecen de conocimiento fundamental sobre la teoría de concurrencia. Los libros de texto clásicos sobre los sistemas operativos de Tanenbaum o Stallings hacen un buen trabajo al explicar la teoría y las implicaciones de la concurrencia: exclusión mutua, sincronización, interbloqueos e inanición. Un buen fondo teórico es obligatorio para trabajar con éxito con concurrencia.

Dicho esto, el soporte de simultaneidad varía mucho entre los lenguajes de programación y las diferentes bibliotecas. Además, el desarrollo basado en pruebas no te lleva demasiado lejos en la detección y solución de problemas de concurrencia (aunque las fallas transitorias de prueba indican problemas de concurrencia).


Enseño mucho concurrencia a mis amigos y compañeros de trabajo. Estas son algunas de las grandes trampas:

  • Suponiendo que una variable que se lee principalmente en muchos hilos y solo está escrita en un hilo no necesita ser bloqueada. (En Java, esta situación puede hacer que los hilos de lectura nunca vean el nuevo valor).
  • Suponiendo que los hilos se ejecutarán en un orden particular.
  • Suponiendo que los hilos se ejecutarán simultáneamente.
  • Suponiendo que los hilos NO se ejecutarán simultáneamente.
  • Suponiendo que todos los subprocesos avancen antes de que finalice alguno de los subprocesos.

También veo:

  • Grandes confusiones entre thread_fork() y fork() .
  • Confusiones cuando la memoria se asigna en un hilo y free() d en otro hilo.
  • Confusiones resultantes del hecho de que algunas bibliotecas son enhebrables y otras no.
  • Las personas que usan bloqueos giratorios cuando deberían usar el modo de suspensión y sueño despierto, o el de selección, o cualquier mecanismo de bloqueo que admita su idioma.

Esto no es un escollo, sino más bien un consejo basado en las respuestas de los demás. El lector de textos de .NET frameworkwriterlockslim mejorará drásticamente su rendimiento en muchos casos sobre la declaración de "bloqueo", mientras se vuelve a ingresar.


La concurrencia no tiene muchas trampas.

Sin embargo, sincronizar el acceso a datos compartidos es complicado.

Aquí hay algunas preguntas que cualquier persona que escriba el código de sincronización de datos compartidos debería ser capaz de responder:

  1. ¿Qué es InterlockedIncrement?
  2. ¿Por qué InterlockedIncrement necesita existir en un nivel de lenguaje ensamblador?
  3. ¿Qué se lee reordenando?
  4. ¿Cuál es la palabra clave volátil (en c ++) y cuándo necesita usarla?
  5. ¿Qué es una jerarquía de sincronización?
  6. ¿Cuál es el problema ABA?
  7. ¿Qué es la coherencia del caché?
  8. ¿Qué es una barrera de memoria?

La concurrencia de "todo compartido" es una abstracción extremadamente permeable. En su lugar, adopte el mensaje de mensaje de nada compartido .


Llamar a clases públicas desde dentro de un bloqueo causando un bloqueo de seguridad

public class ThreadedClass { private object syncHandle = new object(); public event EventHandler Updated = delegate { }; public int state = 0; public void DoSmething() { lock(syncHandle) { // some locked code state = 1; Updated(this, EventArgs.Empty); } } public int State { get { int returnVal; lock(syncHandle) returnVal = state; return returnVal; } } }

No puede estar seguro de lo que su cliente va a llamar, lo más probable es que intenten leer la propiedad del Estado. Haz esto en cambio

public void DoSmething() { lock(syncHandle) { // some locked code state = 1; } // this should be outside the lock Updated(this, EventArgs.Empty); }


Los problemas de simultaneidad son increíblemente difíciles de depurar. Como medida preventiva, uno puede rechazar por completo el acceso a objetos compartidos sin utilizar mutexes de tal manera que los programadores puedan seguir fácilmente las reglas. He visto esto hecho haciendo envoltorios alrededor de los mutexes y semáforos proporcionados por el sistema operativo, etc.

Aquí hay algunos ejemplos confusos de mi pasado:

Solía ​​desarrollar controladores de impresora para Windows. Para evitar que varios hilos escriban en la impresora al mismo tiempo, nuestro monitor de puerto usó una construcción como esta: // pseudo código porque no recuerdo el API BOOL OpenPort () {GrabCriticalSection (); } BOOL ClosePort () {ReleaseCriticalSection (); } BOOL WritePort () {writestuff (); }

Desafortunadamente, cada llamada a WritePort era de un hilo diferente en el grupo de subprocesos de la cola de impresión. Eventualmente entramos en una situación en la que OpenPort y ClosePort fueron llamados por diferentes subprocesos, lo que provocó un punto muerto. La solución para esto se deja como un ejercicio, porque no puedo recordar lo que hice.

También solía trabajar en el firmware de la impresora. La impresora en este caso usó un RTOS llamado uCOS (pronunciado ''moco'') de modo que cada función tenía su propia tarea (motor del cabezal de impresión, puerto serie, puerto paralelo, pila de red, etc.). Una versión de esta impresora tenía una opción interna que se conectaba a un puerto serie en la placa madre de la impresora. En algún momento, se descubrió que la impresora leería el mismo resultado dos veces desde este periférico, y cada valor posterior estaría fuera de secuencia. (Por ejemplo, el periférico leía una secuencia 1,7,3,56,9,230 pero veríamos 1,7,3,3,56,9,230. Este valor se informaba a la computadora y se colocaba en una base de datos, por lo que tenía un montón de documentos con números de ID incorrectos era Muy malo) La causa raíz de esto fue una falla al respetar el mutex que protegía el buffer de lectura para el dispositivo. (De ahí mi consejo al comienzo de esta respuesta)


También podría ver problemas de simultaneidad de tipo fuera de proceso
Por ejemplo:
Un proceso de escritura que crea un archivo y un proceso de consumidor que reclama el archivo.


Todo se reduce a datos compartidos / estado compartido. Si no comparte datos o estados, entonces no tiene problemas de simultaneidad.

La mayoría de las personas, cuando piensan en la simultaneidad, piensan en multithreading en un solo proceso.

Una forma de pensar sobre esto es qué sucede si divide su proceso en múltiples procesos. ¿Dónde tienen que comunicarse entre ellos? Si puede tener claro dónde se deben comunicar los procesos, entonces tiene una buena idea sobre los datos que comparten.

Ahora, como prueba mental, mueva esos procesos múltiples a máquinas individuales. ¿Sus patrones de comunicación siguen siendo correctos? ¿Todavía puedes ver cómo hacerlo funcionar? Si no, uno podría querer reconsiderar múltiples hilos.

(El resto de esto no se aplica al enhebrado de Java, que no uso y, por lo tanto, sé muy poco).

El otro lugar donde uno puede quedar atrapado es, si usa bloqueos para proteger datos compartidos, debe escribir un monitor de bloqueo que pueda encontrar puntos muertos para usted. Entonces necesita que su programa (s) se ocupe de la posibilidad de interbloqueos. Cuando obtiene un error de interbloqueo, debe liberar todos los bloqueos, realizar una copia de seguridad e intentar nuevamente.

Es poco probable que haga que las cerraduras múltiples funcionen bien sin lo contrario, sin un nivel de cuidado que es bastante raro en los sistemas reales.

¡Buena suerte!


Una es la condición de carrera , que básicamente supone que se ejecutará una pieza de código antes / después de otra pieza de código concurrente.

También hay Deadlocks , es decir, el código A espera que el código B libere el recurso Y, mientras que el código B espera que A libere el recurso X.


Una falla de programación concurrente es una encapsulación incorrecta que conduce a carreras y puntos muertos . Esto probablemente puede suceder de muchas maneras diferentes, aunque hay dos en particular que he visto:

  1. Dando variables innecesariamente amplio alcance. Por ejemplo, a veces las personas declaran una variable en el alcance de instancia cuando lo haría el alcance local. Esto puede crear el potencial para las carreras donde no necesita ninguna.

  2. Exponer bloqueos innecesariamente. Si no hay necesidad de exponer un candado, entonces considere mantenerlo escondido. De lo contrario, los clientes pueden usarlo y crear puntos muertos que podría haber evitado.

Aquí hay un ejemplo simple de # 1 arriba , uno que está muy cerca de algo que vi en el código de producción:

public class CourseService { private CourseDao courseDao; private List courses; public List getCourses() { this.courses = courseDao.getCourses(); return this.courses; } }

En este ejemplo, no hay necesidad de que la variable de courses tenga alcance de instancia, y ahora las llamadas concurrentes a getCourses() pueden tener carreras.


Una verdad a tener en cuenta es que incluso si los desarrolladores iniciales consiguen que su modelo de tareas funcione correctamente (lo cual es un gran síntoma), entonces el equipo de mantenimiento que sigue sin duda arruinará las cosas de forma inimaginable. La conclusión de eso es limitar los rastros de concurrencia a través de su sistema. Haga todo lo posible para asegurarse de que la mayoría de su sistema ignore felizmente que se está produciendo la concurrencia. Eso ofrece menos oportunidades para que las personas que no están familiarizadas con el modelo de tareas inadvertidamente arruinen las cosas.

Demasiado a menudo las personas se vuelven locas. Todo funciona en su propio hilo. El resultado final es que casi todas las partes del código deben estar íntimamente al tanto de los problemas de enhebrado. Obliga a código que de lo contrario es simple a estar plagado de confuscaciones de bloqueo y sincronización. Cada vez que veo esto, el sistema finalmente se convierte en un desastre inmanejable. Sin embargo, cada vez que he visto esto, los desarrolladores originales aún insisten en que fue un buen diseño :(

Al igual que la herencia múltiple, si desea crear un nuevo hilo / tarea, suponga que está equivocado hasta que se demuestre lo contrario. Ni siquiera puedo contar la cantidad de veces que he visto el patrón. El subproceso A llama al subproceso B, luego el subproceso B llama al subproceso C, luego el subproceso C llama a todas las D esperando una respuesta del subproceso anterior. Todo lo que el código está haciendo es hacer llamadas de función largas a través de diferentes hilos. No use hilos cuando las llamadas a funciones funcionen bien.

Recuerda siempre que Message Queues es tu mejor amigo cuando quieres trabajar al mismo tiempo.

Descubrí que la creación de una infraestructura central que maneja casi todos los problemas de concurrencia funciona mejor. Si hay hilos fuera de la infraestructura central que deben hablar con otra pieza de software, entonces deben pasar por la infraestructura central. De esta forma, el resto del sistema puede permanecer sin simpatía y los problemas de concurrencia pueden ser manejados por la (s) persona (s) que con suerte comprendan la concurrencia.


Uno de los mayores escollos es el uso de concurrencia en primer lugar. La simultaneidad agrega una carga general de diseño / depuración, por lo que realmente tiene que examinar el problema y ver si realmente requiere concurrencia.

La concurrencia es ciertamente inevitable en algunos dominios, pero cuando es evitable, evítela.


Ya hay muchas buenas respuestas y sugerencias en este hilo, pero déjame agregar una cosa.

NO CONFÍE EN LAS PRUEBAS PARA ENCONTRAR LAS CONDICIONES DE LA CARRERA Y LOS DESADLOQUES

Asumiendo que tiene todos los buenos procesos de desarrollo en su lugar: pruebas unitarias para cada componente, pruebas de humo para cada construcción nocturna, requiriendo que los cambios de cada desarrollador pasen las pruebas antes del registro, etc.

Todo eso está muy bien, pero lleva a una actitud de "bueno, pasó el banco de pruebas, por lo que no puede ser un error en mi código". que no te servirá bien en la programación concurrente. Los errores de concurrencia en tiempo real son tremendamente difíciles de reproducir. Puede ejecutar un código con una condición de carrera un billón de veces antes de que falle.

Deberá ajustar su proceso para poner mayor énfasis en las revisiones de los códigos, realizadas por sus mejores mentes. Tener una revisión de código separada solo para problemas de simultaneidad no es una mala idea.

Deberá poner más énfasis en hacer que su aplicación se depure por sí misma. Es decir, cuando obtiene una falla en el laboratorio de pruebas o en el sitio de su cliente, necesita asegurarse de que se capture y registre suficiente información para que pueda realizar una autopsia definitiva, ya que las probabilidades de que pueda reproducir el informe de error en tu conveniencia es insignificante.

Deberá poner más énfasis en las comprobaciones de cordura paranoide en su código para que se detecte una falla lo más cercana posible al problema y no 50,000 líneas de código.

Sé paranoico muy paranoico