Capitulo 3. Sincronización básica.

En este capitulo:

¿Qué datos son compartidos entre los hilos?

Primero que nada, es valioso conocer exactamente cuales son los estados que están almacenados en un proceso y en un hilo básico. Cada hilo tiene su propio contador de programa y estado del procesador. Esto quiere decir que los hilos progresan en forma independiente a través del código. Cada hilo tiene, a su vez, su propia pila, de modo que las variables locales son intrínsecamente locales para cada hilo y no poseen formas de sincronizarse por sí estas de variables. Los datos globales del programa pueden ser libremente compartidos entre los hilos de ejecución, por lo que, desde luego, existirán problemas de sincronización con estas variables. Es claro que, si una variable es globalmente accesible, pero sólo un hilo de ejecución la usa, no habrá problemas con esto. La misma situación se aplica para el alojamiento en memoria (normalmente con los objetos): en principio, cualquier hilo puede acceder a cualquier objeto en particular, pero si el programa fue escrito de modo que sólo un hilo tiene un puntero a un objeto en particular, entonces sólo un hilo podrá acceder a el y no habrá problemas de concurrencia.

Delphi provee la palabra reservada threadvar. Esta permite que variables “globales” sean declaradas cuando hay una copia de la variable en cada hilo. Sin embargo, esta característica no se usa mucho, porque es generalmente más conveniente poner ese tipo de variables dentro de una clase hilo, en vez de crear una instancia de la variable para cada hilo descendiente creado.

Atomicidad cuando se accede a datos compartidos.

Para poder entender cómo es que los hilos funcionan juntos, es necesario entender el concepto de atomicidad. Una acción o secuencia de acciones es atómica si la acción o secuencia es indivisible. Cuando un hilo realiza una acción atómica, esto lo ven los otros hilos como que la acción o no empezó o ya se completó. No es posible para un hilo atrapar al otro “en el acto”. Si no se realiza ningún tipo de sincronización entre los hilos, entonces casi ninguna operación es atómica. Tomemos un ejemplo sencillo. Considera este fragmento de código. ¿Qué podría ser más sencillo? Desgraciadamente, aún un fragmento de código tan trivial, puede ocasionar problemas si dos hilos separados lo usan para incrementar la variable compartida A. Esta sentencia de pascal se desdobla en tres operaciones a nivel assembler:

Leer A desde la memoria hacia el registro del procesador.

Agregar 1 al registro del procesador.

Escribir los contenidos del registro del procesador en A en la memoria.

Aún en una máquina uniprocesador, la ejecución del este código por múltiples hilos puede causar problemas. La razón por la que esto es así, es la administración de tareas. Cuando existe sólo un procesador, entonces sólo un hilo se ejecuta por vez, pero el administrador de tareas de Win32 cambia el hilo en ejecución cerca de 18 veces por segundo. El administrador de tareas puede detener un hilo en funcionamiento e iniciar otro en cualquier momento. El sistema operativo no espera tener un permiso para suspender un hilo e iniciar otro: el cambio puede suceder en cualquier momento. Como el cambio puede suceder entre cuales quiera instrucciones de procesador, puede haber puntos inconvenientes en medio de una función, y aún a medio camino en la ejecución de una sentencia en particular. Imaginemos que dos hilos (X e Y) están ejecutando el código del ejemplo en una máquina uniprocesador. En un caso deseable, el programa puede estar corriendo y el administrador de tareas puede pasar el punto crítico, entregando el resultado esperado: A es incrementado por dos.

Instrucciones ejecutadas por el hilo X

Instrucciones ejecutadas por el hilo Y

Valor de la variable A en memoria

Hilo suspendido

1

Lee A desde la memoria en un registro del procesador.

Hilo suspendido

1

Incrementa en 1 el registro del procesador.

Hilo suspendido

1

Escribe los contenidos del registro del procesador en A (2) en memoria.

Hilo suspendido

2

Hilo suspendido

2

CAMBIO DE HILO

CAMBIO DE HILO

2

Hilo suspendido

2

Hilo suspendido

Lee A desde la memoria en un registro del procesador.

2

Hilo suspendido

Incrementa en 1 el registro del procesador.

