multithreading - thread - Lograr seguridad de subprocesos
delphi multithreading (7)
Pregunta ¿Cómo puedo asegurarme de que mi aplicación es segura para subprocesos? ¿Son sus prácticas comunes, métodos de prueba, cosas que evitar, cosas que buscar?
Antecedentes Actualmente estoy desarrollando una aplicación de servidor que realiza una serie de tareas en segundo plano en diferentes subprocesos y se comunica con los clientes que usan Indy (usando otro grupo de subprocesos generados automáticamente para la comunicación). Dado que la aplicación debe estar disponible, un bloqueo del programa es algo muy malo y quiero asegurarme de que la aplicación es segura para subprocesos. No importa qué, de vez en cuando descubro un fragmento de código que arroja una excepción que nunca ocurrió antes y en la mayoría de los casos me doy cuenta de que se trata de algún tipo de error de sincronización, donde olvidé sincronizar mis objetos correctamente. De ahí mi pregunta sobre las mejores prácticas, las pruebas de seguridad de hilos y cosas por el estilo.
mghie: ¡Gracias por la respuesta! Tal vez debería ser un poco más preciso. Para ser claros, sé sobre los principios del multihilo, uso la sincronización (monitores) en todo mi programa y sé cómo diferenciar los problemas de subprocesos de otros problemas de implementación. Sin embargo, me olvido de agregar una sincronización adecuada de vez en cuando. Solo para dar un ejemplo, utilicé la función de clasificación RTL en mi código. Se veía algo así como
FKeyList.Sort (CompareKeysFunc);
Resulta que tuve que sincronizar FKeyList mientras ordenaba. Simplemente no me vino a la mente cuando inicialmente escribí esa simple línea de código. Son estas cosas de las que quiero hablar. ¿Cuáles son los lugares donde uno fácilmente olvida agregar código de sincronización? ¿Cómo se asegura de que haya agregado el código de sincronización en todos los lugares importantes?
Como la gente ha mencionado y creo que usted sabe, estar seguro, en general, de que su código es seguro para subprocesos es imposible (creo que es probablemente imposible, pero tendría que rastrear el teorema). Naturalmente, quieres hacer las cosas más fáciles que eso.
Lo que trato de hacer es:
- Utilice un patrón conocido de diseño multiproceso : un grupo de subprocesos , el paradigma del modelo de actor , el patrón de comando o algo de ese enfoque. De esta forma, el proceso de sincronización ocurre de la misma manera, de manera uniforme , en toda la aplicación.
- Limite y concentre los puntos de sincronización . Escriba su código para que necesite sincronización en el menor número posible de lugares y mantenga el código de sincronización en uno o pocos lugares del código.
- Escriba el código de sincronización para que la relación lógica entre los valores sea clara tanto al ingresar como al salir de la protección. Uso muchas afirmaciones para esto (su entorno puede limitar esto).
- No acceda nunca a las variables compartidas sin guardias / sincronización . Sea muy claro cuáles son sus datos compartidos. (He oído que hay paradigmas para la programación sin hilos multiproceso, pero eso requeriría aún más investigación).
- Escriba su código de la manera más clara, clara y SECA posible.
Mi respuesta simple combinada con esas respuestas es:
- Cree su aplicación / programa usando la manera de seguridad de subprocesos
- Evite usar variables estáticas públicas en todos los lugares
Por lo tanto, generalmente cae en este hábito / práctica fácilmente, pero necesita un tiempo para acostumbrarse a:
programe su lógica (no la UI) en lenguaje de programación funcional como F # o incluso usando Scheme o Haskell. Además, la programación funcional promueve la práctica de seguridad de hilos mientras que también nos advierte a programar siempre hacia la pureza en la programación funcional. Si usa F #, también hay una clara distinción sobre el uso de objetos mutables o inmutables, como las variables.
Dado que el método (o simplemente funciones) es un ciudadano de primera clase en F # y Haskell, entonces el código que escriba también tendrá más disciplina hacia un estado menos mutable.
También utilizando el estilo de evaluación diferida que normalmente se puede encontrar en estos lenguajes funcionales, puede estar seguro de que su programa está a salvo de los efectos laterales, y también se dará cuenta de que si su código necesita efectos, debe definirlo claramente. Si se tienen en cuenta los efectos secundarios, su código estará listo para aprovechar la capacidad de compilación dentro de los componentes de sus códigos y la programación multinúcleo.
Realmente no se puede probar la seguridad de los subprocesos. Todo lo que puede hacer es mostrar que su código no es seguro para subprocesos, pero si sabe cómo hacerlo, ya sabe qué hacer en su programa para corregir ese error en particular. Son los errores que no sabes que son el problema, y ¿cómo escribirías pruebas para esos? Aparte de eso, los problemas de enhebrado son mucho más difíciles de encontrar que otros problemas, ya que el acto de depuración ya puede alterar el comportamiento del programa. Las cosas diferirán de un programa al siguiente, de una máquina a la otra. Cantidad de CPU y núcleos de CPU, número y tipo de programas que se ejecutan en paralelo, orden exacto y tiempo de las cosas que suceden en el programa; todo esto y mucho más tendrá influencia en el comportamiento del programa. [En realidad, quería agregar la fase de la luna y cosas así a esta lista, pero entiendes lo que quiero decir.]
Mi consejo es dejar de ver esto como un problema de implementación, y comenzar a ver esto como un problema de diseño del programa. Necesita aprender y leer todo lo que pueda encontrar sobre multi-threading, ya sea que esté escrito para Delphi o no. Al final, necesita comprender los principios subyacentes y aplicarlos adecuadamente en su programación. Primitivas como secciones críticas, mutexes, condiciones y subprocesos son algo que proporciona el sistema operativo, y la mayoría de los lenguajes solo los envuelve en sus bibliotecas (esto ignora cosas como hilos verdes proporcionados, por ejemplo, por Erlang, pero es un buen punto de partida desde )
Yo diría que empiece con el artículo de Wikipedia sobre hilos y siga su camino a través de los artículos vinculados. Empecé con el libro "Win32 Multithreaded Programming" de Aaron Cohen y Mike Woodring; está agotado, pero tal vez puedas encontrar algo similar.
Editar: Permítanme hacer un seguimiento breve de su pregunta editada. Todo el acceso a datos que no son de solo lectura debe sincronizarse correctamente para que sea seguro para la ejecución de subprocesos, y la clasificación de una lista no es una operación de solo lectura. Entonces, obviamente, uno debería agregar sincronización en todos los accesos a la lista.
Pero con más y más núcleos en un sistema, el bloqueo constante limitará la cantidad de trabajo que se puede hacer, por lo que es una buena idea buscar una forma diferente de diseñar su programa. Una idea es introducir la mayor cantidad de datos de solo lectura posible en su programa; el bloqueo ya no es necesario, ya que todos los accesos son de solo lectura.
He encontrado que las interfaces son una ayuda muy valiosa en el diseño de programas de subprocesos múltiples. Las interfaces pueden implementarse para tener solo métodos de acceso de solo lectura a los datos internos, y si se atiene a ellos puede estar bastante seguro de que muchos de los posibles errores de programación no ocurren. Puede compartirlos libremente entre subprocesos y el recuento de referencias seguro de subprocesos se asegurará de que los objetos de implementación se liberen correctamente cuando la última referencia a ellos salga del alcance o se le asigne otro valor.
Lo que haces es crear objetos que descienden de TInterfacedObject. Implementan una o más interfaces que proporcionan acceso de solo lectura a las partes internas del objeto, pero también pueden proporcionar métodos públicos que modifican el estado del objeto. Cuando crea el objeto, conserva una variable del tipo de objeto y una variable de puntero de interfaz. De esta forma, la administración de por vida es fácil, ya que el objeto se eliminará automáticamente cuando ocurra una excepción. Utiliza la variable que apunta al objeto para llamar a todos los métodos necesarios para configurar correctamente el objeto. Esto muta el estado interno, pero dado que esto ocurre solo en el hilo activo, no hay posibilidad de conflicto. Una vez que el objeto está configurado correctamente, devuelve el puntero de la interfaz al código de llamada y, como no hay forma de acceder al objeto después, excepto yendo a través del puntero de la interfaz, puede estar seguro de que solo se puede realizar el acceso de solo lectura. Al usar esta técnica, puedes eliminar completamente el bloqueo dentro del objeto.
¿Qué pasa si necesitas cambiar el estado del objeto? Si no lo hace, cree uno nuevo copiando los datos de la interfaz y luego mutee el estado interno de los objetos nuevos. Finalmente, devuelve el puntero de referencia al nuevo objeto.
Al usar esto, solo necesitará bloquear donde obtiene o establece dichas interfaces. Incluso se puede hacer sin bloqueo, mediante el uso de las funciones de intercambio atómico. Vea esta publicación de blog de Primoz Gabrijelcic para un caso de uso similar donde se establece un puntero de interfaz.
Simple: no use datos compartidos. Cada vez que accede a datos compartidos corre el riesgo de encontrarse con un problema (si olvida sincronizar el acceso). Peor aún, cada vez que accede a datos compartidos se arriesga a bloquear otros hilos que dañarán su paralelización.
Sé que este consejo no siempre es aplicable. Aún así, no duele si tratas de seguirlo tanto como sea posible.
EDITAR: Mayor respuesta al comentario de Smasher. No cabría en un comentario :(
Usted es totalmente correcto. Es por eso que me gusta mantener una copia oculta de los datos principales en un hilo de solo lectura. Agrego un control de versiones a la estructura (un DWORD con 4 alineamientos) e incremento esta versión en el escritor de datos (protegido con bloqueo). El lector de datos compararía la versión global y la versión privada (lo que se puede hacer sin bloquear) y solo si son diferentes bloquearía la estructura, la duplicaría en un almacenamiento local, actualizaría la versión local y se desbloquearía. Luego accedería a la copia local de la estructura. Funciona muy bien si la lectura es la forma principal de acceder a la estructura.
Voy a dar un segundo consejo a Mghie: la seguridad de los hilos está diseñada. Lee sobre ello en cualquier lugar que puedas.
Para una mirada de nivel realmente bajo sobre cómo se implementa, busque un libro sobre las partes internas de un núcleo del sistema operativo en tiempo real. Un buen ejemplo es MicroC / OS-II: The Real Time Kernel de Jean J. Labrosse, que contiene el código fuente anotado completo para un kernel en funcionamiento junto con discusiones sobre por qué las cosas se hacen tal como son.
Editar : A la luz de la pregunta mejorada que se centra en el uso de una función RTL ...
Cualquier objeto que pueda verse por más de un hilo es un posible problema de sincronización. Un objeto seguro para subprocesos seguiría un patrón consistente en la implementación de cada método de bloqueo "suficiente" del estado del objeto durante la duración del método, o tal vez, reducido a solo "suficientemente largo". Ciertamente, cualquier secuencia de lectura, modificación y escritura de cualquier parte del estado de un objeto debe realizarse atómicamente con respecto a otros hilos.
El arte radica en descubrir cómo hacer un trabajo útil sin bloquear ni crear un cuello de botella de ejecución.
En cuanto a encontrar tales problemas, las pruebas no serán ninguna garantía. Un problema que aparece en las pruebas puede ser reparado. Pero es extremadamente difícil escribir pruebas unitarias o pruebas de regresión para seguridad de subprocesos ... así que ante un cuerpo de código existente, tu recurso probable es la revisión constante de código hasta que la práctica de seguridad de subprocesos se convierta en una segunda naturaleza.
Solo quería agregar dos enlaces a esta discusión que encontré útiles, cuando pienso en la seguridad de subprocesos y las posibilidades de lograrlo:
M2C: Java Concurrency in Practice es realmente bueno.