multithreading - threads - Mejor enfoque/metodología de programación para garantizar la seguridad del hilo
manejo de threads en python (15)
- Evite compartir datos entre subprocesos donde sea posible (copie todo).
- Nunca se bloquea en llamadas a métodos a objetos externos, siempre que sea posible.
- Mantenga bloqueos por el menor tiempo posible.
Cuando estaba aprendiendo Java proveniente de un entorno de unos 20 años de programación de procedimientos con básico, Pascal, COBOL y C, pensé en ese momento que lo más difícil era entender la jerga y los conceptos de OOP. Ahora, con unos 8 años de Java sólido en mi haber, he llegado a la conclusión de que lo más difícil de programar en Java y en lenguajes similares, como C #, son los aspectos multiproceso / concurrente.
¡Codificar aplicaciones multiproceso fiables y escalables es simplemente difícil! Y con la tendencia de que los procesadores crezcan "más" en lugar de más rápido, se está convirtiendo rápidamente en algo absolutamente crítico.
El área más difícil es, por supuesto, controlar las interacciones entre los hilos y los errores resultantes: interbloqueos, condiciones de carrera, datos obsoletos y latencia.
Así que mi pregunta para usted es la siguiente: ¿qué enfoque o metodología emplea para producir un código concurrente seguro mientras se mitiga el potencial de bloqueos, latencia y otros problemas? He llegado a un enfoque que es poco convencional pero que ha funcionado muy bien en varias aplicaciones grandes, que compartiré en una respuesta detallada a esta pregunta.
Algunos expertos creen que la respuesta a su pregunta es evitar los hilos por completo, porque es casi imposible evitar problemas imprevistos. Para citar el problema con los hilos :
Desarrollamos un proceso que incluía un sistema de calificación de madurez del código (con cuatro niveles, rojo, amarillo, verde y azul), revisiones de diseño, revisiones de códigos, compilaciones nocturnas, pruebas de regresión y métricas de cobertura de códigos automáticos. La porción del kernel que aseguró una vista consistente de la estructura del programa se escribió a principios de 2000, el diseño se revisó en amarillo y el código se revisó en verde. Los revisores incluyeron expertos en simultaneidad, no solo estudiantes de postgrado inexpertos (Christopher Hylands (ahora Brooks), Bart Kienhuis, John Reekie y [Ed Lee] fueron todos revisores). Escribimos pruebas de regresión que lograron una cobertura del código del 100 por ciento ... El sistema en sí comenzó a ser ampliamente utilizado, y cada uso del sistema ejerció este código. No se observaron problemas hasta que el código se estancó el 26 de abril de 2004, cuatro años después.
El modelo de actor es lo que estás usando y es, de lejos, el modo más simple (y eficiente) de hacer subprocesos múltiples. Básicamente, cada hilo tiene una cola (sincronizada) (puede ser dependiente del sistema operativo o no) y otros hilos generan mensajes y los ponen en la cola del hilo que manejará el mensaje.
Ejemplo básico:
thread1_proc() {
msg = get_queue1_msg(); // block until message is put to queue1
threat1_msg(msg);
}
thread2_proc() {
msg = create_msg_for_thread1();
send_to_queue1(msg);
}
Es un ejemplo típico del problema del consumidor productor .
El enfoque más seguro para diseñar nuevas aplicaciones con múltiples subprocesos es cumplir con la regla:
Sin diseño debajo del diseño.
Qué significa eso?
Imagine que ha identificado los principales componentes de su aplicación. Que sea la GUI, algunos motores de computación. Normalmente, una vez que tiene un tamaño de equipo lo suficientemente grande, algunas personas en el equipo solicitarán "bibliotecas" para "compartir el código" entre esos componentes principales. Si bien fue relativamente fácil al principio definir las reglas de enhebrado y colaboración para los principales bloques de construcción, todo ese esfuerzo está ahora en peligro ya que las "bibliotecas de reutilización de códigos" estarán mal diseñadas, diseñadas cuando sea necesario y cubiertas con bloqueos y mutexes que "sentirse bien". Esas bibliotecas ad-hoc son el diseño debajo de su diseño y el mayor riesgo para su arquitectura de enhebrado.
¿Qué hacer al respecto?
- Dígales que prefiere duplicar el código que el código compartido a través de los límites del hilo.
- Si cree que el proyecto realmente se beneficiará de algunas bibliotecas, establezca la regla de que deben ser libres de estado y reentrantes.
- Su diseño está evolucionando y parte de ese "código común" podría "moverse hacia arriba" en el diseño para convertirse en un nuevo componente principal de su aplicación.
- Aléjate de la genial biblioteca en la web manía. Algunas bibliotecas de terceros realmente pueden ahorrarle mucho tiempo. Pero también hay una tendencia a que cualquiera tenga sus "favoritos", que son apenas esenciales. Y con cada biblioteca de terceros que agregue, aumenta el riesgo de que se produzcan problemas de subprocesamiento.
Por último, considere tener alguna interacción basada en mensajes entre sus principales bloques de construcción; ver el modelo de actor mencionado a menudo, por ejemplo.
Es claramente un problema difícil. Además de la obvia necesidad de ser cuidadoso, creo que el primer paso es definir con precisión qué hilos necesitas y por qué.
Diseñe los hilos como diseñaría las clases: asegúrese de saber qué los hace consistentes: sus contenidos y sus interacciones con otros hilos.
Escribiendo todo el código en una aplicación multihilo muy ... ¡con cuidado! No conozco ninguna mejor respuesta que eso. (Esto involucra cosas como jonnii mencionado).
He escuchado a la gente discutir (y estar de acuerdo con ellos) que el modelo de enhebrado tradicional realmente no funcionará en el futuro, por lo que vamos a tener que desarrollar un conjunto diferente de paradigmas / lenguajes para usar realmente estos nuevos multinivel. núcleos de manera efectiva. Los lenguajes como Haskell, cuyos programas son fácilmente paralelizables ya que cualquier función que tenga efectos secundarios deben estar marcados explícitamente de esa manera, y Erlang, de lo que desafortunadamente no sé mucho.
Esto no solo se aplica a Java, sino a la programación con hilos en general. Me encuentro evitando la mayoría de los problemas de concurrencia y latencia simplemente siguiendo estas pautas:
1 / Permita que cada hilo ejecute su propia vida (es decir, decida cuándo morir). Se puede solicitar desde el exterior (por ejemplo, una variable de indicador) pero es completamente responsable.
2 / Haga que todos los subprocesos asignen y liberen sus recursos en el mismo orden; esto garantiza que no se produzca un interbloqueo.
3 / Bloquear recursos por el menor tiempo posible.
4 / Pase la responsabilidad de los datos con los datos en sí: una vez que notifique a un hilo que los datos deben ser procesados, déjelos hasta que le devuelvan la responsabilidad.
Hay una serie de técnicas que están llegando a la conciencia pública en este momento (como en: los últimos años). Uno grande sería actores. Esto es algo que Erlang trajo por primera vez a la red de hierro, pero que ha sido llevado adelante por nuevos lenguajes como Scala (actores en la JVM). Si bien es cierto que los actores no resuelven todos los problemas, hacen que sea mucho más fácil razonar sobre su código e identificar los puntos problemáticos. También hacen que sea mucho más simple diseñar algoritmos paralelos debido a la forma en que lo obligan a usar la continuación del paso sobre el estado mutable compartido.
Fork / Join es algo que deberías mirar, especialmente si estás en la JVM. Doug Lea escribió el documento seminal sobre el tema, pero muchos investigadores lo han discutido a lo largo de los años. Según tengo entendido, el marco de referencia de Doug Lea está programado para su inclusión en Java 7.
En un nivel ligeramente menos invasivo, a menudo los únicos pasos necesarios para simplificar una aplicación de subprocesos múltiples son solo reducir la complejidad del bloqueo. El bloqueo de grano fino (en el estilo de Java 5) es excelente para el rendimiento, pero muy difícil de corregir. Un enfoque alternativo al bloqueo que está ganando algo de tracción a través de Clojure sería la memoria transaccional de software (STM). Esto es esencialmente lo opuesto al bloqueo convencional porque es más optimista que pesimista. Empieza suponiendo que no habrá colisiones y luego permita que el marco corrija los problemas si ocurren. Las bases de datos a menudo funcionan de esta manera. Es excelente para el rendimiento en sistemas con bajas tasas de colisión, pero la gran victoria está en la componente lógica de tus algoritmos. En lugar de asociar arbitrariamente un bloqueo (o una serie de bloqueos) con algunos datos, simplemente ajusta el código peligroso en una transacción y deja que el marco descubra el resto. Incluso puedes obtener un poco de tiempo de compilación para verificar implementaciones decentes de STM como la mónada STM de GHC o mi Scala STM experimental.
Hay muchas opciones nuevas para crear aplicaciones concurrentes, la que elijas depende en gran medida de tu experiencia, tu idioma y el tipo de problema que intentas modelar. Como regla general, creo que los actores junto con las estructuras de datos persistentes e inmutables son una apuesta sólida, pero como he dicho, STM es un poco menos invasivo y en ocasiones puede producir mejoras más inmediatas.
Las preocupaciones principales como las vi fueron (a) evitar interbloqueos y (b) intercambiar datos entre hilos. Una preocupación del arrendador (pero solo un poco de arrendador) era evitar los cuellos de botella. Ya había tenido varios problemas con el bloqueo desproporcionado de secuencias que causaba interbloqueos; es muy útil decir "siempre adquirir bloqueos en el mismo orden", pero en un sistema mediano a grande es casi imposible garantizarlo.
Advertencia: cuando se me ocurrió esta solución, tuve que apuntarme a Java 1.1 (por lo que el paquete de concurrencia aún no brillaba en los ojos de Doug Lea): las herramientas disponibles estaban completamente sincronizadas y esperaban / notificaban. Me basé en la experiencia de escribir un complejo sistema de comunicaciones multiproceso que utiliza el sistema QNX basado en mensajes en tiempo real.
Basado en mi experiencia con QNX que tenía la preocupación del punto muerto, pero evité la simultaneidad de los datos al manejar los mensajes del espacio de memoria de un proceso a otros, diseñé un enfoque basado en mensajes para objetos, que llamé COI, para la coordinación entre objetos. . Al principio, imaginé que podría crear todos mis objetos de esta manera, pero en retrospectiva resulta que solo son necesarios en los principales puntos de control en una aplicación grande: los "intercambios interestatales", si se quiere, no son apropiados para cada uno "intersección" en el sistema de carreteras. Eso resulta ser un gran beneficio porque son bastante poco POJO.
Imaginé un sistema donde los objetos no invocarían conceptualmente métodos sincronizados, sino que "enviarían mensajes". Los mensajes pueden ser de envío / respuesta, donde el remitente espera mientras el mensaje se procesa y regresa con la respuesta, o asincrónico donde el mensaje se deja caer en una cola y se quita de la cola y se procesa en una etapa posterior. Tenga en cuenta que esta es una distinción conceptual: el mensaje se implementó mediante llamadas a métodos sincronizados.
Los objetos centrales para el sistema de mensajería son un objeto aislado, un enlace encuadernado y una aplicación Ioc.
El IsolatedObject se llama así porque no tiene métodos públicos; es esto lo que se extiende para recibir y procesar mensajes. Al usar la reflexión, se aplica además que el objeto hijo no tiene ningún método público, ni ningún paquete o método protegido, excepto los heredados de IsolatedObject, que son casi todos finales. se ve muy extraño al principio porque cuando subescribe Isobject, crea un objeto con 1 método protegido:
Object processIocMessage(Object msgsdr, int msgidn, Object msgdta)
y el resto de los métodos son métodos privados para manejar mensajes específicos.
IocTarget es un medio de abstraer la visibilidad de un objeto aislado y es muy útil para darle a otro objeto una autorreferencia para enviarle señales de vuelta, sin exponer su referencia de objeto real.
Y el IocBinding simplemente vincula un objeto emisor a un receptor de mensaje para que no se realicen comprobaciones de validación para cada mensaje enviado, y se crea utilizando un IocTarget.
Toda interacción con los objetos aislados es a través del "envío" de mensajes: el método processIocMessage del receptor está sincronizado, lo que garantiza que solo se maneje un mensaje a la vez.
Object iocMessage(int mid, Object dta)
void iocSignal (int mid, Object dta)
Habiendo creado una situación en la que todo el trabajo realizado por el objeto aislado se canaliza a través de un único método, organizo los objetos en una jerarquía declarada por medio de una "clasificación" que declaran cuando se construye, simplemente una cadena que los identifica como uno de cualquier cantidad de "tipos de receptor de mensajes", que coloca el objeto dentro de una jerarquía predeterminada. Luego usé el código de entrega del mensaje para asegurarme de que si el remitente era en sí mismo un objeto aislado que para los mensajes de envío / respuesta síncronos era uno que estaba más abajo en la jerarquía. Los mensajes asíncronos (señales) se envían a los receptores de mensajes usando hilos separados en un grupo de subprocesos cuyas señales de entrega de trabajo completas, por lo tanto, las señales se pueden enviar desde cualquier objeto a cualquier receptor en el sistema. Las señales pueden entregar cualquier información de mensaje deseada, pero no es posible responder.
Debido a que los mensajes solo se pueden entregar en una dirección ascendente (y las señales son siempre hacia arriba porque se entregan mediante un subproceso separado que se ejecuta únicamente para ese fin), los interbloqueos se eliminan por diseño.
Debido a que las interacciones entre hilos se logran mediante el intercambio de mensajes mediante la sincronización de Java, las condiciones de carrera y los problemas de datos obsoletos también se eliminan por diseño.
Debido a que un receptor determinado maneja solo un mensaje a la vez, y porque no tiene otros puntos de entrada, se eliminan todas las consideraciones de estado del objeto; de hecho, el objeto está completamente sincronizado y la sincronización no puede quedar accidentalmente fuera de ningún método; no hay getters que devuelvan datos de hilos en caché obsoletos y ningún setter cambia de estado de objeto mientras otro método está actuando sobre él.
Debido a que solo las interacciones entre los componentes principales se canalizan a través de este mecanismo, en la práctica esto se ha escalado muy bien, esas interacciones no ocurren casi tan a menudo en la práctica como teoricé.
El diseño completo se convierte en uno de una colección ordenada de subsistemas que interactúan de una manera estrechamente controlada.
Tenga en cuenta que esto no se usa para situaciones más simples donde los subprocesos de trabajo que usan agrupaciones de subprocesos más convencionales serán suficientes (aunque a menudo inyectaré los resultados del trabajador en el sistema principal enviando un mensaje IOC). Tampoco se usa para situaciones en las que un hilo se activa y hace algo completamente independiente del resto del sistema, como un hilo de servidor HTTP. Por último, no se usa para situaciones donde hay un coordinador de recursos que no interactúa con otros objetos y donde la sincronización interna hará el trabajo sin riesgo de interbloqueo.
EDITAR: Debería haber declarado que los mensajes intercambiados deberían ser generalmente objetos inmutables; si se usan objetos mutables, el acto de enviarlo se considerará una entrega y hará que el emisor renuncie a todo control, y preferiblemente no retenga referencias a los datos. Personalmente, utilizo una estructura de datos bloqueable que está bloqueada por el código IOC y, por lo tanto, se vuelve inmutable al enviar (la bandera de bloqueo es volátil).
No hay una respuesta verdadera para la seguridad de hilos en Java. Sin embargo, hay al menos un gran libro: Java Concurrency in Practice . Me refiero a él regularmente (especialmente la versión en línea de Safari cuando estoy de viaje).
Recomiendo encarecidamente que lea detenidamente este libro en profundidad. Puede encontrar que los costos y beneficios de su enfoque no convencional se examinan en profundidad.
Normalmente sigo un enfoque de estilo Erlang. Yo uso el Patrón de Objeto Activo. Funciona de la siguiente manera.
Divida su aplicación en unidades de grano muy grueso. En una de mis aplicaciones actuales (400.000 LOC) tengo aprox. 8 de estas unidades de grano grueso. Estas unidades no comparten ningún dato. Cada unidad mantiene sus propios datos locales. Cada unidad se ejecuta en su propio hilo (= Patrón de objeto activo) y, por lo tanto, tiene un solo hilo. No necesitas ningún bloqueo dentro de las unidades. Cuando las unidades necesitan enviar mensajes a otras unidades lo hacen publicando un mensaje en una cola de las otras unidades. La otra unidad selecciona el mensaje de la cola y reacciona a ese mensaje. Esto podría desencadenar otros mensajes a otras unidades. En consecuencia, los únicos bloqueos en este tipo de aplicación son alrededor de las colas (una cola y bloqueo por unidad). ¡Esta arquitectura está libre de interbloqueos por definición!
Esta arquitectura se escala extremadamente bien y es muy fácil de implementar y ampliar tan pronto como entienda el principio básico. Me gusta pensar que es una SOA dentro de una aplicación.
Al dividir tu aplicación en las unidades, recuerda. La cantidad óptima de hilos de larga ejecución por núcleo de CPU es 1.
Parece que tu COI es algo así como FBP :-) Sería fantástico si el código JavaFBP pudiera obtener una investigación exhaustiva de alguien como tú versado en el arte de escribir código seguro para subprocesos ... Está en SVN en SourceForge.
Recomiendo la programación basada en flujo, también conocida como programación de flujo de datos. Utiliza OOP e hilos, lo siento como un paso natural hacia delante, como OOP fue de procedimiento. Tengo que decir que la programación del flujo de datos no se puede usar para todo, no es genérica.
Wikipedia tiene buenos artículos sobre el tema:
http://en.wikipedia.org/wiki/Dataflow_programming
http://en.wikipedia.org/wiki/Flow-based_programming
Además, tiene varias ventajas, como la increíble configuración flexibile, estratificación; el programador (programador de componentes) no tiene que programar la lógica comercial, se hace en otra etapa (uniendo la red de procesamiento).
¿Sabías que make es un sistema de flujo de datos? Vea make -j , especialmente si tiene un procesador multi-core.
Recuerdo que me sorprendió un poco descubrir que la clase synchronizedList de Java no era completamente segura para subprocesos, pero solo condicionalmente era segura para subprocesos. Todavía podría quemarme si no ajustara mis accesos (iteradores, ajustadores, etc.) en un bloque sincronizado. Esto significa que podría haber asegurado a mi equipo y a mi dirección que mi código era seguro para las cadenas, pero podría haber estado equivocado. Otra forma en que puedo asegurar la seguridad de los hilos es que una herramienta analice el código y lo pase. STP, modelo de actor, Erlang, etc. son algunas formas de obtener esta última forma de seguridad. Ser capaz de asegurar las propiedades de un programa confiablemente es / será un gran paso adelante en la programación.
Sugiero el modelo de actor.