2

Hilo suspendido

Escribe el contenido del registro del procesador en A (3) en memoria.

3

Hilo suspendido

3

Sin embargo, este funcionamiento no es seguro y es una chance más de cómo podría darse la ejecución de los hilos. La ley de Murphy existe y la siguiente situación puede ocurrir:

Instrucciones ejecutadas por el hilo X

Instrucciones ejecutadas por el hilo Y

Valor de la variable A en memoria

Hilo suspendido

1

Lee A desde la memoria en un registro del procesador.

Hilo suspendido

1

Incrementa en 1 el registro del procesador.

Hilo suspendido

1

CAMBIO DE HILO

CAMBIO DE HILO

1

Hilo suspendido

1

Hilo suspendido

Lee A desde la memoria en un registro del procesador.

1

Hilo suspendido

Incrementa en 1 el registro del procesador.

1

Hilo suspendido

Escribe el contenido del registro del procesador en A (2) en memoria.

1

CAMBIO DE HILO

CAMBIO DE HILO

2

Escribe los contenidos del registro del procesador en A (2) en memoria.

Hilo suspendido

2

Hilo suspendido

2

En este caso, A no es incrementado en dos, sino sólo en uno. ¡Oh, diablos! Si A fuera la posición de una barra de progreso, entonces quizás esto no sería un problema, pero si es algo más importante, como un contador de número de ítems en una lista, entonces empezamos a estar en problemas. Si la variable compartida resulta ser un puntero entonces uno puede esperar cualquier tipo de resultado. Esto es conocido como una condición de carrera.

Problemas adicionales con la VLC.

La VCL no posee protección para estos conflictos. Esto significa que los cambios de hilos en ejecución, puede suceder cuando uno o más hilos están ejecutando código de la VCL. Gran parte de la VCL esta bastante bien contenida como para que esto no sea un problema. Desgraciadamente, los componentes, y en particular, los heredados de TControl poseen varios mecanismos que no le hacen ninguna gracia a los cambios de hilos en ejecución. Un cambio de hilo en ejecución en un momento inadecuado puede provocar estragos, corrompiendo los contadores de referencia de manejadores compartidos, destruyendo no sólo datos, sino también las conexiones entre los componentes.

Aún cuando los hilos no están ejecutando código VCL, malas sincronizaciones pueden seguir causando problemas futuros: no es suficiente con asegurarse de que el hilo principal de VCL esté inactivo antes de que otro hilo entre y modifique algo. Puede que se ejecute un código en la VCL que (de momento) muestra una caja de diálogo y llama a una escritura en disco, suspendiendo el hilo principal. Si otro hilo mificara los datos compartidos, esto puede parecerle al hilo principal que algunos datos globales han cambiando mágicamente como resultado de mostrar la caja de diálogo o escribir en un archivo. Esto es obviamente inaceptable; solo un hilo puede ejecutar código VCL, o un mecanismo debe ser encontrado para asegurarse de que los hilos separados no interfieran entre sí.

Diversión con máquinas multiprocesador.

Por suerte para los programadores, el problema no es más complejo para máquinas con más de un microprocesador. Los métodos de sincronización que proveen Delphi y Windows funcionan igual de bien más allá del número de procesadores. Los que hicieron el sistema operativo Windows tuvieron que escribir código extra para lidiar con máquinas multiprocesador: Windows NT 4 informa al usuario en el momento de arranque si está usando un kernel multiprocesador o uniprocesador. Como sea, para el programador, todo esto queda oculto. No necesitas preocuparte acerca de cuántos procesadores tiene la máquina, más de lo que te tienes que preocupar por que chipset utiliza el mother.

La solución Delphi: TThread.Synchronize.

Delphi provee una solución que es ideal para que principiantes escriban hilos de ejecución. Es simple y evita todos los problemas mencionados antes. TThread tiene un método llamado Synchronize. Este método toma como parámetro otro método que no lleva parámetros, que tu desees ejecutar. Con esto tienes la garantía de que el código en el método sin parámetros será ejecutado como un resultado de la llamada a synchronize y no generará conflictos con el hilo VCL. En lo que concierne al hilo no-VCL, pareciera que todo el código en el método sin parámetros sucede en el momento en que es llamado synchronize.

Umm. ¿Suena confuso? Puede ser. Lo ilustraré con un ejemplo. Modificaremos nuestro programa de números primos, de modo que en vez de mostrar una caja de mensajes, éste indicará si el número es primo o no agregando un texto en un memo en el formulario principal. Primero que nada, agregaremos un memo a nuestro formulario principal (ResultsMemo), como este. Ahora podemos hacer el trabajo real. Agregamos otro método (UpdateResults) en nuestro hilo que mostrará el resultado en el memo, y en vez de llamar a ShowMessage, llamaremos a Synchronize, pasando el nuevo método como parámetro. La declaración del hilo y las partes modificadas, ahora se ven así. Nótese que UpdateResults accede a ambos, el formulario principal y la variable con el resultado. Desde el punto de vista del hilo principal, el formulario principal parece haber sido modificado en respuesta a un evento. Desde el punto de vista del hilo que calcula los números primos, la variable de resultado es accedida durante la llamada a Synchronize.

¿Cómo funciona esto? ¿Qué hace Synchronize?

El código que es invocado cuando se llama a Synchronize, puede realizar cualquier cosa que el hilo principal de VCL pueda hacer. Además, puede modificar datos asociados con su propio objeto hilo de manera segura, sabiendo que la ejecución de su propio hilo está en un punto particular (el llamado a synchronize). Lo que realmente ocurre es bastante elegante, y es ilustrado mejor por otro diagrama.


Cuando se llama a synchronize, el hilo de cálculo de números primos es suspendido. En este punto, el hilo principal de VCL puede estar suspendido y en inactividad, o puede que haya sido suspendido temporalmente por una E/S u alguna otra operación, o puede que se esté ejecutando. Si no esta suspendido en un estado totalmente inactivo (en el bucle de espera de mensajes de la aplicación principal), entonces el hilo de cálculo de números primos espera. Una vez que el hilo principal se vuelve inactivo, la función sin parámetros pasada a synchronize se ejecuta en el contexto del hilo principal de VCL. En nuestro caso, la función sin parámetros se llama UpdateResults y actúa sobre un memo. Esto asegura que no habrá conflictos con el hilo principal de VCL, y en esencia, el procesamiento de este código es parecido a cualquier código de Delphi que ocurriera en el hilo principal de VCL en respuesta a un mensaje enviado por la aplicación. No ocurren conflictos con el hilo que llamó a synchronize porque está suspendido en un punto que se sabe que es seguro (en alguna parte dentro del código de TThread.Synchronize).

Una vez que este “procesamiento por proxy” se completa, el hilo principal de VCL es liberado para seguir con su trabajo normal, y el hilo que llamó a synchronize se reanuda, y vuelve de la llamada de función. De hecho, una llamada a Synchronize parece ser un mensaje más al hilo principal de VCL, y una llamada a la función de cálculo de números primos. Los hilos están en posiciones conocidas y no se ejecutan concurrentemente. No hay ninguna condición de carrera. Problema resulto.

Sincronizado a hilos no-VCL.

El ejemplo anterior mostró como se puede hacer un simple hilo para interactuar con el hilo principal de VCL. De hecho, éste le roba tiempo al hilo principal de VCL para hacerlo. Esto no es así arbitrariamente entre los hilos. Si tienes dos hilos no VCL, X e Y, no puedes llamar a synchronize en X solamente, y luego modificar datos almacenados en Y. Es necesario llamar a synchronize en ambos hilos cuando se está leyendo o escribiendo datos compartidos. En efecto, esto significa que los datos son modificados por el hilo principal de VCL, y todos los demás hilos sincronizan con el hilo principal de VCL cada vez que necesitan acceder a sus datos. Esto podría funcionar, pero es ineficiente, especialmente si el hilo principal de VCL está ocupado: cada vez que dos hilos necesitan comunicarse, tienen que esperara que un tercer hilo se vuelva inactivo. Luego, vamos a ver como controlar la concurrencia entre hilos y hacer que se comuniquen directamente.


Contenido - Anterior - Siguiente