SlideShare uma empresa Scribd logo
1 de 136
Multithreading - A la manera de Delphi
   Esta guía fue escrita para quien esté interesado en mejorar la
respuesta en sus aplicaciones Delphi mediante el uso de hilos de
ejecución (Threads). Cubre aspectos desde los más simples (para
el novato) hasta algunos más sofisticados en un nivel intermedio
 y algunos ejemplos traen aspectos que rozan el nivel avanzado.
Se asume que el lector conoce la programación en Object Pascal,
       incluyendo la programación orientada a objetos y una
      comprensión del trabajo con eventos de programación.
   Introducción
   Capítulo 1. ¿Qué son los hilos de ejecución? ¿Porqué usarlos?
   Capítulo 2. Crear un hilo de ejecución en Delphi.
   Capítulo 3. Sincronización básica.
   Capítulo 4. Destrucción simple de hilos.
   Capítulo 5. Más sobre destrucciones de hilos. Deadlock.
   Capítulo 6. Más sincronización: Secciones críticas y mutexes.
   Capítulo 7. Guía de programación de mutex. Control de
   concurrencia.
   Capítulo 8. Clases Delphi seguras para entornos multihilo y
   prioridades.
   Capítulo 9. Semáforos. Administración del flujo de datos. La
   relación productor - consumidor.
   Capítulo 10. E/S y flujo de datos: del bloqueo a lo asincrónico, ida
   y vuelta.
   Capítulo 11. Sicronizadores y Eventos.
   Capítulo 12. Más dispositivos Win32 para la sincronización.
   Capítulo 13. Usar hilos conjuntamente con el BDE, las excepciones
   y las DLLs.
   Capítulo 14. Un problema del mundo real, y su solución.

Introducción
  Esta guía fue escrita para quien esté interesado en mejorar la
respuesta en sus aplicaciones Delphi mediante el uso de hilos de
ejecución (Threads). Cubre aspectos desde los más simples (para el
novato) hasta algunos más sofisticados en un nivel intermedio y
algunos ejemplos traen aspectos que rozan el nivel avanzado. Se asume
que el lector conoce la programación en Object Pascal, incluyendo la
programación orientada a objetos y una comprensión del trabajo con
eventos de programación.

Dedicatorias
  Dedicado a tres miembros del departamento de Ciencias de la
Computación de la Universidad de Cambridge: Dr Jean Bacon, Dr
Simon Crosby, and Dr Arthur Norman.
  Muchas gracias a Jean, como tutor, por hacer que algo complicado
pareciera sencillo, por proveer excelente material de referencia, por
levantar la cortina alrededor de un tema muy misterioso. Además
merece agradecimiento como directora de estudios, por explicar la
ciencia de la computación a mi propio ritmo. ¡Me tomó tres años
darme cuenta por mi mismo!
  Muchas gracias a Simons como tutor, por mostrarme que apesar de
que los modernos sistemas operativos pueden ser endemoniadamente
complicados los principios en los que se basan son muy simples.
Merece además las gracias por tomar a un estudiantes con ideas no
convecionales acerca del proyecto final de la materia, y por proveerme
acesoramiento muy útil en mi disertación del proyecto.
  Arthur Norman nunca me enseño nada acerca de multitarea. Sin
embargo me enseñó muchas otras cosas que me ayudaron a escribir las
partes más complicadas de esta guía.
    No hay limites a la excentricidad de los lectores universitarios.
    A pesar de que la mayoría de la gente prefiere la simplicidad, hay
    cierto perverso placer en hacer las cosas de la forma complicada,
    especialmente si eres un cinico.
  También merece una mención por algunas de las mejores citas nunca
leídas por un lector de ciencias de la computación:
   "Hay algo en los cursos lo cual no debe haber sido evidente hasta
   ahora, es la realidad..."
"Los teóricos han probado que esto no tiene solución, pero
   nosotros somos tres, y somos listos..."
   "La gente que no usa computadoras son más sociables, rasonables y
   menos... retorcidos."
   "(Si la teoría de la complejidad se sostiene por su título) si eso se
   prueba ser así, seré el ganador como no muchos de ustedes
   intentarán las preguntas del examen."
  Él hasta tiene su propia página de fans.

Lecturas recomendadas.
  Título: Concurrent Systems: An integrated approach to Operating
Systems,
  Database, and Distributed Systems.
  Autor: Jean Bacon.
  Editorial : Addison-Wesley
  ISBN: 0-201-41677-8
  El autor acepta sugerencias de otros títulos útiles.

Ayuda para la navegación.
  Los escritos y los diagramas de esta guía están contenidos en paginas
HTML simples, una por cada capítulo. Los códigos fuente de ejemplo
aparecen en ventanas emergentes. Necesitarás habilitar javascript en tu
navegador para verlos. Para facilitar la vista de los escritos y el código
fuente en paralelo, el lector encontrará muy útil poner varias ventanas
del navegador en mosaico. Esto se puede lograr haciando click derecho
en la barra de tareas y seleccionar "Mosaico vertical".

Historial de cambios.
  Versión 1.1
   Corrección de ortografía y errores de puntuación en la prosa, y
   reescritura de explicaciones poco claras. Capítulos 1-9 y 12
   modificados.
Agregado historial de cambios y otros créditos a la tabla de
   contenidos.
   Capítulo 12 renombrado.
   Agregado el capítulo 14.

Créditos.
  Muchas gracias a las siguientes personas por revisar, sugerir, corregir
y mejorar esta guía.
   Tim Frost
   Conor Boyd
   Alan Lloyd
   Bruce Roberts
   Bjørge Sæther
   Craig Stuntz
   Jim Vaught
  Créditos de esta traducción
   Andrés Galluzzi.
   Diego Romero.
   Descargar el tutorial completo (340 KB).

    Capítulo 1. ¿Qué son los hilos de ejecución?
                 ¿Porqué usarlos?
En este capítulo:
   Historia
   Definiciones
   Un ejemplo
   Tiempo compartido
   ¿Porqué usar hilos de ejecución?

Historia
  En los primeros días de la computación, toda la programación era
esencialmente tratada en un solo hilo. Los programas se creaban
perforando tarjetas o cintas, con las que formabas tu grupo de tarjetas
que enviabas luego al centro local de computación y, tras de un par de
días, recibías otro grupo de tarjetas que, si estabas de suerte, contenían
los resultados solicitados. Todo el procesamiento era por lotes, de
ningún modo crítico, basado en la premisa de que el primero que
llegaba era el primero en ser servido y cuando tu programa estaba
corriendo, tenía uso exclusivo del tiempo de la computadora.
  Las cosas han cambiado. El concepto de múltiples hilos de ejecución
aparece por primera vez con los sistemas de tiempo compartido, donde
más de una persona podía conectarse a una computadora central a la
vez. Era importante asegurarse que el tiempo de procesamiento de la
máquina era dividido adecuadamente entre todos los usuarios; los
sistemas operativos de ese tiempo comienzan a usar los conceptos de
“proceso” (process) e “hilos de ejecución” (threads). Las
computadoras de escritorio han visto un progreso similar. Los
primeros DOS y Windows funcionaban con un único hilo de
ejecución. Los programas, o funcionaban en forma exclusiva en la
máquina, o no funcionaban. Con la creciente sofisticación de las
aplicaciones y la creciente demanda de computadoras personales,
especialmente en lo relativo a la performance gráfica y el trabajo en
red, los sistemas operativos multiproceso y multihilo se volvieron algo
común. Las aplicaciones multihilo en las PC’s fueron principalmente
conducidas por la búsqueda de una mejor performance y usabilidad.

Definiciones
  El primer concepto a definir es el del proceso. La mayoría de los
usuarios de Windows 95, 98 y NT intuyen bastante bien lo que es un
proceso. Lo ven como un programa que corre en la computadora, co-
existiendo y compartiendo el microprocesador, la memoria y otros
recursos con otros programas. Los programadores saben que un
proceso es invocado por un código ejecutable, como también saben
que ese código tiene una única existencia y que las instrucciones
ejecutadas por ese proceso son procesadas de una manera ordenada.
En suma, los procesos se ejecutan en forma aislada. Los recursos que
usan (memoria, disco, E/S, tiempo del microprocesador) son
virtualizados, de modo que todos los procesos tienen su propio grupo
de recursos virtuales que son exclusivos de ese proceso. El sistema
operativo provee esta virtualización. Los procesos
ejecutan módulos de código. Estos pueden ser independientes, en el
sentido de que, los módulos ejecutables de código que competen al
Windows Explorer son independientes de los del Microsoft Word. Sin
embargo, éstos también pueden ser compartidos, como es el caso de
las DLL’s. El código de una DLL típicamente es ejecutado en el
contexto de muchos procesos diferentes, y habitualmente en forma
simultánea. La ejecución de instrucciones no es totalmente ordenada
por los procesos: Microsoft Word no deja de abrir un documento
sencillamente porque la cola de impresión está enviando algo a la
impresora! Por supuesto, cuando diferentes procesos interactúan entre
sí, el programador debe establecer un orden, un problema central que
será tratado luego.
   Nuestro próximo concepto es el del hilo de ejecución (Thread). Los
hilos de ejecución fueron desarrollados cuando se vio claramente el
deseo de tener aplicaciones que realizaran varias acciones con mayor
libertad en cuanto al orden, posiblemente, realizando varias acciones en
el mismo momento. En situaciones donde algunas acciones pudieran
causar una demora considerable a un hilo de ejecución (por ejemplo,
cuando se espera que el usuario haga algo), era más deseable que el
programa siguiera funcionando, ejecutando otras acciones
concurrentemente (por ejemplo, verificación ortográfica en segundo
plano, o procesamiento de los mensajes que arriban desde la red). Sin
embargo, crear todo un nuevo proceso para cada acción concurrente y
luego hacer que ese proceso se comunicara con el primero era
generalmente una sobrecarga demasiado grande.

Un ejemplo
  Si se necesita ver un buen ejemplo de programación multihilo,
entonces el Windows Explorer (aka Windows Shell) es un ejemplo
excelente. Haz doble clic en “Mi PC” y abre varias subcarpetas
abriendo nuevas ventanas a medida que lo haces. Ahora, realiza una
larga operación de copia en una de esas ventanas. La barra de progreso
aparece y esa ventana en particular deja de responder al usuario. Sin
embargo, todas las demás ventanas son perfectamente usables.
Obviamente, varias cosas se están haciendo en el mismo momento,
pero sólo una copia de explorer.exe está corriendo. Esa es la esencia de
la programación multihilo.

Tiempo compartido.
   En la mayoría de los sistemas que soportan varios hilos de ejecución,
puede haber muchos usuarios haciendo llamadas simultáneas al
sistema. Para responder a todas estas demandas, se suele necesitar una
cantidad de hilos de ejecución que suele ser superior al número de
procesadores que existen físicamente en el sistema. Esto es posible
gracias a que la mayoría de los sistemas permiten compartir el tiempo
del procesador, y así solucionar este problema. En un sistema con
tiempo compartido, los hilos de ejecución corren por un corto espacio
y luego son invalidados; es decir, un temporizador en el hardware de la
máquina se dispara, lo que causa que el sistema operativo re-evalúe qué
hilos de ejecución deben correr, pudiendo detener la ejecución de los
hilos en funcionamiento y continuando la ejecución de otros hilos que
habían quedado detenidos. Esto permite que las máquinas, aún con un
solo procesador, puedan correr muchos hilos de ejecución. En las
PC’s, los tiempos compartidos tienden a ser de alrededor de 55
milisegundos.

¿Porqué usar hilos de ejecución?
  Los hilos de ejecución no deben alterar la semántica de un programa.
Ellos cambian simplemente los tiempos de operación. Como resultado,
son casi siempre usados como una solución elegante a problemas de
performance. Aquí hay algunos ejemplos de situaciones donde puedes
usar hilos de ejecución:
   Realizar largos procesamientos: Cuando una aplicación de Windows
   está realizando cálculos, no puede procesar ningún mensaje. Como
   resultado, la pantalla no puede ser actualizada.
Realizar procesamientos en segundo plano: Algunas tareas pueden
   no ser críticas, pero necesitan ser ejecutadas continuamente.
   Realizar tareas de E/S: E/S a disco o red puede tener demoras
   imposibles de prever. Los hilos de ejecución permiten asegurar que
   la demora de E/S no demora otras partes no relacionadas con esto
   en tu aplicación.
  Todos estos ejemplos tienen una cosa en común: En el programa,
algunas operaciones incurren en una potencial demora o sobrecarga del
microprocesador, pero esta demora es inaceptable para otras
operaciones; ellas necesitan estar disponibles ya. Por supuesto, hay
otros beneficios y estos son:
   Hacer uso de sistemas multiprocesador: No puedes esperar que una
   aplicación con sólo un hilo de ejecución haga uso de dos o más
   procesadores. El capítulo 3 explica esto con más detalles.
   Compartir el tiempo con eficiencia: Usar hilos de ejecución y
   prioridades en los procesos asegura una correcta justa del tiempo
   del microprocesador.
  El uso adecuado de los hilos de ejecución convierte a lentas, duras y
no muy disponibles aplicaciones en unas que tienen una brillante
respuesta, eficiencia y velocidad, además de que puede simplificar
radicalmente varios problemas de performance y usabilidad.

 Capítulo 2. Crear un hilo de ejecución en Delphi.
En este capítulo:
   Un diagrama de intervalos.
   Nuestro primer hilo no-VCL.
   ¿Qué hace exactamente este programa?
   Cuestiones, problemas y sorpresas.
   Cuestiones en la inicialización.
   Cuestiones en la comunicación.
   Cuestiones de terminación.

Un diagrama de intervalos.
Antes de meterse en los detalles de crear hilos de ejecución, y
ejecutar código independiente del hilo principal de la aplicación, es
necesario introducir un nuevo tipo de diagrama ilustrativo de la
dinámica de la ejecución de hilos. Esto nos ayudará cuando
comencemos a diseñar y crear programas multihilo. Considera esta
simple aplicación.
  La aplicación tiene un hilo de ejecución: el hilo principal de la VCL.
El progreso de este hilo puede ser ilustrado con un diagrama que
muestra el estado del hilo en la aplicación a través del tiempo. El
progreso de este hilo está representado por una línea, y el tiempo fluye
en forma descendente en la página. Incluí una referencia en este
diagrama que se aplica a todos los subsecuentes diagramas de hilos de
ejecución.




   Nótese que este diagrama no indica mucho acerca de los algoritmos
que se ejecutan. En cambio, ilustra el orden de los eventos a través del
tiempo y el estado de los hilos de ejecución entre ese tiempo. La
distancia entre diferentes puntos del diagrama no es importante, pero sí
el ordenamiento vertical de esos puntos. Hay mucha información que
se puede extraer de este diagrama.
   El hilo en esta aplicación no se ejecuta continuamente. Puede haber
   largos períodos de tiempo durante los cuales no recibe estímulos
   externos y no está llevando ningún cálculo ni ningún otro tipo de
   operación. La memoria y los recursos ocupados por la aplicación
   existen y la ventana está aún en la pantalla, pero ningún código está
   siendo ejecutado por el microprocesador.
   La aplicación es inicializada y el hilo principal es ejecutado. Una vez
   que se crea la ventana principal, no tiene más trabajo que hacer y se
   reposa sobre una pieza de código VCL conocida como el bucle de
   mensajes de la aplicación que espera más mensajes del sistema
   operativo. Si no hay más mensajes para ser procesados, el sistema
   operativo suspende el hilo y el hilo de ejecución está
   ahora suspendido.
   En un momento posterior, el usuario hace clic en el botón, para
   mostrar el mensaje de texto. El sistema operativo despierta
   (o reanuda) el hilo principal, y le entrega un mensaje indicando que
   un botón ha sido presionado. El hilo principal está
   ahora activo nuevamente.
   Este proceso de suspensión – reanudación ocurre varias veces en el
   tiempo. Ilustré una espera de confirmación del usuario para cerrar la
   caja de mensajes y espera que el botón de cerrar sea presionado. En
   la práctica, muchos otros mensajes pueden ser recibidos.

Nuestro primer hilo no-VCL
  A pesar de que el API Win32 provee un extenso soporte multihilo, al
momento de crear y destruir hilos de ejecución, el VCL tiene una clase
muy útil, TThread, que abstrae la mayoría de las técnicas para crear un
hilo, provee una simplificación muy útil, e intenta evitar que el
programador caiga en una de las muchas trampas indeseables que esta
nueva disciplina provee. Yo recomiendo su uso. La ayuda de Delphi
provee una guía razonable para crear diferentes tipos de hilos, de modo
que no voy a mencionar mucho sobre las secuencias de menú
necesarias para crear un hilo de ejecución independiente mas allá de
sugerir que el lector seleccione File | New… y luego elija Thread Object.
  Este ejemplo en particular consiste en un programa que calcula si un
número en particular es un número primo o no. Contiene dos units,
una con un formulario convencional, y otra con un objeto hilo. Más o
menos funciona; de hecho tiene algunos rasgos indeseables que ilustran
algunos de los problemas básicos que los programadores multihilo
deben considerar. Discutiremos el modo de evitar estos problemas más
tarde. Aquí está el código fuente del formulario y aquí está el código
fuente del objeto hilo.

¿Qué hace exactamente este programa?
  Cada vez que el botón “Spawn” es presionado, el programa crea un
nuevo objeto hilo, inicializa algunos campos en el objeto y luego hace
andar al hilo. Tomando el número ingresado, el hilo se aparta
calculando si el número es primo y una vez que ha terminado el
cálculo, muestra una caja de mensajes indicando si el número es primo.
Estos hilos son concurrentes, mas allá de que se tenga una máquina
uniprocesador o multiprocesador; desde el punto de vista del usuario,
estos se ejecutan en forma simultánea. Además, este programa no
limita el número de hilos creados. Como resultado, se puede demostrar
que hay una concurrencia real de la siguiente manera:
   Como he comentado un comando de salida en la rutina que
   determina si el número es primo, el tiempo que corre el hilo es
   directamente proporcional al tamaño del número ingresado. He
   notado que con un valor de aproximadamente 224, el hilo necesita
   entre 10 y 20 segundos en completarse. Encuentra un valor que
   produzca una demora similar en tu máquina.
   Ejecuta el programa, introduce un número grande y haz clic en el
   botón.
   Inmediatamente introduce un número pequeño (digamos, 42) y haz
   clic en el botón nuevamente. Notarás que el resultado para el
   número pequeño se produce antes que el resultado para el número
grande, aún cuando comenzamos el hilo con el número grande
   primero. El diagrama de abajo ilustra la situación.




Cuestiones, problemas y sorpresas.
  Hasta este punto, el tema de la sincronización se ve espinoso. Una
vez que el hilo principal llamó a Resume en un hilo “funcionando”, el
programa principal no puede asumir absolutamente nada sobre el
estado del hilo en funcionamiento y viceversa. Es completamente
posible que el hilo en funcionamiento complete su ejecución antes de
que el progreso del hilo principal de VCL termine. De hecho, para
números pequeños que toman menos de una veinteava de segundo en
calcularse, es absolutamente probable. De forma similar, el hilo en
funcionamiento no puede asumir nada acerca del estado de progreso
del hilo principal. Todo está a merced del administrador de tareas de
Win32. Hay tres “factores de gracia” que uno encuentra aquí:
cuestiones de Iniciación, cuestiones de Comunicación y cuestiones
de Terminación.
Cuestiones de iniciación.
  Delphi hace que lidiar con las cuestiones de iniciación de hilos de
ejecución sea cosa fácil. Antes de hacer correr un hilo, uno suele desear
establecer algunos estados en el hilo. Creando un hilo suspendido (un
argumento soportado por el constructor), uno puede estar seguro de
que el código no es ejecutado hasta que el hilo es reanudado (Resume).
Esto significa que el hilo principal de VCL puede leer y modificar datos
en el objeto del hilo de una forma segura, y con la garantía de que
serán actualizados y validados en el momento en que el hilo hijo
comienza a ejecutarse.
  En el caso de este programa, las propiedades del hilo
“FreeOnTerminate” (liberarse cuando termine) y “TestNumber” (la
variable), son establecidas antes de que el hilo comience a ejecutarse. Si
este no fuera el caso, el funcionamiento del hilo quedaría indefinido. Si
no deseas crear el hilo suspendido, entonces estarás pasándole el
problema de la inicialización a la siguiente categoría: cuestiones de
comunicación.

Cuestiones de comunicación.
  Esto ocurre cuando tienes dos hilos que están ambos corriendo y
necesitas comunicarte entre ellos de algún modo. Este programa evade
el problema simplemente no teniendo nada que comunicar entre los
hilos separados. De más esta decir que si no proteges todas tus
operaciones en datos compartidos (en el más estricto sentido de
“protección”), tu programa no será confiable. Si no tienes una
adecuada sincronización o un sólido control de concurrencia, lo
siguiente será imposible:
   Acceder a cualquier tipo de datos compartidos entre dos hilos.
   Interactuar con partes inseguras del VCL desde un hilo no-VCL.
   Intentar relegar operaciones relacionadas con gráficas en hilos
   independientes.
  Aún haciendo las cosas tan simples como tener dos hilos accediendo
a una variable de tipo integer compartida puede resultar en un
completo desastre. Accesos no sincronizados a recursos compartidos o
llamadas de VCL resultarán en muchas horas de tensos debugueo,
considerable confusión y eventuales internaciones en el hospital mental
más cercano. Hasta que aprendas la técnica apropiada para hacer esto
en los capítulos siguientes, no lo hagas.
  ¿La buena noticia? Puedes hacer todo lo de arriba si usas el
mecanismo correcto para controlar la concurrencia, ¡y ni siquiera es
difícil! Veremos un modo sencillo de resolver aspectos de
comunicación a través de la VCL en el próximo capitulo, y más
elegantes (y complicados) métodos luego.

Cuestiones de terminación.
  Los hilos de ejecución, al igual que otros objetos de Delphi,
involucran la asignación de memoria y recursos. No debería sorprender
saber la importancia de que el hilo termine adecuadamente, algo que el
programa de este ejemplo hace mal. Hay dos enfoques posibles para el
problema de la liberación del hilo.
  El primero es dejar que el hilo maneje el problema por sí mismo.
Esto es principalmente usado para hilos que, o comunica los resultados
de la ejecución del hilo al hilo principal de la VCL antes de terminar o
no poseen ninguna información que resulte útil para otros hilos al
momento de terminar. En estos casos, el programador puede activar la
variable “FreeOnTerminate” en el objeto hilo, y se liberará cuando
termine.
  La segunda es que el hilo principal de VCL lea datos del hilo en
funcionamiento cuando este haya terminado, y luego liberar el hilo.
Esto es tratado en el capítulo 4.
   He hecho a un lado el tema de comunicar los resultados de vuelta al
hilo principal al hacer que el hilo hijo presenta la respuesta al usuario
mediante una llamada a “ShowMessage”. Esto no involucra ningún
tipo de comunicación con el hilo principal de VCL y el llamado a
ShowMessage es seguro entre hilos, de modo que el VCL no tiene
problemas. Como resultado de esto, puedo usar el primer enfoque de
liberación del hilo, dejando que el hilo se libere a sí mismo. A pesar de
esto, el programa de ejemplo ilustra una característica indeseable al
hacer que los hilos se liberen a sí mismos:




  Como podrá notar, hay dos cosas que pueden suceder. La primera es
que intentemos salir del programa, mientras el hilo continua activo y
calculando. La segunda es que intentemos salir del programa mientras
éste esta suspendido. El primer caso es bastante malo: la aplicación
termina sin siquiera asegurarse de que no haya hilos funcionando. El
código de liberación de Delphi y Windows hace que la aplicación
termine bien. Lo segundo que podría pasar no es tan bellamente
manejable, ya que el hilo está suspendido en algún lugar dentro de las
entrañas del sistema de mensajería de Win32. Cuando la aplicación
termina, parece que Delphi hace una buena liberación en ambas
circunstancias. Sin embargo, no es un buen estilo de programación
hacer que el hilo sea forzado a finalizar sin ninguna referencia de lo que
está haciendo en el momento, de modo que un archivo pueda quedar
corrompido. Esta es la razón por la que es una buena idea tener una
buena coordinación de la salida del hilo hijo desde el hilo principal de
la VCL, aún cuando no haga falta transferir ningún dato entre los hilos:
una salida limpia del hilo y el proceso es posible. En el capitulo 4 se
discuten algunas soluciones a este problema.

            Capítulo 3. Sincronización básica.
En este capitulo:
   ¿Qué datos son compartidos entre los hilos?
   Atomicidad cuando se accede a datos compartidos.
   Problemas adicionales con la VLC.
   Diversión con máquinas multiprocesador.
   La solución Delphi: TThread.Synchronize.
   ¿Cómo funciona esto? ¿Qué hace Synchronize?
   Sincronizado a hilos no-VCL.

¿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.
                                                           Valor de la
Instrucciones ejecutadas            Instrucciones
                                                          variable A en
        por el hilo X         ejecutadas por el hilo Y
                                                            memoria
<otras instrucciones>        Hilo suspendido             1
Lee A desde la memoria en
                             Hilo suspendido             1
un registro del procesador.
Incrementa en 1 el registro
                             Hilo suspendido             1
del procesador.
Escribe los contenidos del
registro del procesador en Hilo suspendido               2
A (2) en memoria.
<otras instrucciones>        Hilo suspendido             2
CAMBIO DE HILO               CAMBIO DE HILO              2
Hilo suspendido              <otras instrucciones>       2
                             Lee A desde la memoria
Hilo suspendido              en un registro del          2
                             procesador.
                             Incrementa en 1 el registro
Hilo suspendido                                          2
                             del procesador.
                             Escribe el contenido del
Hilo suspendido              registro del procesador en 3
                             A (3) en memoria.
Hilo suspendido              <otras instrucciones>       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:
Valor de la
Instrucciones ejecutadas             Instrucciones
                                                           variable A en
        por el hilo X          ejecutadas por el hilo Y
                                                             memoria
<otras instrucciones>         Hilo suspendido             1
Lee A desde la memoria en
                              Hilo suspendido             1
un registro del procesador.
Incrementa en 1 el registro
                              Hilo suspendido             1
del procesador.
CAMBIO DE HILO                CAMBIO DE HILO              1
Hilo suspendido               <otras instrucciones>       1
                              Lee A desde la memoria
Hilo suspendido               en un registro del          1
                              procesador.
                              Incrementa en 1 el registro
Hilo suspendido                                           1
                              del procesador.
                              Escribe el contenido del
Hilo suspendido               registro del procesador en 1
                              A (2) en memoria.
CAMBIO DE HILO                CAMBIO DE HILO              2
Escribe los contenidos del
registro del procesador en Hilo suspendido                2
A (2) en memoria.
<otras instrucciones>         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.


       Capítulo 4. Destrucción simple de hilos.
En este capítulo
   Consideraciones de completado, terminación y destrucción de hilos.
   Terminado prematuro de hilos.
   El evento OnTerminate.
Terminación controlada de hilos – Efoque 1.

Consideraciones de completado, terminación y destrucción de
hilos.
  En el capitulo 2 se dio un lineamiento de algunos de los problemas
relacionado con la finalización de hilos. Hay dos consideraciones
principales:
    Salir del hilo limpiamente y limpiar todos los recursos asignados.
    Obtener los resultados del hilo cuando éste haya terminado.
   Estos puntos están fuertemente relacionados. Si un hilo no tiene que
comunicar nada al hilo principal de la VCL cuando haya terminado, o
si uno usa la técnica descripta en el capítulo anterior para comunicar
los resultados justo antes de que el hilo termine, entonces no hay
necesidad del hilo principal de VCL de participar en ninguna limpieza
del hilo. En este caso, uno puede establecer a verdadero la variable
FreeOnTerminate del hilo, y dejar que el hilo se encargue de liberarse a
sí mismo. Recuerda que si uno hace esto, el usuario puede forzar la
salida del programa, resultando en una terminación de todos los hilos
en él, con posibles consecuencias indeseables. Si el hilo sólo escribe en
la memoria, o se comunica con otras partes de la aplicación, entonces
este no es un problema, pero si escribe en un archivo o en un recurso
compartido del sistema, entonces esto es inaceptable.
  Si un hilo tiene que intercambiar información con la VCL antes de
terminar, entonces un mecanismo tiene que ser encontrado para
sincronizar el hilo principal de VCL con el hilo en funcionamiento, y el
hilo principal de VCL debe realizar la limpieza (tu tienes que escribir el
código para liberar el hilo). Dos mecanismos serán presentados luego.
  Hay un punto más para tener en cuenta:
   Terminar un hilo antes de que su curso de ejecución haya
   concluido.
  Esto puede suceder bastante seguido. Algunos hilos, especialmente
aquellos que procesan E/S, se ejecutan en un bucle permanente: el
programa puede estar recibiendo siempre más datos, y el hilo siempre
tiene que estar preparado para procesarlos hasta que el programa
termine.
 Entonces, si organizamos estos puntos en orden inverso…

Terminado prematuro de hilos.
  En algunas circunstancias, un hilo puede necesitar indicarle a otro
hilo que debe terminar. Esto generalmente ocurre si el hilo está
ejecutando una operación muy larga, y el usuario decide salir de la
aplicación, o la operación debe ser abortada. TThread provee un
mecanismo simple para soportar esto en la forma del
método Terminate, y la propiedad Terminated. Cuando un hilo es
creado su propiedad terminated se establece a false. Cuando se llama al
método terminate de un hilo, la propiedad terminated para ese hilo es
ahora true. Es la responsabilidad de todos los hilos de verificar
periódicamente si se les ha solicitado terminar, y si así fuera, salir
limpiamente. Nótese que no se producen sincronizaciones de gran
escala en este proceso; cuando un hilo activa la propiedad terminated
del otro, no puede asumir que el otro hilo ha leído el valor de la
propiedad terminated y comenzó su finalización. La propiedad
Terminated es simplemente una señal, diciendo “por favor termina tan
rápido como sea posible”. El diagrama de abajo ilustra esta situación.
Cuando se diseñan los objetos hilos, se deberá considerar leer la
variable terminated cuando sea necesario. Si el hilo se bloquea, como
resultado de algún mecanismo de sincronización de los que
discutiremos luego, podría tener que sobrecargar el método terminate
para desbloquear el hilo. En particular, recodará llamar primero al
método heredado (inherited) terminate, antes de desbloquear el hilo, si
espera que su próxima verificación de terminated devuelva verdadero.
Pronto veremos más de esto. Como ejemplo, aquí hay una pequeña
modificación al hilo que calcula los números primos del capitulo
anterior, para asegurarnos de que verifica el valor de terminated. He
asumido que es aceptable para el hilo devolver un resultado incorrecto
cuando se establece la propiedad terminated.

El evento OnTerminate.
  El evento OnTerminate ocurre cuando un hilo realmente ha
terminado su ejecución. No ocurre cuando es llamado el método
terminate. Este evento es bastante útil, en el sentido de que se ejecuta
en el contexto del hilo principal de VCL, de la misma forma en que lo
hacen los métodos pasados a synchronize. Además, si uno desea
ejecutar algunas operaciones de la VCL con un hilo que se libera
automáticamente a sí mismo, entonces este es el lugar de hacerlo. La
mayoría de los nuevos programadores de hilos de ejecución van a
encontrar esto como la mejor manera de lograr que un hilo no-VCL
transfiera sus datos de vuelta al VCL, con un mínimo de alboroto, y sin
requerir llamadas explícitas a synchronize.




  Como pueden ver en el diagrama de arriba, OnTerminate trabaja
bastante parecido a como lo hace Synchronize, y es prácticamente
idéntico semánticamente a poner una llamada a Synchronize al final del
hilo. El principal uso de esto es que, mediante el uso de indicadores,
como “La aplicación puede finalizar” o conteos de referencias de los
hilos que hay en funcionamiento en el hilo principal de VCL, un
mecanismo simple puede ser provisto para asegurarse de que el hilo
principal de VCL puede salir sólo cuando todos los demás hilos han
terminado. Aquí hay algunos detalles de sincronización involucrados,
especialmente si un programador va a poner una llamada a
Application.Terminate en el evento OnTerminate de un hilo, pero
todo esto será tratado más tarde.
Terminación controlada de hilos – Efoque 1.
  En este ejemplo, tomaremos el código del programa de números
primos del capítulo 3 y lo modificaremos de modo que el usuario no
pueda cerrar la aplicación cuando hay otros hilos ejecutándose. Esto se
vuelve simple. De hecho, no necesitamos modificar el código del hilo
ni en lo más mínimo. Nosotros simplemente agregaremos una
referencia a un campo de conteo en el hilo principal, incrementándolo
cuando se cree un nuevo hilo, estableciendo el evento OnTerminate
del hilo para que apunte a un manejador en el formulario principal que
decremente el conteo de referencia, y cuando el usuario solicite
terminar la aplicación, mostraremos una caja de diálogo de alerta si
fuera necesario.
  El ejemplo muestra lo simple de este enfoque: todo el código
concerniente con tomar cuenta de los números de hilos en ejecución
sucede en el hilo principal de VCL, y el código es esencialmente
disparado por un evento, lo mismo que como sería con cualquier otra
aplicación Delphi. En el próximo capitulo, vamos a considerar un
enfoque sensiblemente más complicado, que es beneficioso cuando se
usan mecanismos de sincronización más avanzados.


    Capítulo 5. Más sobre destrucciones de hilos.
                     Deadlock.
En este capitulo:
   El método WaitFor.
   Terminación controlada de hilos – Enfoque 2.
   Una rápida introducción al pasaje de mensajes y notificaciones.
   WaitFor puede resultar en largas demoras.
   ¿Haz notado el bug?
   Evitando esta particular manifestación de Deadlock.

El método WaitFor.
El evento OnTerminate, discutido en el capítulo anterior, es muy útil
si estás usando hilos que inicializas y luego los olvidas, con destrucción
automática. ¿Que pasa si, en cierto punto de la ejecución del hilo
principal de la VCL, quieres asegurarte de que todos los demás hilos
hayan terminado? La solución a esto es el método WaitFor. Este
método es útil si:
   El hilo principal de VCL necesita acceder al objeto hilo en
   funcionamiento antes de que su ejecución haya terminado, y ya no
   se pueda leer o modificar datos en el hilo.
   Forzar la terminación de un hilo cuando se termina el programa no
   es una opción viable.
  Bastante sencillo. Cuando el hilo A llama al método WaitFor del hilo
B, el hilo A queda suspendido hasta que el hilo B termina su ejecución.
Cuando el hilo A se vuelve a activar, puede estar seguro que los
resultados del hilo B se pueden leer, y que el objeto hilo representado
por B puede ser destruido. Típicamente esto ocurre cuando el
programa termina, donde el hilo principal de VCL llamará el método
Terminate en todos los hilos no-VCL y luego al método WaitFor en
todos los hilos no-VCL antes de salir.

Terminación controlada de hilos – Enfoque 2.
  En este ejemplo, modificaremos el código del programa de números
primos de modo que sólo un hilo se ejecute por vez, y el programa
espere hasta que el hilo complete su ejecución antes de salir. A pesar de
que en este programa no es estrictamente necesario esperar a que los
hilos terminen, es un ejercicio útil y demuestra algunas propiedades de
WaitFor que no son siempre deseables. Tambien ilustra algunos claros
bugs con los que se pueden topar programadores principiantes.
Primero que nada, el código del formulario principal. Como puede ver,
hay varias diferencias con el ejemplo anterior:
   Tenemos un “número mágico” declarado al inicio del unit. Este es
   un número arbitrario de mensaje, y su valor no es importante; es el
   único mensaje en la aplicación con este número.
En vez de tener un conteo de hilos, mantenemos una referencia
   explícita a un hilo y sólo un hilo, apuntado por la variable FThread
   del formulario principal.
   Sólo queremos que un hilo se ejecute por vez, ya que sólo tenemos
   una única variable apuntando al hilo que realizará el trabajo. Por
   este motivo, el código de creación del hilo verifica si hay hilos
   ejecutándose, antes de crear otros.
   El código de creación del hilo no establece la propiedad
   FreeOnTerminate a verdadero. En cambio, el hilo principal de VCL
   liberará el hilo en funcionamiento más tarde.
   El hilo principal tiene un manejador de mensajes definido que
   espera que el hilo en ejecución se complete y entonces lo libera.
   De igual modo, el código ejecutado cuando el usuario desea liberar
   el formulario espera que el hilo en ejecución se complete y lo libera.
  Habiendo notado estos puntos, aquí esta el hilo que hará el trabajo.
Nuevamente, hay algunas diferencias con el código presentado en el
capitulo 3.
   La función IsPrime verifica ahora si se solicitó que el hilo termine,
   resultando en una rápida salida si la propiedad terminated es
   establecida.
   La función Execute verifica si se produjo una terminación anormal.
   Si la terminación fue normal, entonces usa synchronize para
   mostrar los resultados, y envía un mensaje al formulario principal
   solicitando que el formulario principal lo libere.

Una rápida introducción al pasaje de mensajes y notificaciones.
   Bajo circunstancias normales, el hilo es ejecutado, corre por su curso,
usa synchronize para mostrar los resultados y luego envía un mensaje al
formulario principal. Este envío de mensaje es asincrónico: el
formulario principal toma el mensaje en algún punto en el
futuro. PostMessage no suspende el trabajo del hilo en ejecución, lo
hace correr hasta que se complete. Esta es una propiedad muy útil: no
podemos usar synchronize para decirle al formulario principal que
libere al hilo, porque volveremos de la llamada a Synchronize a un hilo
que no existe más. En cambio, esto simplemente actúa como una
notificación, un gentil recordatorio para el formulario principal de que
debe liberar el hilo tan rápido como le sea posible.
  En un momento posterior, el hilo del programa principal recibe el
mensaje y ejecuta al manejador. Este manejador verifica si el hilo aún
existe y, si existe, espera a que se complete su ejecución. Este paso es
necesario porque si bien es sabido que el hilo en ejecución está
terminando (no hay muchas sentencias más luego del PostMessage),
esto no es una garantía. Una vez que la espera haya terminado, el hilo
principal puede liberar el hilo que hizo el trabajo.
  El diagrama de abajo ilustra este primer caso. Para mantenerlo
simple, fueron omitidos los detalles de la operación de Synchronize del
diagrama. Además, la llamada a PostMessage se muestra como que
ocurre en algún momento antes de que el hilo completa su
funcionamiento de modo de ilustrar el funcionamiento de la operación
WaitFor.
En capítulos posteriores se va a cubrir la ventaja de enviar mensajes
con mayor detalle. Es suficiente decir hasta este punto que esta técnica
es muy útil cuando se trata de comunicarse con el hilo VCL.
   En un caso anormal de funcionamiento, el usuario intentará salir de
la aplicación, y confirmará que desea salir inmediatamente. El hilo
principal establecerá la propiedad terminated del hilo en proceso, lo
que se espera que provoque una terminación en un tiempo
razonablemente corto, y luego aguardará para que este se complete.
Una vez que se ha completado el procesamiento del hilo, el proceso de
liberación es como el caso anterior. El diagrama de abajo ilustra el
nuevo caso.




  Muchos lectores estarán perfectamente felices a estas alturas. Sin
embargo, los problemas vuelven a aparecer, y como es común cuando
consideramos la sincronización multihilo, el diablo está en los detalles.

WaitFor puede resultar en largas demoras.
  El beneficio de WaitFor es también su mayor desventaja: suspende el
hilo principal en un estado en el que no puede recibir mensajes. Esto
significa que la aplicación no puede realizar ninguna de las operaciones
normalmente asociadas con el procesamiento de mensajes: la
aplicación no re-dibujará, no se re-dimensionará ni responderá a
ningún estímulo externo cuando está esperando. Tan pronto como el
usuario lo note, pensará que la aplicación se colgó. Esto no es un
problema en el caso de un hilo que termina normalmente; llamando a
PostMessage, la última operación en el hilo en funcionamiento, nos
aseguramos de que el hilo principal no tendrá que esperar mucho. Sin
embargo, en el caso de una terminación anormal del hilo, la cantidad
de tiempo que el hilo principal pierde en este estado depende de que
tan frecuentemente verifique el hilo de ejecución la propiedad
terminate. El código fuente para PrimeThread tiene una línea marcada
“Line A”. Si se le quita el fragmento “and not terminated”, podrá
experimentar que sucede al finalizar la aplicación durante la ejecución
de un cálculo que dure mucho tiempo.
  Hay algunos métodos avanzados para suprimir este problema que
involucra a las funciones Win32 de espera de mensajes, una explicación
de este método se puede encontrar
visitando http://www.midnightbeach.com/jon/pubs/MsgWaits/Msg
Waits.html. En suma, es simple escribir hilos que verifican la propiedad
Terminated con cierta regularidad. Si esto no es posible, entonces es
preferible mostrarle algunas advertencias al usuario acerca de la
potencial irresponsabilidad de la aplicación (a la Microsoft Exchange).

¿Haz notado el bug? WaitFor y Synchronize: una introducción a
Deadlock.
  La demora de WaitFor es realmente un problema menor, cuando se
lo compara con otros vicios que tiene. En aplicaciones que usan
Synchronize y WaitFor, es completamente posible hacer que la
aplicación caiga en un Deadlock. Deadlock es un fenómeno donde no
hay problemas de algoritmos en la aplicación, pero toda la aplicación se
detiene, muerta en el agua. El caso general es que Deadlock ocurra
cuando un hilo espera por el otro en forma cíclica. El hilo A esta
esperando por el hilo B para completar algunas operaciones, mientras
que el hilo C espera por el hilo D, etc. etc. Al final de la línea, el hilo D
estará esperando por el hilo A para completar algunas operaciones.
Desgraciadamente el hilo A no puede completar la operación porque
está suspendido. Esto es el equivalente en computación del problema:
“A: Tu vas primero… B: No, tu… A: No, ¡insisto!” que acosa a los
motoristas cuando el derecho de paso no está claro. Este tipo de
funcionamiento está documentado en los archivos de ayuda de la VCL.
   En este caso en particular, el Deadlock puede ocurrir entre dos hilos
de ejecución si el hilo de cálculo llama a Synchronize poco tiempo
antes de que el hilo principal llame a WaitFor. Si esto sucediera,
entonces el hilo de cálculo estará esperando que el hilo principal se
libere para regresar al bucle de mensajes, mientras que el hilo principal
está esperando que el hilo de cálculo se complete. Deadlock ocurrirá.
También es posible que el hilo principal de VCL llame a WaitFor poco
tiempo antes de que el hilo de cálculo llame a Synchronize. Dando una
implementación simplista, esto también resultaría en un Deadlock. Por
suerte, los que hicieron la VCL trataron de sortear este caso de error, lo
que resulta en el surgimiento de una excepción en el hilo de cálculo,
rompiendo el Deadlock y finalizando el hilo.




  La programación del ejemplo, como está, se vuelve bastante
indeseable. El hilo de cálculo llama a Synchronize si verifica que
Terminated está es falso poco antes de terminar su ejecución. El hilo
principal de la aplicación establece terminated poco antes de llamar a
WaitFor. De modo que, para que ocurra un Deadlock, el hilo de
cálculo deberá encontrar Terminated en falso, ejecutar Synchronize, y
luego el control debe ser transferido al hilo principal exactamente en el
punto donde el usuario ha confirmado forzar la salida.
  Más allá del hecho de que estos casos de Deadlock son indeseables,
eventos de este tipo son claras condiciones de carrera. Todo depende
del momento exacto de los eventos, lo que variará de funcionamiento
en funcionamiento en la máquina. El 99.9% de las veces, un cierre
forzado funcionará, y una en mil veces, todo se bloqueará: exactamente
el tipo de problema que necesitamos evitar a toda costa. El lector
recordará que anteriormente le mencioné que ninguna sincronización
de gran escala ocurrirá cuando se está leyendo o escribiendo la
propiedad terminated. Esto quiere decir que no es posible usar la
propiedad terminated para evitar este problema, como el diagrama
anterior lo deja en claro.
  Algún lector interesado en duplicar el problema del Deadlock, puede
hacer relativamente fácil, modificando los siguientes fragmentos del
código fuente:
   Quite el texto “and not terminated” a la altura de “Line A”
   Remplace el texto “not terminated” a la altura de “Line B” por
   “true”
   Quite el comentario en “Line C”
  El deadlock puede ser entonces provocado corriendo un hilo cuya
ejecución demore cerca de 20 segundos, y forzar la salida de la
aplicación poco tiempo después de que el hilo fue creado. El lector
puede desear también ajustar el tiempo que el hilo principal de la
aplicación se suspende, de modo de saber el “correcto” ordenamiento
de los eventos:
   El usuario comienza cualquier hilo de cálculo.
   El usuario intenta salir y dice: “Sí, quiero salir más allá de que haya
   un hilo en funcionamiento”.
   El hilo principal de la aplicación se suspende (Line C)
   El hilo de cálculo eventualmente llega al final de la ejecución y llama
   a Synchronize. (asistido por las modificaciones en las líneas A y B).
El hilo principal de la aplicación se reactiva y llama a WaitFor.

Evitando esta particular manifestación de Deadlock.
  El mejor modo de evitar esta forma de Deadlock, es no usar WaitFor
y Synchronize en la misma aplicación. WaitFor puede ser evitado
usando el evento OnTerminate, como fue expuesto previamente. Por
suerte, en este ejemplo, el resultado del hilo es suficientemente simple
como para evitar usar Synchronize a favor de un modo más trivial.
Usando WaitFor, el hilo principal puede ahora acceder legalmente a las
propiedades del hilo en funcionamiento luego de que éste termina, y
todo lo que se necesita es una variable “resultado” para contener el
texto producido por el hilo de cálculo. Las modificaciones necesarias
para esto son:
   Quitar el método “DisplayResults” del hilo.
   Agregar una propiedad al hilo de cálculo.
   Modificar el manejador de mensajes en el formulario principal.
  Aquí hay cambios relevantes. Con esto termina la discusión de los
mecanismos de sincronización comunes a todas las versiones Win32 de
Delphi. Aún no he discutido dos métodos: TThread.Suspend y
TThread.Resume. Estos son discutidos en el capitulo 10. Los
siguientes capítulos exploran las facilidades del API Win32, y
posteriores versiones de Delphi. Sugiero que, una vez que el usuario
haya asimilado los aspectos básicos de la programación multihilo en
Delphi, se tome el tiempo de estudiar estos mecanismos más
avanzados, ya que son una buena manera, más flexible, que trabajar
con los mecanismos nativos de Delphi, y permiten al programador
coordinar hilos de ejecución en un modo más elegante y eficiente, así
como reducir las posibilidades de escribir código que pueda caer en
Deadlocks.


Capítulo 6. Más sincronización: Secciones críticas
                   y mutexes.
En este capítulo:
   Limitaciones de la sincronización.
   Secciones críticas.
   ¿Qué significa todo esto para el programador Delphi?
   Puntos de interés.
   ¿Pueden perderse los datos o quedar congelados en el buffer?
   ¿Qué hay de los mensajes “desactualizados”?
   Control de Flujo: consideraciones y lista de ineficiencias.
   Mutexes.

Limitaciones de la sincronización.
  Synchronize tiene algunas desventajas que lo hacen inadecuado para
cualquier cosa, salvo aplicaciones multihilo muy sencillas.
  Synchronize es útil solamente cuando se intenta comunicar un hilo
  en funcionamiento con el hilo principal de VCL.
  Synchronize insiste en que el hilo en funcionamiento espere hasta
  que el hilo principal de VCL esté completamente inactivo aún
  cuando esto no es estrictamente necesario.
  Si las aplicaciones hacen un uso frecuente de Synchronize, el hilo
  principal de VCL se vuelve un cuello de botella y no una verdadera
  ganancia de performance.
  Si Synchronize es usado para comunicar indirectamente dos hilos
  en ejecución, ambos hilos pueden quedar suspendidos esperando
  por el hilo principal de VCL.
  Synchronize puede causar Deadlock si el hilo principal de VCL
  espera por algún otro hilo.
 En la parte de las ventajas, Synchronize tiene una por sobre la
mayoría de los demás mecanismos de sincronización:
   Casi cualquier código puede ser pasado a Synchronize, incluso
   código VCL inseguro entre hilos.
  Es importante recordar porque los hilos son usados en la aplicación.
La principal razón para la mayoría de los programadores Delphi es que
quieren que sus aplicaciones permanezcan siempre con capacidad de
respuesta, mientras se estén realizando otras operaciones que pueden
llevar más tiempo o usan transferencias de datos con bloqueo o E/S.
Esto generalmente significa que el hilo principal de la aplicación debe
realizar rutinas cortas, basadas en eventos y el manejo de las
actualizaciones de la interfaz. Es bueno al responder a las entradas de
usuario y mostrar las salidas al usuario. Los otros hilos no usan partes
de la VCL que no son seguros para trabajar con múltiples hilos. Los
hilos que realizan el trabajo pueden realizar operaciones con archivos,
bases de datos, pero rara vez usarán descendentes de TControl. A la
vista de esto, Synchronize es un caso perdido.
  Muchos hilos necesitan comunicarse con la VCL de una manera
sencilla, como realizar transferencias de cadenas de datos, o ejecutar
querys de bases de datos y devolver una estructura de datos como
resultado del query. Volviendo atrás, al capitulo 3, notamos que sólo
necesitamos mantener la atomicidad cuando modificamos datos
compartidos. Para tomar un ejemplo sencillo, nosotros podemos tener
una cadena que puede ser escrita por un hilo de procesamiento y ser
leída periódicamente por el hilo principal de VCL. ¿Necesitamos
asegurarnos que el hilo principal de VCL no se está ejecutando nunca
en el mismo momento que el hilo en funcionamiento? ¡Por supuesto
que no! Todo lo que necesitamos asegurarnos es que sólo un hilo por
vez modifica este recurso compartido, de modo de eliminar las
condiciones de carrera y hacer las operaciones en los recursos
compartidos atómicas. Esta propiedad es conocida como exclusión
mutua. Hay muchas primitivas de sincronización que pueden ser
usadas para forzar esta propiedad. La más simple de esta es conocida
como Mutex. Win32 provee la primitiva mutex, y una pariente cercana
de esta, la Sección Crítica (Critical Section). Algunas versiones de
Delphi poseen una clase que encapsula las llamadas a secciones críticas
Win32. Esta clase no será discutida aquí, ya que su funcionalidad no es
común a todas las versiones de 32 bits de Delphi. Los usuarios de esa
clase han de tener algunas dificultades usando los métodos
correspondientes en la clase para lograr los mismos efectos que los
discutidos aquí.

Secciones Críticas.
La sección crítica es una primitiva que nos permite forzar la
exclusión mutua. El API Win32 soporta varias operaciones sobre esta:
    InitializeCriticalSection.
    DeleteCriticalSection.
    EnterCriticalSection.
    LeaveCriticalSection.
    TryEnterCriticalSection (Windows NT unicamente).
  Las operaciones InitializeCriticalSection y DeleteCriticalSection
pueden considerarse como algo muy parecido a la creación y
destrucción de objetos en memoria. Por ende, es sensato dejar la
creación y destrucción de secciones críticas a un hilo en particular,
normalmente el que exista más tiempo en memoria. Obviamente,
todos los hilos que quieran tener un acceso sincronizado usando esta
primitiva deberán tener un manejador o puntero a esta primitiva. Esto
puede ser directo, a través de una variable compartida, o indirecto,
quizá porque la sección crítica está embebida en un clase hilo segura, a
la que ambos hilos puedan acceder.
   Una vez que el objeto sección crítica es creado, puede ser usado para
controlar el acceso a recursos compartidos. Las dos operaciones
principales son EnterCriticalSection y LeaveCriticalSection. En una
gran lucha de la literatura estándar en el tema de las sincronizaciones,
estas operaciones son también conocidas como WAIT y SIGNAL,
o LOCK y UNLOCKrespectivamente. Estos términos alternativos
son también usados para otras primitivas de sincronización, y tienen
significados equivalentes. Por defecto, cuando se crea la sección crítica,
, ninguno de los hilos de la aplicación tiene posesión de ella. Para
obtener posesión, un hilo debe llamar a EnterCriticalSection, y si la
sección crítica no pertenece a nadie, entonces el hilo obtiene su
posesión. Es entonces cuando, típicamente, el hilo realiza operaciones
sobre recursos compartidos (la parte crítica del codigo, ilustrada por
una doble línea), y una vez que ha terminado, libera su posesión
mediante un llamado a LeaveCriticalSection.
La propiedad que tienen las secciones críticas es que sólo un hilo por
vez puede ser propietario de alguna de ellas. Si un hilo intenta entrar a
una sección crítica cuando otro hilo está aún en la sección crítica, el
que intenta entrar quedará suspendido, y solamente se reactivará
cuando el otro hilo abandone la sección crítica. Esto nos provee la
exclusión mutua necesaria con los recursos compartidos. Más de un
hilo puede ser suspendido, esperando ser propietario en algún
momento, de modo que las secciones críticas pueden ser útiles para
sincronizaciones entre más de dos hilos. A modo de ejemplo, aquí está
lo que sucedería si cuatros hilos intentaran tener acceso a la misma
sección crítica en momentos muy cercanos.
Como deja en claro el gráfico, sólo un hilo esta ejecutando código
crítico por vez, de modo que no hay problemas de carreras ni de
atomicidad.

¿Qué significa todo esto para el programador Delphi?
  Esto significa que, más allá de que uno no esté realizando
operaciones con la VCL, sino sólo haciendo sencillas transferencias de
datos, el programador de hilos en Delphi es libre de la carga que
significa trabajar con TThread.Synchronize.
   El hilo principal de la VCL no necesita estar inactivo antes de que el
   hilo en proceso pueda modificar recursos compartidos, sólo
   necesita estar fuera de la sección crítica.
   Las secciones críticas no saben ni les preocupa saber si un hilo es el
   hilo principal de la VCL o una instancia de un objeto TThread, de
   modo que uno puede usar las secciones críticas entre cualquier par
   de hilos.
   El programador de hilos puede ahora (prácticamente) usar WaitFor
   en forma segura, evitando problemas de Deadlock.
El último punto no es absoluto, ya que aún es posible producir
Deadlocks de la misma manera que antes. Todo lo que uno tiene que
hacer es llamar a WaitFor en el hilo principal cuando está actualmente
en una sección crítica. Como veremos luego, suspender hilos por
largos períodos de tiempo mientras está en una sección crítica es
normalmente una mala idea. Ahora que la teoría fue explicada
adecuadamente, presentaré un nuevo ejemplo. Este es un poco más
elegante e interesante que el programa de números primos. Cuando
empieza, intenta buscar números primos empezando por el 2, y sigue
hacia arriba. Cada vez que encuentra un número primo, actualiza una
estructura de datos compartida (una lista de strings) e informa al hilo
principal que ha agregado datos a la lista de strings. Aquí está el código
del formulario principal.
  Es bastante similar a los ejemplos anteriores con respecto a la
creación del hilo, pero hay algunos miembros extra en el formulario
principal que deben ser inicializadas. StringSection es la sección crítica
que controla el acceso al recurso compartido entre hilos. FStringBuf es
una lista de strings que actúa como buffer entre el formulario principal
y el hilo en proceso. El hilo en proceso envía los resultados al
formulario principal agregándolos a esta lista de strings, que es el único
recurso compartido en este programa. Finalmente tenemos una
variable boleana, FStringSectInit. Esta variable actúa como un
verificador, asegurándose que los objetos necesarios en la
sincronización están realmente creados antes de ser usados. Los
recursos compartidos son creados cuando comenzamos un hilo de
procesamiento y se destruyen poco tiempo después de que estemos
seguros que el hilo de procesamiento ha salido. Nótese que pese a que
las listas de strings actúan como buffer que son asignados
dinámicamente,debemos usar WaitFor al momento de destruir el hilo,
para asegurarnos que el hilo de procesamiento no usa más el buffer
antes de liberarlo.
  Podemos usar WaitFor en este programa sin tener que preocuparnos
por posibles Deadlocks, porque podemos probar que no hay nunca
una situación donde dos hilos se estén esperando uno al otro. La línea
de razonamiento para probar esto es bien simple:
1. El hilo de procesamiento sólo espera cuando intenta ganar acceso a
   la sección crítica.
2. El hilo del programa principal sólo espera cuando está esperando que
   el hilo de procesamiento termine.
3. El programa principal no espera cuando tiene posesión de la sección
   crítica.
4. Si el hilo de procesamiento está esperando por la sección crítica, el
   programa principal abandonará la sección crítica antes de esperar por
   algún motivo al hilo de procesamiento.
   Aquí está el código del hilo de procesamiento. El hilo de
 procesamiento busca a través de sucesivos enteros positivos, tratando
 de encontrar alguno que sea primo. Cuando lo encuentra, toma
 posesión de la sección crítica, modifica el buffer, abandona la sección
 crítica y luego envía un mensaje al formulario principal indicando que
 hay datos en el buffer.

Puntos de interés.
   Este ejemplo es más complicado que los ejemplos anteriores, porque
tenemos un largo de buffer arbitrario entre dos hilos, y como
resultado, hay varios problemas que deben ser considerados y evitados,
como así también algunas características del código que lidian con
situaciones inesperadas. Estos puntos se pueden resumir en:
    ¿Pueden perderse los datos o quedar congelados en el buffer?
    ¿Qué hay acerca de mensajes “desactualizados”?
    Aspectos de control de flujo.
    Ineficiencias en la lista de strings, dimensionado estático vs.
    dinámico.

¿Pueden perderse los datos o quedar congelados en el buffer?
  El hilo de procesamiento le indica al hilo principal del programa que
hay datos para procesar en el buffer mediante el envío de un mensaje.
Vale la pena hacer notar que, cuando se usan mensajes de Windows de
esta manera, no hay nada inherente al objeto de sincronización del hilo
que enlace a un mensaje de windows con una actualización en
particular del buffer. Por suerte en este caso, las reglas de causa y
efecto funcionan a nuestro favor: cuando el buffer es actualizado, un
mensaje es enviado después de la actualización. Esto significa que el hilo
principal del programa siempre recibe mensajes de actualización del
buffer después de una actualización del buffer. Por este motivo, es
imposible que los datos permanezcan en el buffer por una
indeterminada cantidad de tiempo. Si los datos están actualmente en el
buffer, el hilo de procesamiento y el hilo principal están en algún punto
en el proceso desde el envío a la recepción de mensajes de
actualización del buffer. Nótese que si el hilo de procesamiento enviara
un mensaje antes de actualizar el buffer, puede ser posible que el hilo
principal procese el mensaje y lea el buffer antes de que el hilo de
procesamiento actualice el buffer con los resultados más recientes,
provocando que los resultados más recientes queden atascados en el
buffer por algún tiempo.

¿Qué hay de los mensajes “desactualizados”?
   Las leyes de causa y efecto funcionaron bien en el caso anterior, pero
por desgracia, los problemas de comunicación también cuentan. Si el
hilo principal está ocupado actualizando por un largo período de
tiempo, es posible que los mensajes se apilen en el la cola, de modo
que recibimos los mensajes de actualizaciones mucho tiempo después
de que el hilo de procesamiento enviara esos mensajes. En la mayoría
de las situaciones, esto no presenta un problema. Sin embargo, un caso
particular que necesita ser considerado es el caso de que el usuario
detenga al hilo de procesamiento, ya sea directamente, presionando el
botón “stop”, o indirectamente, mediante el cierre del programa. En
este caso, es completamente posible para el hilo principal de VCL
terminar el hilo de procesamiento, quitar todos los objetos de
sincronización y el buffer, y luego, subsecuentemente, recibir mensajes
que se han apilado durante algún tiempo. En el ejemplo mostrado,
verifiqué este problema, asegurándome que la sección crítica y el objeto
buffer existen antes de procesar los mensajes (La línea de código
comentada Not necessarily the case!). Esta consideración tiende a ser
suficiente para la mayoría de las aplicaciones.
Consideraciones de control de flujo y lista de ineficiencias.
  Atrás, en el capitulo 2, dije que una vez que se crean hilos, no existe
ninguna sincronización implícita entre ellos. Esto era evidente en
ejemplos anteriores, como fue demostrado con el problema que puede
causar el intercambio de datos entre hilos, como una manifestación del
nivel del problema de sincronización en un programa. El mismo
problema existe al querer sincronizar la transferencia de datos. No hay nada
en el ejemplo de arriba que garantice que el hilo de procesamiento
producirá resultados lo suficientemente rápido para que el hilo
principal de VCL los pueda tomar cuando los muestra. De hecho, si el
programa se ejecuta de modo que el hilo de procesamiento comienza
buscando números primos pequeños, es bastante probable que,
compartiendo igual cantidad de tiempo de CPU, el hilo de
procesamiento desplace el hilo VCL por un margen bastante grande.
Este problema es solucionado mediante algo que se llama control de
flujo.
  Control de flujo es el nombre dado al proceso por el que la velocidad
de ejecución de algunos hilos es balanceada de modo que la tasa de
entradas en el buffer y la tasa de salidas estén medianamente
balanceadas. El ejemplo de arriba es particularmente simple, pero
ocurre en muchos otros casos. Casi cualquier E/S o mecanismo de
transferencia de datos entre hilos o procesos incorpora algún tipo de
control de flujo. En casos simples, esto simplemente puede involucrar
alguna pieza excepcional de dato en tránsito, suspendiendo ya sea
al productor (el hilo que coloca los datos en el buffer) o
al consumidor (el hilo que toma los datos). En casos más complejos,
el hilo puede ejecutarse en diferentes máquinas y el “buffer” puede
estar compuesto de buffers internos en esas máquinas, y las
capacidades de almacenamiento de la red entre ellas. Una gran parte del
protocolo TCP es la que administra el control de flujo. Cada vez que
descargas una página web, el protocolo TCP arbitra entre las dos
máquinas, asegurándose que más allá del microprocesador o la
velocidad de disco, toda la transferencia de datos ocurre a una tasa que
puedan manejar las dos máquinas [1] . En el caso de nuestro ejemplo
de arriba, se hizo un intento tosco de controlar el flujo. La prioridad
del hilo de procesamiento ha sido establecida de modo que el
administrador de tareas seleccione preferentemente al hilo principal de
la VLC y no al hilo de procesamiento, mas allá de que ambos tengan
trabajo que hacer. En el administrador de tareas de Win32, esto
soluciona el problema, pero no es realmente una garantía de hierro.
   Otro aspecto relacionado con el control de flujo es que, en el caso
del ejemplo de arriba, el tamaño del buffer es ilimitado. Primero, esto
crea un problema de eficiencia, en el que el hilo principal de la VCL
tiene que hacer un gran número de movimientos de memoria cuando
quita el primer elemento de una larga lista de strings, y segundo, esto
significa que con el control de flujo mencionado arriba, el buffer puede
crecer sin límite. Intenta quitar la sentencia que establece la prioridad
del hilo. Notarás que el hilo de procesamiento genera resultados mas
rápido de lo que el hilo principal de VCL pueda procesar, lo que hace a
la lista de strings muy larga. Esto, además, lentifica más el hilo principal
de la VCL (ya que las operaciones para quitar strings en una lista larga
toman mas tiempo), y el problema se vuelve peor. Eventualmente,
notará que la lista se vuelve tan larga como para llenar la memoria
principal, la máquina comenzará a retorcerse y todo se detendrá
ruidosamente. ¡Tan caótico es, que cuando probé el ejemplo, no pude
conseguir que Delphi respondiera a mis solicitudes para salir de la
aplicación, y tuve que recurrir al administrador de tareas de Windows
NT para terminar el proceso!
  Simplemente piensa en lo que este programa parece a primera vista.
Ha disparado un gran número de potenciales gremlins. Soluciones más
robustas a este problema son discutidas en la segunda parte de esta
guía.

Mutexes.
  Un mutex funciona exactamente del mismo modo que las secciones
críticas. La única diferencia en las implementaciones Win32 es que la
sección crítica esta limitada para ser usada con solamente un proceso.
Si tienes un programa que usa varios hilos, entonces la sección crítica
es liviana y adecuada para tus necesidades. Sin embargo, cuando
escribes una DLL, es muy posible que diferentes procesos usen la DLL
en el mismo momento. En este caso, debes usar mutexes, en lugar de
secciones críticas. Pese a que el API Win32 provee un rango más
variado de funciones para trabajar con mutexes y otros objetos de
sincronización que serán explicados aquí, las siguientes funciones son
análogas a las descriptas para secciones críticas más arriba:
   CreateMutex / OpenMutex
   CloseHandle
   WaitForSingleObject(Ex)
   ReleaseMutex
  Estas funciones están bien documentadas en los archivos de ayuda
del API Win32, y serán discutidas en más detalle luego.
   [1] El protocolo TCP también realiza muchas otras funciones raras y maravillosas, como
copiar con datos perdidos y el optimizado del tamaño de las ventanas de modo que el flujo de
la información no sólo se ajusta a las dos máquinas en los extremos de la conexión, sino
también a la red que las une, mientras mantiene una mínima latencia y maximizando la
conexión. También posee algoritmos de back-off para asegurarse que varias conexiones TCP
puedan compartir una conexión física, sin que ninguna de ellas monopolice el recurso físico.



      Capítulo 7. Guía de programación de mutex.
                Control de concurrencia.
En este capitulo:
    Momento para introducir un poco de estilo.
    Deadlock en función del ordenamiento de mutex.
    Evitando el Deadlock de un hilo, dejando que la espera de time-out.
    Evitando el Deadlock de un hilo, imponiendo un orden en la
    adquisición de mutex.
    Fuera de la cacerola y ¡en el fuego!
    Evitando el Deadlock al “modo vago” y dejando que Win32 lo haga
    por ti.
    Atomicidad en la composición de operaciones – optimismo versus
    pesimismo en el control de concurrencia.
    Control de concurrencia optimista.
Control de concurrencia pesimista.
   Evitando agujeros en el esquema de bloqueo.
   ¿Ya está confundido? ¡Puede tirar la toalla!

¿Momento para introducir un poco de estilo?
  La mayoría de los ejemplos presentados en este tutorial eran bastante
puntuales y preparados. Cuando diseñamos componentes reusables, o
las bibliotecas para una gran aplicación multihilo, una concepción de
“vuelo de águila” no es apropiada. El programador o diseñador de
componentes necesitan construir clases que tengan seguridad para la
programación multihilo en sí mismos, es decir, clases que asuman que
podrían ser accedidas desde diferentes hilos y poseer los mecanismos
internos necesarios para asegurarse de que los datos se mantengan
consistentes. Para hacer esto, el diseñador de componentes necesita
estar al tanto de algunos problemas que surgen cuando se usan mutex
en aplicaciones cada vez más complicadas. Si esta tratando de escribir
una clase que sea segura para funcionar con hilos por primera vez, no
se deje desanimar por la aparente complejidad de algunas
consideraciones de este capitulo. Con bastante frecuencia se pueden
adoptar soluciones simplistas, que nos evitan muchas de las
consideraciones mencionadas en este capitulo, a cambio de una menor
eficiencia. Nótese que cada vez que se mencione “mutex” de aquí en
más, lo mismo vale para las secciones críticas; omitiré mencionar las
secciones críticas en cada caso para abreviar.

Deadlock en función del ordenamiento de mutex.
  Si un programa posee más de un mutex, entonces será
sorprendentemente sencillo provocar un Deadlock, con un código de
sincronismo descuidado. La situación más común es cuando existen
dependencias cíclicas por el orden en que los mutex son adquiridos.
Esto es generalmente conocido en la literatura académica como el
problema de la cena de los filósofos. Como vimos antes, el criterio de
un Deadlock es que todos los hilos están esperando a otro para liberar
el objeto de sincronización. El ejemplo más sencillo de esto es entre
dos hilos, uno que quiere adquirir el mutex A antes de adquirir el
mutex B y otro que quiere adquirir el mutex B antes de adquirir el
mutex A.




  Por supuesto, es completamente posible hacer caer un programa en
un Deadlock de una manera más delicada con una cadena de
dependencias, como la ilustrada más abajo con cuatro hilos y cuatro
mutexes, A a D.




  Obviamente, situaciones como esta no son aceptables en la mayoría
de las aplicaciones. Hay muchas maneras de evitar este problema, y un
montón de técnicas para aliviar problemas de dependencia de este tipo,
haciendo mucho más sencillo evitar situaciones de Deadlock.

Evitando el Deadlock de un hilo, dejando que la espera de time-
out.
Las funciones de Win32 para lidiar con mutex no requieren que un hilo
espere por siempre para adquirir un objeto mutex. La función
WaitForSingleObject le permite a uno especificar un tiempo que el hilo
está preparado a esperar. Una vez que ha pasado este tiempo, el hilo
será desbloqueado y la llamada devolverá un código de error indicando
que a la espera se le acabó el tiempo (time-out). Cuando usamos mutex
para forzar el acceso sobre una región crítica del código, uno no espera
típicamente que el hilo tenga que esperar mucho tiempo, y un time-out
establecido para suceder en pocos segundos debería ser apropiado. Si
tu hilo usa este método, entonces deberá, por supuesto, poder manejar
situaciones de error en forma adecuada, quizás volviéndolo a intentar o
abandonándolo. Desde luego que los usuarios de las secciones críticas
no tienen este lujo, ya que las funciones de espera de las funciones
críticas esperan por siempre.

Evitando el Deadlock de un hilo imponiendo un orden en la
adquisición de mutex.
  Si bien es una buena idea ser capaz de manejar situaciones de error al
adquirir un mutex, es una buena práctica asegurarse que las situaciones
de Deadlock no sucedan en primer lugar. Como este tipo de Deadlock
es provocado por dependencias cíclicas, puede ser eliminado al
imponer un orden en la adquisición de mutexes. Este ordenamiento es
muy sencillo. Digamos que tenemos un programa con mutexes M1,
M2, M3, … Mn, donde uno o más de estos mutex pueden ser
adquiridos por los hilos en el programa.
   El Deadlock no ocurrirá ya que para algún mutex arbitrario Mx, los
   hilos sólo intentarán adquirir el mutex Mx si no tienen posesión de
   alguno de los mutex de “mayor prioridad”, esto es M(x+1)… Mn.
  ¿Suena un poco abstracto? Tomemos un ejemplo concreto bastante
sencillo. En esta parte del capitulo, me referiré a objetos de “bloqueo”
y “desbloqueo”. Esta terminología parece apropiada cuando un mutex
está asociado con un dato, y el acceso atómico a ese dato es necesario.
Uno debería notar que esto efectivamente significa que cada hilo
obtiene el mutex antes de acceder a un objeto, y abandona el mutex
después de haber accedido: la operación es idéntica a las discutidas
anteriormente, el único cambio está en la terminología, que para esta
coyuntura, es más apropiada para un modelo orientado a objetos. En
esencia, Objeto.Lock puede ser considerado completamente
equivalente a EnterCriticalSection(Objecto.CriticalSection) o quizás
WaitForSingleObject(Objeto.Mutex, INFINITE).




  Tenemos una lista con estructuras de datos que es accedida por
varios hilos. Enganchados a la lista hay algunos objetos, cada uno de
los cuales tiene su propio mutex. De momento, asumiremos que la
estructura de la lista es estática, no cambia, y puede ser leída libremente
por los hilos sin ningún tipo de bloqueo. Los hilos que operan en esta
estructura de datos quieren hacer alguna de estas cosas:
   Leer un ítem, bloqueándolo, leyendo los datos, y luego
   desbloqueándolo.
   Escribir en un ítem, bloqueándolo, escribiendo los datos, y luego
   desbloqueándolo.
   Comparar dos ítems, bloqueándolos primero en la lista, luego
   realizando la comparación y desbloqueándolo.
  Un simple pseudo-código para estas funciones, ignorando los tipos,
manejos de excepciones y otros aspectos que no son centrales, puede
verse como algo así.
   Imaginémonos por un momento que a un hilo se le pide comparar
los ítems X e Y de la lista. Si el hilo siempre bloquea X y luego Y,
entonces podría ocurrir un Deadlock si a un hilo se le pide comparar
ítems 1 y 2, y a otro hilo se le pide comparar ítems 2 y 1. Una solución
sencilla sería bloquear primero el ítem cuyo número sea el menor, u
ordenar los índices de entrada, realizar los bloqueos y ajustar los
resultados de la comparación apropiadamente. Sin embargo, una
situación más interesante es cuando un objeto contiene detalles de otro
objeto con el que es necesario hacer la comparación. En esta situación,
el hilo puede bloquear el primer objeto, obtener el índice del segundo
objeto en la lista, darse cuenta que el índice de este es menor en la lista,
bloquearlo y proceder luego con la comparación. Todo muy fácil. El
problema ocurre cuando el segundo objeto tiene mayor índice en la
lista que el primero. No podemos bloquearlo inmediatamente, porque
de hacerlo, estaríamos permitiendo que se produzca un Deadlock. Lo
que debemos hacer es desbloquear el primer objeto, bloquear el
segundo y luego volver a bloquear el primero. Esto nos asegura que el
Deadlock no ocurrirá. Aquí hay un ejemplo de comparación indirecta,
representativo de esta discusión.

Fuera de la cacerola y ¡en el fuego!
   Si bien esto evita las situaciones de Deadlock, crea un problema
peliagudo. En la demora entre desbloqueo y vuelta a bloquear del
primer objeto, no podemos estar seguros que otro hilo no ha
modificado el primero objeto antes de que hayamos vuelto. Esto se da
porque nosotros realizamos una operación compuesta: la operación en
sí no es más atómica. Solucione a este problema son discutidas más
abajo, en la página.

Evitando el Deadlock al “modo vago” y dejando que Win32 lo
haga por ti.
  Concientes de la gimnasia mental que estos problemas pueden
presentar, los adorables diseñadores de Sistemas Operativos en
Microsoft, nos han provisto de una manera de solucionar el problema
mediante otra función de sincronización de Win32:
WaitForMultipleObjects(Ex). Esta función le permite al programador
esperar para adquirir muchos objetos de sincronización (incluyendo
mutex) de una vez. En particular, esto le permite a un hilo esperar
hasta que uno o todo un grupo de objetos estén libres (en el caso de
mutex, el equivalente seria “sin propietario”), y luego adquirir la
propiedad de los objetos señalados. Esto tiene la gran ventaja de que si
dos hilos esperan por los mutex A y B, no importa que orden
especificaron en el grupo de objetos para esperar, o ningún objeto es
adquirido o todos son adquiridos atómicamente, de modo que es
imposible un caso de deadlock de esta manera.
   Este enfoque también tiene algunas desventajas. La primera
desventaja es que como todos los objetos de sincronización deben estar
libres antes de que alguno de ellos sea adquirido, es posible que un hilo
que espere por un gran número de objetos, no adquiera la propiedad
por un largo período de tiempo si otros hilos están adquiriendo los
mismos objetos de sincronización de a uno. Por ejemplo, en el
diagrama de abajo, el hilo más a la izquierda espera por los mutexes A,
B y C, mientras que otros tres hilos adquieren cada mutex en forma
individual. En el peor de los casos, el hilo esperando por muchos
objetos puede que nunca adquiera la propiedad.
 La segunda desventaja es que aún es posible caer en trampas de
Deadlock, esta vez no con un solo mutex, ¡sino con un grupo de varios
mutexes!
  La tercera desventaja que tiene este enfoque, en común con método
de “time-out” para evitar el Deadlock, es que no es posible usar esta
función si se están usando secciones críticas, la función
EnterCriticalSection no le permite especificar una cantidad de tiempo
de espera, ni tampoco devuelve un código de error.

Atomicidad en la composición de operaciones – optimismo
versus pesimismo en el control de concurrencia.
  Cuando pensamos en el ordenamiento de mutex anterior, nos
encontramos en una situación donde necesitamos desbloquear para
luego volver a bloquear un objeto de modo de respetar el
ordenamiento de mutex. Esto significa que varias operaciones han
ocurrido en un objeto y el bloqueo de ese objeto ha sido liberado entre
medio de dichas operaciones.

Control de concurrencia optimista.
Una manera de lidiar con el problema es asumir que este tipo de
interferencia de hilos es poco probable que ocurra, y simplemente
verificar el problema y devolver un error si esto es así. Esto es
comúnmente un modo válido de lidiar con el problema en situaciones
complejas donde la “sobrecarga” de estructuras de datos por varios
hilos no es demasiado elevada. En el caso presentado antes, podemos
verificar trivialmente esto, guardando una copia local de los datos y
verificando que aún son válidos cuando volvemos a bloquear ambos
objetos en el orden requerido. Aquí esta la rutina modificada.
  Con estructuras de datos más complicadas, uno puede recurrir
algunas veces a ID’s únicos globales o marcado de versiones en piezas
de código. Como nota personal, recuerdo haber trabajado con un
grupo de otros estudiantes en un proyecto de fin de año de la
universidad, donde este enfoque funcionó muy bien: un número
secuencial era incrementado cuando una pieza de datos era modificada
(en este caso los datos consistían en anotaciones en un diario
multiusuario). Los datos eran bloqueados mientras se leía, luego se
mostraban al usuario y si el usuario editaba los datos, el número era
comparado con el obtenido por el usuario en la última lectura, y la
actualización era abandonada si los números no coincidían.

Control de concurrencia pesimista.
   Podemos tomar un enfoque bastante diferente del problema,
considerando que la lista tiende a ser modificada y, por esto, requiere
su propio bloqueo. Todas las operaciones que lean o escriban en la
lista, incluyendo búsquedas, deberán bloquear primero la lista. Esto
provee una solución alternativa al problema de bloquear limpiamente a
varios objetos en la lista. Revisemos las operaciones que deseamos
realizar nuevamente, con el ojo puesto en este diseño alternativo del
bloqueo.
  Un hilo puede querer leer y modificar los contenidos de un objeto de
la lista, pero sin modificar el objeto existente ni su posición en la lista.
Esta operación toma mucho tiempo, y no queremos obstaculizar a
otros hilos que quieran operar con otros objetos, de modo que el hilo
que modifique el objeto debe realizar las siguientes operaciones:
   Bloquear la lista.
   Buscar el objeto en la lista.
   Bloquear el objeto.
   Desbloquear la lista.
   Realizar las operaciones en el objeto.
   Desbloquear el objeto.
  Esto es fantástico ya que, aún si el hilo realiza operaciones de lectura
o escritura en el objeto que tomen mucho tiempo, no tendrá la lista
bloqueada por ese tiempo y, por ende, no demorará a otros hilos que
quieran modificar otros objetos.
  Un hilo puede eliminar un objeto llevando a cabo el siguiente
algoritmo:
    Bloquear la lista.
    Bloquear el objeto.
    Eliminar el objeto de la lista.
    Desbloquear la lista.
    Eliminar el objeto (esto está sujeto a posibles restricciones al borrar
    un mutex que está bloqueado).
   Nótese que es posible desbloquear la lista antes de eliminar
finalmente el objeto, ya que eliminamos el objeto de la lista, y así
sabemos que ninguna otra operación está en progreso en el objeto o la
lista (al tener a ambos bloqueados).
   Aquí viene la parte interesante. Un hilo puede comparar dos objetos
llevando a cabo un algoritmo más simple que el mencionado en la
sección anterior:
   Bloquear la lista.
   Buscar el primer objeto.
   Bloquear el primer objeto.
   Buscar el segundo objeto.
   Bloquear el segundo objeto.
   Desbloquear la lista.
   Realizar la comparación.
Desbloquear los objetos (en cualquier orden).
   Como verán, en la operación de comparación, no he hecho ninguna
restricción en el orden en que son realizados los bloqueos en los
objetos. ¿Podrá esto provocar un Deadlock? El algoritmo presentado
no necesita el criterio para evitar los Deadlocks presentados al
comienzo del capitulo, porque los Deadlock no ocurrirán nunca. Y no
ocurrirán nunca porque cuando un hilo bloquea un objeto mutex, él ya
tiene posesión del mutex de la lista, y con esta posesión, puede
bloquear varios objetos si no libera el mutex de la lista. El bloqueo
compuesto en varios objetos resulta atómico. Como resultado de esto,
podemos modificar el criterio de Deadlock de arriba:
   El Deadlock no ocurrirá ya que para algún mutex arbitrario Mx, los
   hilos sólo intentarán adquirir el mutex Mx si no tienen posesión de
   alguno de los mutex de “mayor prioridad”, esto es M(x+1)… Mn.
   Además, el Deadlock no ocurrirá si los mutex son adquiridos en
   cualquier orden (rompiendo el criterio de arriba), y para cualquier
   grupo de mutex involucrados en una adquisición que no lleva un
   orden, si todas las operaciones de bloqueo en esos mutex son
   atómicas, normalmente mediante el bloqueo de las operaciones
   dentro de una sección crítica (obtenida por el bloqueo de otro
   mutex).

Evitando agujeros en el esquema de bloqueo.
  No es ninguna novedad a estas alturas, que el ejemplo de arriba es
típico de un código de bloqueo que es muy sensible al ordenamiento.
Más allá de esto, todo esto debe indicarnos que cuando ideamos
esquemas de bloqueo que no son triviales, debemos tener mucho
cuidado en el orden en que suceden las cosas.
   Si estás seguro que tu programa funcionará en Windows NT (o 2K,
XP, 2003), entonces el API de Windows provee en efecto una solución
al problema de operaciones compuestas cuando se desbloquean y
vuelven a bloquear objetos. La llamada del
APISignalObjectAndWait te permite marcar atómicamente (o
liberar) un objeto de sincronización, y esperar por otro. Conservando
estas dos operaciones atómicas, se puede transferir un estado de
bloqueo de un objeto a otro, mientras que se asegura que ningún otro
hilo modifica el estado del objeto durante la transferencia. Esto
significa que el control de concurrencia optimista no es necesario en
estas situaciones.

¿Ya esta confundido? ¡Puede tirar la toalla!
  Si pudo permanecer leyendo hasta este punto, lo felicito, ha
adquirido un conocimiento básico del los problemas que le dan a los
programadores multihilo bastante dolores de cabeza. Es útil destacar
que los esquemas complicados en estructuras internas de datos son
habitualmente necesarios para sistemas con alta performance.
Pequeñas aplicaciones de escritorio pueden funcionar habitualmente
con enfoques no tan complicados. Hay varias maneras de “tirar la
toalla”.
   No se preocupe por la eficiencia, y bloquee todo.
   Meta todos los datos en la BDE.
  Bloquear todos los datos compartidos es habitualmente útil, si uno
está dispuesto a sacrificar eficiencia. La mayoría de los usuarios
prefieren un programa que funciona un poco lento que uno que falla
en intervalos impredecibles, por errores en el esquema de bloqueo. Si
uno tiene una gran cantidad de datos que necesitan ser persistentes de
alguna manera, poner todos los datos en la BDE es otro enfoque.
Todos los (medianamente decentes) motores de bases de datos son
seguros para trabajar con múltiples hilos, lo que significa que puedes
acceder a tus datos sin ningún problema desde hilos separados. Si usas
un motor de bases de datos, entonces deberás estudiar algo sobre
administración de transacciones, por ejemplo, las
semánticas reservation, y el uso de premature, commit y rollback,
pero recuerda que esto es sólo el enfoque basado en transacciones para
solucionar problemas de concurrencia, y sencillamente la otra cara de la
misma moneda; la mayor parte de la programación difícil (incluido los
dolores de cabeza) lo han hecho por ti. El uso de la BDE con hilos de
ejecución será tratado luego.
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi
Multithreading a la manera de Delphi

Mais conteúdo relacionado

Mais procurados

Arquitectura de bases de datos distribuidas
Arquitectura de bases de datos distribuidasArquitectura de bases de datos distribuidas
Arquitectura de bases de datos distribuidasJimRocy
 
Segmentación Memoria Virtual
Segmentación Memoria VirtualSegmentación Memoria Virtual
Segmentación Memoria VirtualAna Brooks
 
Paging,Segmentation & Segment with Paging
Paging,Segmentation & Segment with PagingPaging,Segmentation & Segment with Paging
Paging,Segmentation & Segment with PagingMeghaj Mallick
 
Funciones de administracion de memoria
Funciones de administracion de memoriaFunciones de administracion de memoria
Funciones de administracion de memoriaMiguel Magaña
 
Gestion de archivos
Gestion de archivosGestion de archivos
Gestion de archivosJulian Parra
 
El Perceptrón Multicapa
El Perceptrón  MulticapaEl Perceptrón  Multicapa
El Perceptrón MulticapaESCOM
 
Características, componentes y arquitectura de los dbms.
Características, componentes y arquitectura de los dbms.Características, componentes y arquitectura de los dbms.
Características, componentes y arquitectura de los dbms.Julicamargo
 
Grafico Modelo Osi
Grafico Modelo OsiGrafico Modelo Osi
Grafico Modelo OsiAlfonso
 
Cuadro comparativo de los tipos de conexiones a internet
Cuadro comparativo de los tipos de conexiones a internetCuadro comparativo de los tipos de conexiones a internet
Cuadro comparativo de los tipos de conexiones a internetRichard Pérez
 
Query processing and optimization (updated)
Query processing and optimization (updated)Query processing and optimization (updated)
Query processing and optimization (updated)Ravinder Kamboj
 
08 virtual memory
08 virtual memory08 virtual memory
08 virtual memoryKamal Singh
 

Mais procurados (20)

Arquitectura de bases de datos distribuidas
Arquitectura de bases de datos distribuidasArquitectura de bases de datos distribuidas
Arquitectura de bases de datos distribuidas
 
Modelo entidad relacion
Modelo entidad relacionModelo entidad relacion
Modelo entidad relacion
 
Segmentación Memoria Virtual
Segmentación Memoria VirtualSegmentación Memoria Virtual
Segmentación Memoria Virtual
 
SMBD
SMBDSMBD
SMBD
 
Exposicion semaforos
Exposicion semaforosExposicion semaforos
Exposicion semaforos
 
Paging,Segmentation & Segment with Paging
Paging,Segmentation & Segment with PagingPaging,Segmentation & Segment with Paging
Paging,Segmentation & Segment with Paging
 
Modos de transmisión de Datos
Modos de transmisión de DatosModos de transmisión de Datos
Modos de transmisión de Datos
 
Funciones de administracion de memoria
Funciones de administracion de memoriaFunciones de administracion de memoria
Funciones de administracion de memoria
 
Noción de archivo real y virtual
Noción de archivo real y virtual Noción de archivo real y virtual
Noción de archivo real y virtual
 
Gestion de archivos
Gestion de archivosGestion de archivos
Gestion de archivos
 
Ejercicios Modelo E-R
Ejercicios Modelo E-REjercicios Modelo E-R
Ejercicios Modelo E-R
 
El Perceptrón Multicapa
El Perceptrón  MulticapaEl Perceptrón  Multicapa
El Perceptrón Multicapa
 
Características, componentes y arquitectura de los dbms.
Características, componentes y arquitectura de los dbms.Características, componentes y arquitectura de los dbms.
Características, componentes y arquitectura de los dbms.
 
Grafico Modelo Osi
Grafico Modelo OsiGrafico Modelo Osi
Grafico Modelo Osi
 
Cuadro comparativo de los tipos de conexiones a internet
Cuadro comparativo de los tipos de conexiones a internetCuadro comparativo de los tipos de conexiones a internet
Cuadro comparativo de los tipos de conexiones a internet
 
Query processing and optimization (updated)
Query processing and optimization (updated)Query processing and optimization (updated)
Query processing and optimization (updated)
 
Ensayo wi fi
Ensayo wi fiEnsayo wi fi
Ensayo wi fi
 
Dial Up
Dial UpDial Up
Dial Up
 
Alu
AluAlu
Alu
 
08 virtual memory
08 virtual memory08 virtual memory
08 virtual memory
 

Semelhante a Multithreading a la manera de Delphi (20)

Ensayo del profesor erick
Ensayo del profesor erickEnsayo del profesor erick
Ensayo del profesor erick
 
Atix23
Atix23Atix23
Atix23
 
Atix23
Atix23Atix23
Atix23
 
GUIA 1 HILOS Y PROCESOS
GUIA 1 HILOS Y PROCESOSGUIA 1 HILOS Y PROCESOS
GUIA 1 HILOS Y PROCESOS
 
Programacion Basica
Programacion Basica Programacion Basica
Programacion Basica
 
Presentación de programacion
Presentación  de programacionPresentación  de programacion
Presentación de programacion
 
Ccnadiscovery
CcnadiscoveryCcnadiscovery
Ccnadiscovery
 
Plataformas para el desarrollo de aplicaciones web
Plataformas para el desarrollo de aplicaciones webPlataformas para el desarrollo de aplicaciones web
Plataformas para el desarrollo de aplicaciones web
 
Ayala
AyalaAyala
Ayala
 
Ayala
AyalaAyala
Ayala
 
Ayala
AyalaAyala
Ayala
 
Aya
AyaAya
Aya
 
Trabajo de sistema
Trabajo de sistemaTrabajo de sistema
Trabajo de sistema
 
Trabajo de sistema
Trabajo de sistemaTrabajo de sistema
Trabajo de sistema
 
Trabajo de sistema
Trabajo de sistemaTrabajo de sistema
Trabajo de sistema
 
Aya
AyaAya
Aya
 
Aya
AyaAya
Aya
 
Nata
NataNata
Nata
 
trabajo de laboratorio 3
trabajo de laboratorio 3trabajo de laboratorio 3
trabajo de laboratorio 3
 
Nata
NataNata
Nata
 

Multithreading a la manera de Delphi

  • 1. Multithreading - A la manera de Delphi Esta guía fue escrita para quien esté interesado en mejorar la respuesta en sus aplicaciones Delphi mediante el uso de hilos de ejecución (Threads). Cubre aspectos desde los más simples (para el novato) hasta algunos más sofisticados en un nivel intermedio y algunos ejemplos traen aspectos que rozan el nivel avanzado. Se asume que el lector conoce la programación en Object Pascal, incluyendo la programación orientada a objetos y una comprensión del trabajo con eventos de programación. Introducción Capítulo 1. ¿Qué son los hilos de ejecución? ¿Porqué usarlos? Capítulo 2. Crear un hilo de ejecución en Delphi. Capítulo 3. Sincronización básica. Capítulo 4. Destrucción simple de hilos. Capítulo 5. Más sobre destrucciones de hilos. Deadlock. Capítulo 6. Más sincronización: Secciones críticas y mutexes. Capítulo 7. Guía de programación de mutex. Control de concurrencia. Capítulo 8. Clases Delphi seguras para entornos multihilo y prioridades. Capítulo 9. Semáforos. Administración del flujo de datos. La relación productor - consumidor. Capítulo 10. E/S y flujo de datos: del bloqueo a lo asincrónico, ida y vuelta. Capítulo 11. Sicronizadores y Eventos. Capítulo 12. Más dispositivos Win32 para la sincronización. Capítulo 13. Usar hilos conjuntamente con el BDE, las excepciones y las DLLs. Capítulo 14. Un problema del mundo real, y su solución. Introducción Esta guía fue escrita para quien esté interesado en mejorar la respuesta en sus aplicaciones Delphi mediante el uso de hilos de ejecución (Threads). Cubre aspectos desde los más simples (para el
  • 2. novato) hasta algunos más sofisticados en un nivel intermedio y algunos ejemplos traen aspectos que rozan el nivel avanzado. Se asume que el lector conoce la programación en Object Pascal, incluyendo la programación orientada a objetos y una comprensión del trabajo con eventos de programación. Dedicatorias Dedicado a tres miembros del departamento de Ciencias de la Computación de la Universidad de Cambridge: Dr Jean Bacon, Dr Simon Crosby, and Dr Arthur Norman. Muchas gracias a Jean, como tutor, por hacer que algo complicado pareciera sencillo, por proveer excelente material de referencia, por levantar la cortina alrededor de un tema muy misterioso. Además merece agradecimiento como directora de estudios, por explicar la ciencia de la computación a mi propio ritmo. ¡Me tomó tres años darme cuenta por mi mismo! Muchas gracias a Simons como tutor, por mostrarme que apesar de que los modernos sistemas operativos pueden ser endemoniadamente complicados los principios en los que se basan son muy simples. Merece además las gracias por tomar a un estudiantes con ideas no convecionales acerca del proyecto final de la materia, y por proveerme acesoramiento muy útil en mi disertación del proyecto. Arthur Norman nunca me enseño nada acerca de multitarea. Sin embargo me enseñó muchas otras cosas que me ayudaron a escribir las partes más complicadas de esta guía. No hay limites a la excentricidad de los lectores universitarios. A pesar de que la mayoría de la gente prefiere la simplicidad, hay cierto perverso placer en hacer las cosas de la forma complicada, especialmente si eres un cinico. También merece una mención por algunas de las mejores citas nunca leídas por un lector de ciencias de la computación: "Hay algo en los cursos lo cual no debe haber sido evidente hasta ahora, es la realidad..."
  • 3. "Los teóricos han probado que esto no tiene solución, pero nosotros somos tres, y somos listos..." "La gente que no usa computadoras son más sociables, rasonables y menos... retorcidos." "(Si la teoría de la complejidad se sostiene por su título) si eso se prueba ser así, seré el ganador como no muchos de ustedes intentarán las preguntas del examen." Él hasta tiene su propia página de fans. Lecturas recomendadas. Título: Concurrent Systems: An integrated approach to Operating Systems, Database, and Distributed Systems. Autor: Jean Bacon. Editorial : Addison-Wesley ISBN: 0-201-41677-8 El autor acepta sugerencias de otros títulos útiles. Ayuda para la navegación. Los escritos y los diagramas de esta guía están contenidos en paginas HTML simples, una por cada capítulo. Los códigos fuente de ejemplo aparecen en ventanas emergentes. Necesitarás habilitar javascript en tu navegador para verlos. Para facilitar la vista de los escritos y el código fuente en paralelo, el lector encontrará muy útil poner varias ventanas del navegador en mosaico. Esto se puede lograr haciando click derecho en la barra de tareas y seleccionar "Mosaico vertical". Historial de cambios. Versión 1.1 Corrección de ortografía y errores de puntuación en la prosa, y reescritura de explicaciones poco claras. Capítulos 1-9 y 12 modificados.
  • 4. Agregado historial de cambios y otros créditos a la tabla de contenidos. Capítulo 12 renombrado. Agregado el capítulo 14. Créditos. Muchas gracias a las siguientes personas por revisar, sugerir, corregir y mejorar esta guía. Tim Frost Conor Boyd Alan Lloyd Bruce Roberts Bjørge Sæther Craig Stuntz Jim Vaught Créditos de esta traducción Andrés Galluzzi. Diego Romero. Descargar el tutorial completo (340 KB). Capítulo 1. ¿Qué son los hilos de ejecución? ¿Porqué usarlos? En este capítulo: Historia Definiciones Un ejemplo Tiempo compartido ¿Porqué usar hilos de ejecución? Historia En los primeros días de la computación, toda la programación era esencialmente tratada en un solo hilo. Los programas se creaban
  • 5. perforando tarjetas o cintas, con las que formabas tu grupo de tarjetas que enviabas luego al centro local de computación y, tras de un par de días, recibías otro grupo de tarjetas que, si estabas de suerte, contenían los resultados solicitados. Todo el procesamiento era por lotes, de ningún modo crítico, basado en la premisa de que el primero que llegaba era el primero en ser servido y cuando tu programa estaba corriendo, tenía uso exclusivo del tiempo de la computadora. Las cosas han cambiado. El concepto de múltiples hilos de ejecución aparece por primera vez con los sistemas de tiempo compartido, donde más de una persona podía conectarse a una computadora central a la vez. Era importante asegurarse que el tiempo de procesamiento de la máquina era dividido adecuadamente entre todos los usuarios; los sistemas operativos de ese tiempo comienzan a usar los conceptos de “proceso” (process) e “hilos de ejecución” (threads). Las computadoras de escritorio han visto un progreso similar. Los primeros DOS y Windows funcionaban con un único hilo de ejecución. Los programas, o funcionaban en forma exclusiva en la máquina, o no funcionaban. Con la creciente sofisticación de las aplicaciones y la creciente demanda de computadoras personales, especialmente en lo relativo a la performance gráfica y el trabajo en red, los sistemas operativos multiproceso y multihilo se volvieron algo común. Las aplicaciones multihilo en las PC’s fueron principalmente conducidas por la búsqueda de una mejor performance y usabilidad. Definiciones El primer concepto a definir es el del proceso. La mayoría de los usuarios de Windows 95, 98 y NT intuyen bastante bien lo que es un proceso. Lo ven como un programa que corre en la computadora, co- existiendo y compartiendo el microprocesador, la memoria y otros recursos con otros programas. Los programadores saben que un proceso es invocado por un código ejecutable, como también saben que ese código tiene una única existencia y que las instrucciones ejecutadas por ese proceso son procesadas de una manera ordenada. En suma, los procesos se ejecutan en forma aislada. Los recursos que usan (memoria, disco, E/S, tiempo del microprocesador) son
  • 6. virtualizados, de modo que todos los procesos tienen su propio grupo de recursos virtuales que son exclusivos de ese proceso. El sistema operativo provee esta virtualización. Los procesos ejecutan módulos de código. Estos pueden ser independientes, en el sentido de que, los módulos ejecutables de código que competen al Windows Explorer son independientes de los del Microsoft Word. Sin embargo, éstos también pueden ser compartidos, como es el caso de las DLL’s. El código de una DLL típicamente es ejecutado en el contexto de muchos procesos diferentes, y habitualmente en forma simultánea. La ejecución de instrucciones no es totalmente ordenada por los procesos: Microsoft Word no deja de abrir un documento sencillamente porque la cola de impresión está enviando algo a la impresora! Por supuesto, cuando diferentes procesos interactúan entre sí, el programador debe establecer un orden, un problema central que será tratado luego. Nuestro próximo concepto es el del hilo de ejecución (Thread). Los hilos de ejecución fueron desarrollados cuando se vio claramente el deseo de tener aplicaciones que realizaran varias acciones con mayor libertad en cuanto al orden, posiblemente, realizando varias acciones en el mismo momento. En situaciones donde algunas acciones pudieran causar una demora considerable a un hilo de ejecución (por ejemplo, cuando se espera que el usuario haga algo), era más deseable que el programa siguiera funcionando, ejecutando otras acciones concurrentemente (por ejemplo, verificación ortográfica en segundo plano, o procesamiento de los mensajes que arriban desde la red). Sin embargo, crear todo un nuevo proceso para cada acción concurrente y luego hacer que ese proceso se comunicara con el primero era generalmente una sobrecarga demasiado grande. Un ejemplo Si se necesita ver un buen ejemplo de programación multihilo, entonces el Windows Explorer (aka Windows Shell) es un ejemplo excelente. Haz doble clic en “Mi PC” y abre varias subcarpetas abriendo nuevas ventanas a medida que lo haces. Ahora, realiza una larga operación de copia en una de esas ventanas. La barra de progreso
  • 7. aparece y esa ventana en particular deja de responder al usuario. Sin embargo, todas las demás ventanas son perfectamente usables. Obviamente, varias cosas se están haciendo en el mismo momento, pero sólo una copia de explorer.exe está corriendo. Esa es la esencia de la programación multihilo. Tiempo compartido. En la mayoría de los sistemas que soportan varios hilos de ejecución, puede haber muchos usuarios haciendo llamadas simultáneas al sistema. Para responder a todas estas demandas, se suele necesitar una cantidad de hilos de ejecución que suele ser superior al número de procesadores que existen físicamente en el sistema. Esto es posible gracias a que la mayoría de los sistemas permiten compartir el tiempo del procesador, y así solucionar este problema. En un sistema con tiempo compartido, los hilos de ejecución corren por un corto espacio y luego son invalidados; es decir, un temporizador en el hardware de la máquina se dispara, lo que causa que el sistema operativo re-evalúe qué hilos de ejecución deben correr, pudiendo detener la ejecución de los hilos en funcionamiento y continuando la ejecución de otros hilos que habían quedado detenidos. Esto permite que las máquinas, aún con un solo procesador, puedan correr muchos hilos de ejecución. En las PC’s, los tiempos compartidos tienden a ser de alrededor de 55 milisegundos. ¿Porqué usar hilos de ejecución? Los hilos de ejecución no deben alterar la semántica de un programa. Ellos cambian simplemente los tiempos de operación. Como resultado, son casi siempre usados como una solución elegante a problemas de performance. Aquí hay algunos ejemplos de situaciones donde puedes usar hilos de ejecución: Realizar largos procesamientos: Cuando una aplicación de Windows está realizando cálculos, no puede procesar ningún mensaje. Como resultado, la pantalla no puede ser actualizada.
  • 8. Realizar procesamientos en segundo plano: Algunas tareas pueden no ser críticas, pero necesitan ser ejecutadas continuamente. Realizar tareas de E/S: E/S a disco o red puede tener demoras imposibles de prever. Los hilos de ejecución permiten asegurar que la demora de E/S no demora otras partes no relacionadas con esto en tu aplicación. Todos estos ejemplos tienen una cosa en común: En el programa, algunas operaciones incurren en una potencial demora o sobrecarga del microprocesador, pero esta demora es inaceptable para otras operaciones; ellas necesitan estar disponibles ya. Por supuesto, hay otros beneficios y estos son: Hacer uso de sistemas multiprocesador: No puedes esperar que una aplicación con sólo un hilo de ejecución haga uso de dos o más procesadores. El capítulo 3 explica esto con más detalles. Compartir el tiempo con eficiencia: Usar hilos de ejecución y prioridades en los procesos asegura una correcta justa del tiempo del microprocesador. El uso adecuado de los hilos de ejecución convierte a lentas, duras y no muy disponibles aplicaciones en unas que tienen una brillante respuesta, eficiencia y velocidad, además de que puede simplificar radicalmente varios problemas de performance y usabilidad. Capítulo 2. Crear un hilo de ejecución en Delphi. En este capítulo: Un diagrama de intervalos. Nuestro primer hilo no-VCL. ¿Qué hace exactamente este programa? Cuestiones, problemas y sorpresas. Cuestiones en la inicialización. Cuestiones en la comunicación. Cuestiones de terminación. Un diagrama de intervalos.
  • 9. Antes de meterse en los detalles de crear hilos de ejecución, y ejecutar código independiente del hilo principal de la aplicación, es necesario introducir un nuevo tipo de diagrama ilustrativo de la dinámica de la ejecución de hilos. Esto nos ayudará cuando comencemos a diseñar y crear programas multihilo. Considera esta simple aplicación. La aplicación tiene un hilo de ejecución: el hilo principal de la VCL. El progreso de este hilo puede ser ilustrado con un diagrama que muestra el estado del hilo en la aplicación a través del tiempo. El progreso de este hilo está representado por una línea, y el tiempo fluye en forma descendente en la página. Incluí una referencia en este diagrama que se aplica a todos los subsecuentes diagramas de hilos de ejecución. Nótese que este diagrama no indica mucho acerca de los algoritmos que se ejecutan. En cambio, ilustra el orden de los eventos a través del tiempo y el estado de los hilos de ejecución entre ese tiempo. La distancia entre diferentes puntos del diagrama no es importante, pero sí
  • 10. el ordenamiento vertical de esos puntos. Hay mucha información que se puede extraer de este diagrama. El hilo en esta aplicación no se ejecuta continuamente. Puede haber largos períodos de tiempo durante los cuales no recibe estímulos externos y no está llevando ningún cálculo ni ningún otro tipo de operación. La memoria y los recursos ocupados por la aplicación existen y la ventana está aún en la pantalla, pero ningún código está siendo ejecutado por el microprocesador. La aplicación es inicializada y el hilo principal es ejecutado. Una vez que se crea la ventana principal, no tiene más trabajo que hacer y se reposa sobre una pieza de código VCL conocida como el bucle de mensajes de la aplicación que espera más mensajes del sistema operativo. Si no hay más mensajes para ser procesados, el sistema operativo suspende el hilo y el hilo de ejecución está ahora suspendido. En un momento posterior, el usuario hace clic en el botón, para mostrar el mensaje de texto. El sistema operativo despierta (o reanuda) el hilo principal, y le entrega un mensaje indicando que un botón ha sido presionado. El hilo principal está ahora activo nuevamente. Este proceso de suspensión – reanudación ocurre varias veces en el tiempo. Ilustré una espera de confirmación del usuario para cerrar la caja de mensajes y espera que el botón de cerrar sea presionado. En la práctica, muchos otros mensajes pueden ser recibidos. Nuestro primer hilo no-VCL A pesar de que el API Win32 provee un extenso soporte multihilo, al momento de crear y destruir hilos de ejecución, el VCL tiene una clase muy útil, TThread, que abstrae la mayoría de las técnicas para crear un hilo, provee una simplificación muy útil, e intenta evitar que el programador caiga en una de las muchas trampas indeseables que esta nueva disciplina provee. Yo recomiendo su uso. La ayuda de Delphi provee una guía razonable para crear diferentes tipos de hilos, de modo que no voy a mencionar mucho sobre las secuencias de menú
  • 11. necesarias para crear un hilo de ejecución independiente mas allá de sugerir que el lector seleccione File | New… y luego elija Thread Object. Este ejemplo en particular consiste en un programa que calcula si un número en particular es un número primo o no. Contiene dos units, una con un formulario convencional, y otra con un objeto hilo. Más o menos funciona; de hecho tiene algunos rasgos indeseables que ilustran algunos de los problemas básicos que los programadores multihilo deben considerar. Discutiremos el modo de evitar estos problemas más tarde. Aquí está el código fuente del formulario y aquí está el código fuente del objeto hilo. ¿Qué hace exactamente este programa? Cada vez que el botón “Spawn” es presionado, el programa crea un nuevo objeto hilo, inicializa algunos campos en el objeto y luego hace andar al hilo. Tomando el número ingresado, el hilo se aparta calculando si el número es primo y una vez que ha terminado el cálculo, muestra una caja de mensajes indicando si el número es primo. Estos hilos son concurrentes, mas allá de que se tenga una máquina uniprocesador o multiprocesador; desde el punto de vista del usuario, estos se ejecutan en forma simultánea. Además, este programa no limita el número de hilos creados. Como resultado, se puede demostrar que hay una concurrencia real de la siguiente manera: Como he comentado un comando de salida en la rutina que determina si el número es primo, el tiempo que corre el hilo es directamente proporcional al tamaño del número ingresado. He notado que con un valor de aproximadamente 224, el hilo necesita entre 10 y 20 segundos en completarse. Encuentra un valor que produzca una demora similar en tu máquina. Ejecuta el programa, introduce un número grande y haz clic en el botón. Inmediatamente introduce un número pequeño (digamos, 42) y haz clic en el botón nuevamente. Notarás que el resultado para el número pequeño se produce antes que el resultado para el número
  • 12. grande, aún cuando comenzamos el hilo con el número grande primero. El diagrama de abajo ilustra la situación. Cuestiones, problemas y sorpresas. Hasta este punto, el tema de la sincronización se ve espinoso. Una vez que el hilo principal llamó a Resume en un hilo “funcionando”, el programa principal no puede asumir absolutamente nada sobre el estado del hilo en funcionamiento y viceversa. Es completamente posible que el hilo en funcionamiento complete su ejecución antes de que el progreso del hilo principal de VCL termine. De hecho, para números pequeños que toman menos de una veinteava de segundo en calcularse, es absolutamente probable. De forma similar, el hilo en funcionamiento no puede asumir nada acerca del estado de progreso del hilo principal. Todo está a merced del administrador de tareas de Win32. Hay tres “factores de gracia” que uno encuentra aquí: cuestiones de Iniciación, cuestiones de Comunicación y cuestiones de Terminación.
  • 13. Cuestiones de iniciación. Delphi hace que lidiar con las cuestiones de iniciación de hilos de ejecución sea cosa fácil. Antes de hacer correr un hilo, uno suele desear establecer algunos estados en el hilo. Creando un hilo suspendido (un argumento soportado por el constructor), uno puede estar seguro de que el código no es ejecutado hasta que el hilo es reanudado (Resume). Esto significa que el hilo principal de VCL puede leer y modificar datos en el objeto del hilo de una forma segura, y con la garantía de que serán actualizados y validados en el momento en que el hilo hijo comienza a ejecutarse. En el caso de este programa, las propiedades del hilo “FreeOnTerminate” (liberarse cuando termine) y “TestNumber” (la variable), son establecidas antes de que el hilo comience a ejecutarse. Si este no fuera el caso, el funcionamiento del hilo quedaría indefinido. Si no deseas crear el hilo suspendido, entonces estarás pasándole el problema de la inicialización a la siguiente categoría: cuestiones de comunicación. Cuestiones de comunicación. Esto ocurre cuando tienes dos hilos que están ambos corriendo y necesitas comunicarte entre ellos de algún modo. Este programa evade el problema simplemente no teniendo nada que comunicar entre los hilos separados. De más esta decir que si no proteges todas tus operaciones en datos compartidos (en el más estricto sentido de “protección”), tu programa no será confiable. Si no tienes una adecuada sincronización o un sólido control de concurrencia, lo siguiente será imposible: Acceder a cualquier tipo de datos compartidos entre dos hilos. Interactuar con partes inseguras del VCL desde un hilo no-VCL. Intentar relegar operaciones relacionadas con gráficas en hilos independientes. Aún haciendo las cosas tan simples como tener dos hilos accediendo a una variable de tipo integer compartida puede resultar en un completo desastre. Accesos no sincronizados a recursos compartidos o
  • 14. llamadas de VCL resultarán en muchas horas de tensos debugueo, considerable confusión y eventuales internaciones en el hospital mental más cercano. Hasta que aprendas la técnica apropiada para hacer esto en los capítulos siguientes, no lo hagas. ¿La buena noticia? Puedes hacer todo lo de arriba si usas el mecanismo correcto para controlar la concurrencia, ¡y ni siquiera es difícil! Veremos un modo sencillo de resolver aspectos de comunicación a través de la VCL en el próximo capitulo, y más elegantes (y complicados) métodos luego. Cuestiones de terminación. Los hilos de ejecución, al igual que otros objetos de Delphi, involucran la asignación de memoria y recursos. No debería sorprender saber la importancia de que el hilo termine adecuadamente, algo que el programa de este ejemplo hace mal. Hay dos enfoques posibles para el problema de la liberación del hilo. El primero es dejar que el hilo maneje el problema por sí mismo. Esto es principalmente usado para hilos que, o comunica los resultados de la ejecución del hilo al hilo principal de la VCL antes de terminar o no poseen ninguna información que resulte útil para otros hilos al momento de terminar. En estos casos, el programador puede activar la variable “FreeOnTerminate” en el objeto hilo, y se liberará cuando termine. La segunda es que el hilo principal de VCL lea datos del hilo en funcionamiento cuando este haya terminado, y luego liberar el hilo. Esto es tratado en el capítulo 4. He hecho a un lado el tema de comunicar los resultados de vuelta al hilo principal al hacer que el hilo hijo presenta la respuesta al usuario mediante una llamada a “ShowMessage”. Esto no involucra ningún tipo de comunicación con el hilo principal de VCL y el llamado a ShowMessage es seguro entre hilos, de modo que el VCL no tiene problemas. Como resultado de esto, puedo usar el primer enfoque de liberación del hilo, dejando que el hilo se libere a sí mismo. A pesar de
  • 15. esto, el programa de ejemplo ilustra una característica indeseable al hacer que los hilos se liberen a sí mismos: Como podrá notar, hay dos cosas que pueden suceder. La primera es que intentemos salir del programa, mientras el hilo continua activo y calculando. La segunda es que intentemos salir del programa mientras éste esta suspendido. El primer caso es bastante malo: la aplicación termina sin siquiera asegurarse de que no haya hilos funcionando. El código de liberación de Delphi y Windows hace que la aplicación termine bien. Lo segundo que podría pasar no es tan bellamente manejable, ya que el hilo está suspendido en algún lugar dentro de las entrañas del sistema de mensajería de Win32. Cuando la aplicación termina, parece que Delphi hace una buena liberación en ambas circunstancias. Sin embargo, no es un buen estilo de programación hacer que el hilo sea forzado a finalizar sin ninguna referencia de lo que está haciendo en el momento, de modo que un archivo pueda quedar corrompido. Esta es la razón por la que es una buena idea tener una buena coordinación de la salida del hilo hijo desde el hilo principal de
  • 16. la VCL, aún cuando no haga falta transferir ningún dato entre los hilos: una salida limpia del hilo y el proceso es posible. En el capitulo 4 se discuten algunas soluciones a este problema. Capítulo 3. Sincronización básica. En este capitulo: ¿Qué datos son compartidos entre los hilos? Atomicidad cuando se accede a datos compartidos. Problemas adicionales con la VLC. Diversión con máquinas multiprocesador. La solución Delphi: TThread.Synchronize. ¿Cómo funciona esto? ¿Qué hace Synchronize? Sincronizado a hilos no-VCL. ¿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.
  • 17. 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
  • 18. 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. Valor de la Instrucciones ejecutadas Instrucciones variable A en por el hilo X ejecutadas por el hilo Y memoria <otras instrucciones> Hilo suspendido 1 Lee A desde la memoria en Hilo suspendido 1 un registro del procesador. Incrementa en 1 el registro Hilo suspendido 1 del procesador. Escribe los contenidos del registro del procesador en Hilo suspendido 2 A (2) en memoria. <otras instrucciones> Hilo suspendido 2 CAMBIO DE HILO CAMBIO DE HILO 2 Hilo suspendido <otras instrucciones> 2 Lee A desde la memoria Hilo suspendido en un registro del 2 procesador. Incrementa en 1 el registro Hilo suspendido 2 del procesador. Escribe el contenido del Hilo suspendido registro del procesador en 3 A (3) en memoria. Hilo suspendido <otras instrucciones> 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:
  • 19. Valor de la Instrucciones ejecutadas Instrucciones variable A en por el hilo X ejecutadas por el hilo Y memoria <otras instrucciones> Hilo suspendido 1 Lee A desde la memoria en Hilo suspendido 1 un registro del procesador. Incrementa en 1 el registro Hilo suspendido 1 del procesador. CAMBIO DE HILO CAMBIO DE HILO 1 Hilo suspendido <otras instrucciones> 1 Lee A desde la memoria Hilo suspendido en un registro del 1 procesador. Incrementa en 1 el registro Hilo suspendido 1 del procesador. Escribe el contenido del Hilo suspendido registro del procesador en 1 A (2) en memoria. CAMBIO DE HILO CAMBIO DE HILO 2 Escribe los contenidos del registro del procesador en Hilo suspendido 2 A (2) en memoria. <otras instrucciones> 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.
  • 20. 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.
  • 21. 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
  • 22. 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
  • 23. 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. Capítulo 4. Destrucción simple de hilos. En este capítulo Consideraciones de completado, terminación y destrucción de hilos. Terminado prematuro de hilos. El evento OnTerminate.
  • 24. Terminación controlada de hilos – Efoque 1. Consideraciones de completado, terminación y destrucción de hilos. En el capitulo 2 se dio un lineamiento de algunos de los problemas relacionado con la finalización de hilos. Hay dos consideraciones principales: Salir del hilo limpiamente y limpiar todos los recursos asignados. Obtener los resultados del hilo cuando éste haya terminado. Estos puntos están fuertemente relacionados. Si un hilo no tiene que comunicar nada al hilo principal de la VCL cuando haya terminado, o si uno usa la técnica descripta en el capítulo anterior para comunicar los resultados justo antes de que el hilo termine, entonces no hay necesidad del hilo principal de VCL de participar en ninguna limpieza del hilo. En este caso, uno puede establecer a verdadero la variable FreeOnTerminate del hilo, y dejar que el hilo se encargue de liberarse a sí mismo. Recuerda que si uno hace esto, el usuario puede forzar la salida del programa, resultando en una terminación de todos los hilos en él, con posibles consecuencias indeseables. Si el hilo sólo escribe en la memoria, o se comunica con otras partes de la aplicación, entonces este no es un problema, pero si escribe en un archivo o en un recurso compartido del sistema, entonces esto es inaceptable. Si un hilo tiene que intercambiar información con la VCL antes de terminar, entonces un mecanismo tiene que ser encontrado para sincronizar el hilo principal de VCL con el hilo en funcionamiento, y el hilo principal de VCL debe realizar la limpieza (tu tienes que escribir el código para liberar el hilo). Dos mecanismos serán presentados luego. Hay un punto más para tener en cuenta: Terminar un hilo antes de que su curso de ejecución haya concluido. Esto puede suceder bastante seguido. Algunos hilos, especialmente aquellos que procesan E/S, se ejecutan en un bucle permanente: el programa puede estar recibiendo siempre más datos, y el hilo siempre
  • 25. tiene que estar preparado para procesarlos hasta que el programa termine. Entonces, si organizamos estos puntos en orden inverso… Terminado prematuro de hilos. En algunas circunstancias, un hilo puede necesitar indicarle a otro hilo que debe terminar. Esto generalmente ocurre si el hilo está ejecutando una operación muy larga, y el usuario decide salir de la aplicación, o la operación debe ser abortada. TThread provee un mecanismo simple para soportar esto en la forma del método Terminate, y la propiedad Terminated. Cuando un hilo es creado su propiedad terminated se establece a false. Cuando se llama al método terminate de un hilo, la propiedad terminated para ese hilo es ahora true. Es la responsabilidad de todos los hilos de verificar periódicamente si se les ha solicitado terminar, y si así fuera, salir limpiamente. Nótese que no se producen sincronizaciones de gran escala en este proceso; cuando un hilo activa la propiedad terminated del otro, no puede asumir que el otro hilo ha leído el valor de la propiedad terminated y comenzó su finalización. La propiedad Terminated es simplemente una señal, diciendo “por favor termina tan rápido como sea posible”. El diagrama de abajo ilustra esta situación.
  • 26. Cuando se diseñan los objetos hilos, se deberá considerar leer la variable terminated cuando sea necesario. Si el hilo se bloquea, como resultado de algún mecanismo de sincronización de los que discutiremos luego, podría tener que sobrecargar el método terminate para desbloquear el hilo. En particular, recodará llamar primero al método heredado (inherited) terminate, antes de desbloquear el hilo, si espera que su próxima verificación de terminated devuelva verdadero. Pronto veremos más de esto. Como ejemplo, aquí hay una pequeña modificación al hilo que calcula los números primos del capitulo anterior, para asegurarnos de que verifica el valor de terminated. He asumido que es aceptable para el hilo devolver un resultado incorrecto cuando se establece la propiedad terminated. El evento OnTerminate. El evento OnTerminate ocurre cuando un hilo realmente ha terminado su ejecución. No ocurre cuando es llamado el método terminate. Este evento es bastante útil, en el sentido de que se ejecuta en el contexto del hilo principal de VCL, de la misma forma en que lo hacen los métodos pasados a synchronize. Además, si uno desea ejecutar algunas operaciones de la VCL con un hilo que se libera
  • 27. automáticamente a sí mismo, entonces este es el lugar de hacerlo. La mayoría de los nuevos programadores de hilos de ejecución van a encontrar esto como la mejor manera de lograr que un hilo no-VCL transfiera sus datos de vuelta al VCL, con un mínimo de alboroto, y sin requerir llamadas explícitas a synchronize. Como pueden ver en el diagrama de arriba, OnTerminate trabaja bastante parecido a como lo hace Synchronize, y es prácticamente idéntico semánticamente a poner una llamada a Synchronize al final del hilo. El principal uso de esto es que, mediante el uso de indicadores, como “La aplicación puede finalizar” o conteos de referencias de los hilos que hay en funcionamiento en el hilo principal de VCL, un mecanismo simple puede ser provisto para asegurarse de que el hilo principal de VCL puede salir sólo cuando todos los demás hilos han terminado. Aquí hay algunos detalles de sincronización involucrados, especialmente si un programador va a poner una llamada a Application.Terminate en el evento OnTerminate de un hilo, pero todo esto será tratado más tarde.
  • 28. Terminación controlada de hilos – Efoque 1. En este ejemplo, tomaremos el código del programa de números primos del capítulo 3 y lo modificaremos de modo que el usuario no pueda cerrar la aplicación cuando hay otros hilos ejecutándose. Esto se vuelve simple. De hecho, no necesitamos modificar el código del hilo ni en lo más mínimo. Nosotros simplemente agregaremos una referencia a un campo de conteo en el hilo principal, incrementándolo cuando se cree un nuevo hilo, estableciendo el evento OnTerminate del hilo para que apunte a un manejador en el formulario principal que decremente el conteo de referencia, y cuando el usuario solicite terminar la aplicación, mostraremos una caja de diálogo de alerta si fuera necesario. El ejemplo muestra lo simple de este enfoque: todo el código concerniente con tomar cuenta de los números de hilos en ejecución sucede en el hilo principal de VCL, y el código es esencialmente disparado por un evento, lo mismo que como sería con cualquier otra aplicación Delphi. En el próximo capitulo, vamos a considerar un enfoque sensiblemente más complicado, que es beneficioso cuando se usan mecanismos de sincronización más avanzados. Capítulo 5. Más sobre destrucciones de hilos. Deadlock. En este capitulo: El método WaitFor. Terminación controlada de hilos – Enfoque 2. Una rápida introducción al pasaje de mensajes y notificaciones. WaitFor puede resultar en largas demoras. ¿Haz notado el bug? Evitando esta particular manifestación de Deadlock. El método WaitFor.
  • 29. El evento OnTerminate, discutido en el capítulo anterior, es muy útil si estás usando hilos que inicializas y luego los olvidas, con destrucción automática. ¿Que pasa si, en cierto punto de la ejecución del hilo principal de la VCL, quieres asegurarte de que todos los demás hilos hayan terminado? La solución a esto es el método WaitFor. Este método es útil si: El hilo principal de VCL necesita acceder al objeto hilo en funcionamiento antes de que su ejecución haya terminado, y ya no se pueda leer o modificar datos en el hilo. Forzar la terminación de un hilo cuando se termina el programa no es una opción viable. Bastante sencillo. Cuando el hilo A llama al método WaitFor del hilo B, el hilo A queda suspendido hasta que el hilo B termina su ejecución. Cuando el hilo A se vuelve a activar, puede estar seguro que los resultados del hilo B se pueden leer, y que el objeto hilo representado por B puede ser destruido. Típicamente esto ocurre cuando el programa termina, donde el hilo principal de VCL llamará el método Terminate en todos los hilos no-VCL y luego al método WaitFor en todos los hilos no-VCL antes de salir. Terminación controlada de hilos – Enfoque 2. En este ejemplo, modificaremos el código del programa de números primos de modo que sólo un hilo se ejecute por vez, y el programa espere hasta que el hilo complete su ejecución antes de salir. A pesar de que en este programa no es estrictamente necesario esperar a que los hilos terminen, es un ejercicio útil y demuestra algunas propiedades de WaitFor que no son siempre deseables. Tambien ilustra algunos claros bugs con los que se pueden topar programadores principiantes. Primero que nada, el código del formulario principal. Como puede ver, hay varias diferencias con el ejemplo anterior: Tenemos un “número mágico” declarado al inicio del unit. Este es un número arbitrario de mensaje, y su valor no es importante; es el único mensaje en la aplicación con este número.
  • 30. En vez de tener un conteo de hilos, mantenemos una referencia explícita a un hilo y sólo un hilo, apuntado por la variable FThread del formulario principal. Sólo queremos que un hilo se ejecute por vez, ya que sólo tenemos una única variable apuntando al hilo que realizará el trabajo. Por este motivo, el código de creación del hilo verifica si hay hilos ejecutándose, antes de crear otros. El código de creación del hilo no establece la propiedad FreeOnTerminate a verdadero. En cambio, el hilo principal de VCL liberará el hilo en funcionamiento más tarde. El hilo principal tiene un manejador de mensajes definido que espera que el hilo en ejecución se complete y entonces lo libera. De igual modo, el código ejecutado cuando el usuario desea liberar el formulario espera que el hilo en ejecución se complete y lo libera. Habiendo notado estos puntos, aquí esta el hilo que hará el trabajo. Nuevamente, hay algunas diferencias con el código presentado en el capitulo 3. La función IsPrime verifica ahora si se solicitó que el hilo termine, resultando en una rápida salida si la propiedad terminated es establecida. La función Execute verifica si se produjo una terminación anormal. Si la terminación fue normal, entonces usa synchronize para mostrar los resultados, y envía un mensaje al formulario principal solicitando que el formulario principal lo libere. Una rápida introducción al pasaje de mensajes y notificaciones. Bajo circunstancias normales, el hilo es ejecutado, corre por su curso, usa synchronize para mostrar los resultados y luego envía un mensaje al formulario principal. Este envío de mensaje es asincrónico: el formulario principal toma el mensaje en algún punto en el futuro. PostMessage no suspende el trabajo del hilo en ejecución, lo hace correr hasta que se complete. Esta es una propiedad muy útil: no podemos usar synchronize para decirle al formulario principal que libere al hilo, porque volveremos de la llamada a Synchronize a un hilo que no existe más. En cambio, esto simplemente actúa como una
  • 31. notificación, un gentil recordatorio para el formulario principal de que debe liberar el hilo tan rápido como le sea posible. En un momento posterior, el hilo del programa principal recibe el mensaje y ejecuta al manejador. Este manejador verifica si el hilo aún existe y, si existe, espera a que se complete su ejecución. Este paso es necesario porque si bien es sabido que el hilo en ejecución está terminando (no hay muchas sentencias más luego del PostMessage), esto no es una garantía. Una vez que la espera haya terminado, el hilo principal puede liberar el hilo que hizo el trabajo. El diagrama de abajo ilustra este primer caso. Para mantenerlo simple, fueron omitidos los detalles de la operación de Synchronize del diagrama. Además, la llamada a PostMessage se muestra como que ocurre en algún momento antes de que el hilo completa su funcionamiento de modo de ilustrar el funcionamiento de la operación WaitFor.
  • 32. En capítulos posteriores se va a cubrir la ventaja de enviar mensajes con mayor detalle. Es suficiente decir hasta este punto que esta técnica es muy útil cuando se trata de comunicarse con el hilo VCL. En un caso anormal de funcionamiento, el usuario intentará salir de la aplicación, y confirmará que desea salir inmediatamente. El hilo principal establecerá la propiedad terminated del hilo en proceso, lo que se espera que provoque una terminación en un tiempo razonablemente corto, y luego aguardará para que este se complete. Una vez que se ha completado el procesamiento del hilo, el proceso de liberación es como el caso anterior. El diagrama de abajo ilustra el nuevo caso. Muchos lectores estarán perfectamente felices a estas alturas. Sin embargo, los problemas vuelven a aparecer, y como es común cuando consideramos la sincronización multihilo, el diablo está en los detalles. WaitFor puede resultar en largas demoras. El beneficio de WaitFor es también su mayor desventaja: suspende el hilo principal en un estado en el que no puede recibir mensajes. Esto significa que la aplicación no puede realizar ninguna de las operaciones
  • 33. normalmente asociadas con el procesamiento de mensajes: la aplicación no re-dibujará, no se re-dimensionará ni responderá a ningún estímulo externo cuando está esperando. Tan pronto como el usuario lo note, pensará que la aplicación se colgó. Esto no es un problema en el caso de un hilo que termina normalmente; llamando a PostMessage, la última operación en el hilo en funcionamiento, nos aseguramos de que el hilo principal no tendrá que esperar mucho. Sin embargo, en el caso de una terminación anormal del hilo, la cantidad de tiempo que el hilo principal pierde en este estado depende de que tan frecuentemente verifique el hilo de ejecución la propiedad terminate. El código fuente para PrimeThread tiene una línea marcada “Line A”. Si se le quita el fragmento “and not terminated”, podrá experimentar que sucede al finalizar la aplicación durante la ejecución de un cálculo que dure mucho tiempo. Hay algunos métodos avanzados para suprimir este problema que involucra a las funciones Win32 de espera de mensajes, una explicación de este método se puede encontrar visitando http://www.midnightbeach.com/jon/pubs/MsgWaits/Msg Waits.html. En suma, es simple escribir hilos que verifican la propiedad Terminated con cierta regularidad. Si esto no es posible, entonces es preferible mostrarle algunas advertencias al usuario acerca de la potencial irresponsabilidad de la aplicación (a la Microsoft Exchange). ¿Haz notado el bug? WaitFor y Synchronize: una introducción a Deadlock. La demora de WaitFor es realmente un problema menor, cuando se lo compara con otros vicios que tiene. En aplicaciones que usan Synchronize y WaitFor, es completamente posible hacer que la aplicación caiga en un Deadlock. Deadlock es un fenómeno donde no hay problemas de algoritmos en la aplicación, pero toda la aplicación se detiene, muerta en el agua. El caso general es que Deadlock ocurra cuando un hilo espera por el otro en forma cíclica. El hilo A esta esperando por el hilo B para completar algunas operaciones, mientras que el hilo C espera por el hilo D, etc. etc. Al final de la línea, el hilo D estará esperando por el hilo A para completar algunas operaciones.
  • 34. Desgraciadamente el hilo A no puede completar la operación porque está suspendido. Esto es el equivalente en computación del problema: “A: Tu vas primero… B: No, tu… A: No, ¡insisto!” que acosa a los motoristas cuando el derecho de paso no está claro. Este tipo de funcionamiento está documentado en los archivos de ayuda de la VCL. En este caso en particular, el Deadlock puede ocurrir entre dos hilos de ejecución si el hilo de cálculo llama a Synchronize poco tiempo antes de que el hilo principal llame a WaitFor. Si esto sucediera, entonces el hilo de cálculo estará esperando que el hilo principal se libere para regresar al bucle de mensajes, mientras que el hilo principal está esperando que el hilo de cálculo se complete. Deadlock ocurrirá. También es posible que el hilo principal de VCL llame a WaitFor poco tiempo antes de que el hilo de cálculo llame a Synchronize. Dando una implementación simplista, esto también resultaría en un Deadlock. Por suerte, los que hicieron la VCL trataron de sortear este caso de error, lo que resulta en el surgimiento de una excepción en el hilo de cálculo, rompiendo el Deadlock y finalizando el hilo. La programación del ejemplo, como está, se vuelve bastante indeseable. El hilo de cálculo llama a Synchronize si verifica que Terminated está es falso poco antes de terminar su ejecución. El hilo principal de la aplicación establece terminated poco antes de llamar a
  • 35. WaitFor. De modo que, para que ocurra un Deadlock, el hilo de cálculo deberá encontrar Terminated en falso, ejecutar Synchronize, y luego el control debe ser transferido al hilo principal exactamente en el punto donde el usuario ha confirmado forzar la salida. Más allá del hecho de que estos casos de Deadlock son indeseables, eventos de este tipo son claras condiciones de carrera. Todo depende del momento exacto de los eventos, lo que variará de funcionamiento en funcionamiento en la máquina. El 99.9% de las veces, un cierre forzado funcionará, y una en mil veces, todo se bloqueará: exactamente el tipo de problema que necesitamos evitar a toda costa. El lector recordará que anteriormente le mencioné que ninguna sincronización de gran escala ocurrirá cuando se está leyendo o escribiendo la propiedad terminated. Esto quiere decir que no es posible usar la propiedad terminated para evitar este problema, como el diagrama anterior lo deja en claro. Algún lector interesado en duplicar el problema del Deadlock, puede hacer relativamente fácil, modificando los siguientes fragmentos del código fuente: Quite el texto “and not terminated” a la altura de “Line A” Remplace el texto “not terminated” a la altura de “Line B” por “true” Quite el comentario en “Line C” El deadlock puede ser entonces provocado corriendo un hilo cuya ejecución demore cerca de 20 segundos, y forzar la salida de la aplicación poco tiempo después de que el hilo fue creado. El lector puede desear también ajustar el tiempo que el hilo principal de la aplicación se suspende, de modo de saber el “correcto” ordenamiento de los eventos: El usuario comienza cualquier hilo de cálculo. El usuario intenta salir y dice: “Sí, quiero salir más allá de que haya un hilo en funcionamiento”. El hilo principal de la aplicación se suspende (Line C) El hilo de cálculo eventualmente llega al final de la ejecución y llama a Synchronize. (asistido por las modificaciones en las líneas A y B).
  • 36. El hilo principal de la aplicación se reactiva y llama a WaitFor. Evitando esta particular manifestación de Deadlock. El mejor modo de evitar esta forma de Deadlock, es no usar WaitFor y Synchronize en la misma aplicación. WaitFor puede ser evitado usando el evento OnTerminate, como fue expuesto previamente. Por suerte, en este ejemplo, el resultado del hilo es suficientemente simple como para evitar usar Synchronize a favor de un modo más trivial. Usando WaitFor, el hilo principal puede ahora acceder legalmente a las propiedades del hilo en funcionamiento luego de que éste termina, y todo lo que se necesita es una variable “resultado” para contener el texto producido por el hilo de cálculo. Las modificaciones necesarias para esto son: Quitar el método “DisplayResults” del hilo. Agregar una propiedad al hilo de cálculo. Modificar el manejador de mensajes en el formulario principal. Aquí hay cambios relevantes. Con esto termina la discusión de los mecanismos de sincronización comunes a todas las versiones Win32 de Delphi. Aún no he discutido dos métodos: TThread.Suspend y TThread.Resume. Estos son discutidos en el capitulo 10. Los siguientes capítulos exploran las facilidades del API Win32, y posteriores versiones de Delphi. Sugiero que, una vez que el usuario haya asimilado los aspectos básicos de la programación multihilo en Delphi, se tome el tiempo de estudiar estos mecanismos más avanzados, ya que son una buena manera, más flexible, que trabajar con los mecanismos nativos de Delphi, y permiten al programador coordinar hilos de ejecución en un modo más elegante y eficiente, así como reducir las posibilidades de escribir código que pueda caer en Deadlocks. Capítulo 6. Más sincronización: Secciones críticas y mutexes.
  • 37. En este capítulo: Limitaciones de la sincronización. Secciones críticas. ¿Qué significa todo esto para el programador Delphi? Puntos de interés. ¿Pueden perderse los datos o quedar congelados en el buffer? ¿Qué hay de los mensajes “desactualizados”? Control de Flujo: consideraciones y lista de ineficiencias. Mutexes. Limitaciones de la sincronización. Synchronize tiene algunas desventajas que lo hacen inadecuado para cualquier cosa, salvo aplicaciones multihilo muy sencillas. Synchronize es útil solamente cuando se intenta comunicar un hilo en funcionamiento con el hilo principal de VCL. Synchronize insiste en que el hilo en funcionamiento espere hasta que el hilo principal de VCL esté completamente inactivo aún cuando esto no es estrictamente necesario. Si las aplicaciones hacen un uso frecuente de Synchronize, el hilo principal de VCL se vuelve un cuello de botella y no una verdadera ganancia de performance. Si Synchronize es usado para comunicar indirectamente dos hilos en ejecución, ambos hilos pueden quedar suspendidos esperando por el hilo principal de VCL. Synchronize puede causar Deadlock si el hilo principal de VCL espera por algún otro hilo. En la parte de las ventajas, Synchronize tiene una por sobre la mayoría de los demás mecanismos de sincronización: Casi cualquier código puede ser pasado a Synchronize, incluso código VCL inseguro entre hilos. Es importante recordar porque los hilos son usados en la aplicación. La principal razón para la mayoría de los programadores Delphi es que quieren que sus aplicaciones permanezcan siempre con capacidad de respuesta, mientras se estén realizando otras operaciones que pueden
  • 38. llevar más tiempo o usan transferencias de datos con bloqueo o E/S. Esto generalmente significa que el hilo principal de la aplicación debe realizar rutinas cortas, basadas en eventos y el manejo de las actualizaciones de la interfaz. Es bueno al responder a las entradas de usuario y mostrar las salidas al usuario. Los otros hilos no usan partes de la VCL que no son seguros para trabajar con múltiples hilos. Los hilos que realizan el trabajo pueden realizar operaciones con archivos, bases de datos, pero rara vez usarán descendentes de TControl. A la vista de esto, Synchronize es un caso perdido. Muchos hilos necesitan comunicarse con la VCL de una manera sencilla, como realizar transferencias de cadenas de datos, o ejecutar querys de bases de datos y devolver una estructura de datos como resultado del query. Volviendo atrás, al capitulo 3, notamos que sólo necesitamos mantener la atomicidad cuando modificamos datos compartidos. Para tomar un ejemplo sencillo, nosotros podemos tener una cadena que puede ser escrita por un hilo de procesamiento y ser leída periódicamente por el hilo principal de VCL. ¿Necesitamos asegurarnos que el hilo principal de VCL no se está ejecutando nunca en el mismo momento que el hilo en funcionamiento? ¡Por supuesto que no! Todo lo que necesitamos asegurarnos es que sólo un hilo por vez modifica este recurso compartido, de modo de eliminar las condiciones de carrera y hacer las operaciones en los recursos compartidos atómicas. Esta propiedad es conocida como exclusión mutua. Hay muchas primitivas de sincronización que pueden ser usadas para forzar esta propiedad. La más simple de esta es conocida como Mutex. Win32 provee la primitiva mutex, y una pariente cercana de esta, la Sección Crítica (Critical Section). Algunas versiones de Delphi poseen una clase que encapsula las llamadas a secciones críticas Win32. Esta clase no será discutida aquí, ya que su funcionalidad no es común a todas las versiones de 32 bits de Delphi. Los usuarios de esa clase han de tener algunas dificultades usando los métodos correspondientes en la clase para lograr los mismos efectos que los discutidos aquí. Secciones Críticas.
  • 39. La sección crítica es una primitiva que nos permite forzar la exclusión mutua. El API Win32 soporta varias operaciones sobre esta: InitializeCriticalSection. DeleteCriticalSection. EnterCriticalSection. LeaveCriticalSection. TryEnterCriticalSection (Windows NT unicamente). Las operaciones InitializeCriticalSection y DeleteCriticalSection pueden considerarse como algo muy parecido a la creación y destrucción de objetos en memoria. Por ende, es sensato dejar la creación y destrucción de secciones críticas a un hilo en particular, normalmente el que exista más tiempo en memoria. Obviamente, todos los hilos que quieran tener un acceso sincronizado usando esta primitiva deberán tener un manejador o puntero a esta primitiva. Esto puede ser directo, a través de una variable compartida, o indirecto, quizá porque la sección crítica está embebida en un clase hilo segura, a la que ambos hilos puedan acceder. Una vez que el objeto sección crítica es creado, puede ser usado para controlar el acceso a recursos compartidos. Las dos operaciones principales son EnterCriticalSection y LeaveCriticalSection. En una gran lucha de la literatura estándar en el tema de las sincronizaciones, estas operaciones son también conocidas como WAIT y SIGNAL, o LOCK y UNLOCKrespectivamente. Estos términos alternativos son también usados para otras primitivas de sincronización, y tienen significados equivalentes. Por defecto, cuando se crea la sección crítica, , ninguno de los hilos de la aplicación tiene posesión de ella. Para obtener posesión, un hilo debe llamar a EnterCriticalSection, y si la sección crítica no pertenece a nadie, entonces el hilo obtiene su posesión. Es entonces cuando, típicamente, el hilo realiza operaciones sobre recursos compartidos (la parte crítica del codigo, ilustrada por una doble línea), y una vez que ha terminado, libera su posesión mediante un llamado a LeaveCriticalSection.
  • 40. La propiedad que tienen las secciones críticas es que sólo un hilo por vez puede ser propietario de alguna de ellas. Si un hilo intenta entrar a una sección crítica cuando otro hilo está aún en la sección crítica, el que intenta entrar quedará suspendido, y solamente se reactivará cuando el otro hilo abandone la sección crítica. Esto nos provee la exclusión mutua necesaria con los recursos compartidos. Más de un hilo puede ser suspendido, esperando ser propietario en algún momento, de modo que las secciones críticas pueden ser útiles para sincronizaciones entre más de dos hilos. A modo de ejemplo, aquí está lo que sucedería si cuatros hilos intentaran tener acceso a la misma sección crítica en momentos muy cercanos.
  • 41. Como deja en claro el gráfico, sólo un hilo esta ejecutando código crítico por vez, de modo que no hay problemas de carreras ni de atomicidad. ¿Qué significa todo esto para el programador Delphi? Esto significa que, más allá de que uno no esté realizando operaciones con la VCL, sino sólo haciendo sencillas transferencias de datos, el programador de hilos en Delphi es libre de la carga que significa trabajar con TThread.Synchronize. El hilo principal de la VCL no necesita estar inactivo antes de que el hilo en proceso pueda modificar recursos compartidos, sólo necesita estar fuera de la sección crítica. Las secciones críticas no saben ni les preocupa saber si un hilo es el hilo principal de la VCL o una instancia de un objeto TThread, de modo que uno puede usar las secciones críticas entre cualquier par de hilos. El programador de hilos puede ahora (prácticamente) usar WaitFor en forma segura, evitando problemas de Deadlock.
  • 42. El último punto no es absoluto, ya que aún es posible producir Deadlocks de la misma manera que antes. Todo lo que uno tiene que hacer es llamar a WaitFor en el hilo principal cuando está actualmente en una sección crítica. Como veremos luego, suspender hilos por largos períodos de tiempo mientras está en una sección crítica es normalmente una mala idea. Ahora que la teoría fue explicada adecuadamente, presentaré un nuevo ejemplo. Este es un poco más elegante e interesante que el programa de números primos. Cuando empieza, intenta buscar números primos empezando por el 2, y sigue hacia arriba. Cada vez que encuentra un número primo, actualiza una estructura de datos compartida (una lista de strings) e informa al hilo principal que ha agregado datos a la lista de strings. Aquí está el código del formulario principal. Es bastante similar a los ejemplos anteriores con respecto a la creación del hilo, pero hay algunos miembros extra en el formulario principal que deben ser inicializadas. StringSection es la sección crítica que controla el acceso al recurso compartido entre hilos. FStringBuf es una lista de strings que actúa como buffer entre el formulario principal y el hilo en proceso. El hilo en proceso envía los resultados al formulario principal agregándolos a esta lista de strings, que es el único recurso compartido en este programa. Finalmente tenemos una variable boleana, FStringSectInit. Esta variable actúa como un verificador, asegurándose que los objetos necesarios en la sincronización están realmente creados antes de ser usados. Los recursos compartidos son creados cuando comenzamos un hilo de procesamiento y se destruyen poco tiempo después de que estemos seguros que el hilo de procesamiento ha salido. Nótese que pese a que las listas de strings actúan como buffer que son asignados dinámicamente,debemos usar WaitFor al momento de destruir el hilo, para asegurarnos que el hilo de procesamiento no usa más el buffer antes de liberarlo. Podemos usar WaitFor en este programa sin tener que preocuparnos por posibles Deadlocks, porque podemos probar que no hay nunca una situación donde dos hilos se estén esperando uno al otro. La línea de razonamiento para probar esto es bien simple:
  • 43. 1. El hilo de procesamiento sólo espera cuando intenta ganar acceso a la sección crítica. 2. El hilo del programa principal sólo espera cuando está esperando que el hilo de procesamiento termine. 3. El programa principal no espera cuando tiene posesión de la sección crítica. 4. Si el hilo de procesamiento está esperando por la sección crítica, el programa principal abandonará la sección crítica antes de esperar por algún motivo al hilo de procesamiento. Aquí está el código del hilo de procesamiento. El hilo de procesamiento busca a través de sucesivos enteros positivos, tratando de encontrar alguno que sea primo. Cuando lo encuentra, toma posesión de la sección crítica, modifica el buffer, abandona la sección crítica y luego envía un mensaje al formulario principal indicando que hay datos en el buffer. Puntos de interés. Este ejemplo es más complicado que los ejemplos anteriores, porque tenemos un largo de buffer arbitrario entre dos hilos, y como resultado, hay varios problemas que deben ser considerados y evitados, como así también algunas características del código que lidian con situaciones inesperadas. Estos puntos se pueden resumir en: ¿Pueden perderse los datos o quedar congelados en el buffer? ¿Qué hay acerca de mensajes “desactualizados”? Aspectos de control de flujo. Ineficiencias en la lista de strings, dimensionado estático vs. dinámico. ¿Pueden perderse los datos o quedar congelados en el buffer? El hilo de procesamiento le indica al hilo principal del programa que hay datos para procesar en el buffer mediante el envío de un mensaje. Vale la pena hacer notar que, cuando se usan mensajes de Windows de esta manera, no hay nada inherente al objeto de sincronización del hilo que enlace a un mensaje de windows con una actualización en
  • 44. particular del buffer. Por suerte en este caso, las reglas de causa y efecto funcionan a nuestro favor: cuando el buffer es actualizado, un mensaje es enviado después de la actualización. Esto significa que el hilo principal del programa siempre recibe mensajes de actualización del buffer después de una actualización del buffer. Por este motivo, es imposible que los datos permanezcan en el buffer por una indeterminada cantidad de tiempo. Si los datos están actualmente en el buffer, el hilo de procesamiento y el hilo principal están en algún punto en el proceso desde el envío a la recepción de mensajes de actualización del buffer. Nótese que si el hilo de procesamiento enviara un mensaje antes de actualizar el buffer, puede ser posible que el hilo principal procese el mensaje y lea el buffer antes de que el hilo de procesamiento actualice el buffer con los resultados más recientes, provocando que los resultados más recientes queden atascados en el buffer por algún tiempo. ¿Qué hay de los mensajes “desactualizados”? Las leyes de causa y efecto funcionaron bien en el caso anterior, pero por desgracia, los problemas de comunicación también cuentan. Si el hilo principal está ocupado actualizando por un largo período de tiempo, es posible que los mensajes se apilen en el la cola, de modo que recibimos los mensajes de actualizaciones mucho tiempo después de que el hilo de procesamiento enviara esos mensajes. En la mayoría de las situaciones, esto no presenta un problema. Sin embargo, un caso particular que necesita ser considerado es el caso de que el usuario detenga al hilo de procesamiento, ya sea directamente, presionando el botón “stop”, o indirectamente, mediante el cierre del programa. En este caso, es completamente posible para el hilo principal de VCL terminar el hilo de procesamiento, quitar todos los objetos de sincronización y el buffer, y luego, subsecuentemente, recibir mensajes que se han apilado durante algún tiempo. En el ejemplo mostrado, verifiqué este problema, asegurándome que la sección crítica y el objeto buffer existen antes de procesar los mensajes (La línea de código comentada Not necessarily the case!). Esta consideración tiende a ser suficiente para la mayoría de las aplicaciones.
  • 45. Consideraciones de control de flujo y lista de ineficiencias. Atrás, en el capitulo 2, dije que una vez que se crean hilos, no existe ninguna sincronización implícita entre ellos. Esto era evidente en ejemplos anteriores, como fue demostrado con el problema que puede causar el intercambio de datos entre hilos, como una manifestación del nivel del problema de sincronización en un programa. El mismo problema existe al querer sincronizar la transferencia de datos. No hay nada en el ejemplo de arriba que garantice que el hilo de procesamiento producirá resultados lo suficientemente rápido para que el hilo principal de VCL los pueda tomar cuando los muestra. De hecho, si el programa se ejecuta de modo que el hilo de procesamiento comienza buscando números primos pequeños, es bastante probable que, compartiendo igual cantidad de tiempo de CPU, el hilo de procesamiento desplace el hilo VCL por un margen bastante grande. Este problema es solucionado mediante algo que se llama control de flujo. Control de flujo es el nombre dado al proceso por el que la velocidad de ejecución de algunos hilos es balanceada de modo que la tasa de entradas en el buffer y la tasa de salidas estén medianamente balanceadas. El ejemplo de arriba es particularmente simple, pero ocurre en muchos otros casos. Casi cualquier E/S o mecanismo de transferencia de datos entre hilos o procesos incorpora algún tipo de control de flujo. En casos simples, esto simplemente puede involucrar alguna pieza excepcional de dato en tránsito, suspendiendo ya sea al productor (el hilo que coloca los datos en el buffer) o al consumidor (el hilo que toma los datos). En casos más complejos, el hilo puede ejecutarse en diferentes máquinas y el “buffer” puede estar compuesto de buffers internos en esas máquinas, y las capacidades de almacenamiento de la red entre ellas. Una gran parte del protocolo TCP es la que administra el control de flujo. Cada vez que descargas una página web, el protocolo TCP arbitra entre las dos máquinas, asegurándose que más allá del microprocesador o la velocidad de disco, toda la transferencia de datos ocurre a una tasa que puedan manejar las dos máquinas [1] . En el caso de nuestro ejemplo de arriba, se hizo un intento tosco de controlar el flujo. La prioridad
  • 46. del hilo de procesamiento ha sido establecida de modo que el administrador de tareas seleccione preferentemente al hilo principal de la VLC y no al hilo de procesamiento, mas allá de que ambos tengan trabajo que hacer. En el administrador de tareas de Win32, esto soluciona el problema, pero no es realmente una garantía de hierro. Otro aspecto relacionado con el control de flujo es que, en el caso del ejemplo de arriba, el tamaño del buffer es ilimitado. Primero, esto crea un problema de eficiencia, en el que el hilo principal de la VCL tiene que hacer un gran número de movimientos de memoria cuando quita el primer elemento de una larga lista de strings, y segundo, esto significa que con el control de flujo mencionado arriba, el buffer puede crecer sin límite. Intenta quitar la sentencia que establece la prioridad del hilo. Notarás que el hilo de procesamiento genera resultados mas rápido de lo que el hilo principal de VCL pueda procesar, lo que hace a la lista de strings muy larga. Esto, además, lentifica más el hilo principal de la VCL (ya que las operaciones para quitar strings en una lista larga toman mas tiempo), y el problema se vuelve peor. Eventualmente, notará que la lista se vuelve tan larga como para llenar la memoria principal, la máquina comenzará a retorcerse y todo se detendrá ruidosamente. ¡Tan caótico es, que cuando probé el ejemplo, no pude conseguir que Delphi respondiera a mis solicitudes para salir de la aplicación, y tuve que recurrir al administrador de tareas de Windows NT para terminar el proceso! Simplemente piensa en lo que este programa parece a primera vista. Ha disparado un gran número de potenciales gremlins. Soluciones más robustas a este problema son discutidas en la segunda parte de esta guía. Mutexes. Un mutex funciona exactamente del mismo modo que las secciones críticas. La única diferencia en las implementaciones Win32 es que la sección crítica esta limitada para ser usada con solamente un proceso. Si tienes un programa que usa varios hilos, entonces la sección crítica es liviana y adecuada para tus necesidades. Sin embargo, cuando
  • 47. escribes una DLL, es muy posible que diferentes procesos usen la DLL en el mismo momento. En este caso, debes usar mutexes, en lugar de secciones críticas. Pese a que el API Win32 provee un rango más variado de funciones para trabajar con mutexes y otros objetos de sincronización que serán explicados aquí, las siguientes funciones son análogas a las descriptas para secciones críticas más arriba: CreateMutex / OpenMutex CloseHandle WaitForSingleObject(Ex) ReleaseMutex Estas funciones están bien documentadas en los archivos de ayuda del API Win32, y serán discutidas en más detalle luego. [1] El protocolo TCP también realiza muchas otras funciones raras y maravillosas, como copiar con datos perdidos y el optimizado del tamaño de las ventanas de modo que el flujo de la información no sólo se ajusta a las dos máquinas en los extremos de la conexión, sino también a la red que las une, mientras mantiene una mínima latencia y maximizando la conexión. También posee algoritmos de back-off para asegurarse que varias conexiones TCP puedan compartir una conexión física, sin que ninguna de ellas monopolice el recurso físico. Capítulo 7. Guía de programación de mutex. Control de concurrencia. En este capitulo: Momento para introducir un poco de estilo. Deadlock en función del ordenamiento de mutex. Evitando el Deadlock de un hilo, dejando que la espera de time-out. Evitando el Deadlock de un hilo, imponiendo un orden en la adquisición de mutex. Fuera de la cacerola y ¡en el fuego! Evitando el Deadlock al “modo vago” y dejando que Win32 lo haga por ti. Atomicidad en la composición de operaciones – optimismo versus pesimismo en el control de concurrencia. Control de concurrencia optimista.
  • 48. Control de concurrencia pesimista. Evitando agujeros en el esquema de bloqueo. ¿Ya está confundido? ¡Puede tirar la toalla! ¿Momento para introducir un poco de estilo? La mayoría de los ejemplos presentados en este tutorial eran bastante puntuales y preparados. Cuando diseñamos componentes reusables, o las bibliotecas para una gran aplicación multihilo, una concepción de “vuelo de águila” no es apropiada. El programador o diseñador de componentes necesitan construir clases que tengan seguridad para la programación multihilo en sí mismos, es decir, clases que asuman que podrían ser accedidas desde diferentes hilos y poseer los mecanismos internos necesarios para asegurarse de que los datos se mantengan consistentes. Para hacer esto, el diseñador de componentes necesita estar al tanto de algunos problemas que surgen cuando se usan mutex en aplicaciones cada vez más complicadas. Si esta tratando de escribir una clase que sea segura para funcionar con hilos por primera vez, no se deje desanimar por la aparente complejidad de algunas consideraciones de este capitulo. Con bastante frecuencia se pueden adoptar soluciones simplistas, que nos evitan muchas de las consideraciones mencionadas en este capitulo, a cambio de una menor eficiencia. Nótese que cada vez que se mencione “mutex” de aquí en más, lo mismo vale para las secciones críticas; omitiré mencionar las secciones críticas en cada caso para abreviar. Deadlock en función del ordenamiento de mutex. Si un programa posee más de un mutex, entonces será sorprendentemente sencillo provocar un Deadlock, con un código de sincronismo descuidado. La situación más común es cuando existen dependencias cíclicas por el orden en que los mutex son adquiridos. Esto es generalmente conocido en la literatura académica como el problema de la cena de los filósofos. Como vimos antes, el criterio de un Deadlock es que todos los hilos están esperando a otro para liberar el objeto de sincronización. El ejemplo más sencillo de esto es entre dos hilos, uno que quiere adquirir el mutex A antes de adquirir el
  • 49. mutex B y otro que quiere adquirir el mutex B antes de adquirir el mutex A. Por supuesto, es completamente posible hacer caer un programa en un Deadlock de una manera más delicada con una cadena de dependencias, como la ilustrada más abajo con cuatro hilos y cuatro mutexes, A a D. Obviamente, situaciones como esta no son aceptables en la mayoría de las aplicaciones. Hay muchas maneras de evitar este problema, y un montón de técnicas para aliviar problemas de dependencia de este tipo, haciendo mucho más sencillo evitar situaciones de Deadlock. Evitando el Deadlock de un hilo, dejando que la espera de time- out.
  • 50. Las funciones de Win32 para lidiar con mutex no requieren que un hilo espere por siempre para adquirir un objeto mutex. La función WaitForSingleObject le permite a uno especificar un tiempo que el hilo está preparado a esperar. Una vez que ha pasado este tiempo, el hilo será desbloqueado y la llamada devolverá un código de error indicando que a la espera se le acabó el tiempo (time-out). Cuando usamos mutex para forzar el acceso sobre una región crítica del código, uno no espera típicamente que el hilo tenga que esperar mucho tiempo, y un time-out establecido para suceder en pocos segundos debería ser apropiado. Si tu hilo usa este método, entonces deberá, por supuesto, poder manejar situaciones de error en forma adecuada, quizás volviéndolo a intentar o abandonándolo. Desde luego que los usuarios de las secciones críticas no tienen este lujo, ya que las funciones de espera de las funciones críticas esperan por siempre. Evitando el Deadlock de un hilo imponiendo un orden en la adquisición de mutex. Si bien es una buena idea ser capaz de manejar situaciones de error al adquirir un mutex, es una buena práctica asegurarse que las situaciones de Deadlock no sucedan en primer lugar. Como este tipo de Deadlock es provocado por dependencias cíclicas, puede ser eliminado al imponer un orden en la adquisición de mutexes. Este ordenamiento es muy sencillo. Digamos que tenemos un programa con mutexes M1, M2, M3, … Mn, donde uno o más de estos mutex pueden ser adquiridos por los hilos en el programa. El Deadlock no ocurrirá ya que para algún mutex arbitrario Mx, los hilos sólo intentarán adquirir el mutex Mx si no tienen posesión de alguno de los mutex de “mayor prioridad”, esto es M(x+1)… Mn. ¿Suena un poco abstracto? Tomemos un ejemplo concreto bastante sencillo. En esta parte del capitulo, me referiré a objetos de “bloqueo” y “desbloqueo”. Esta terminología parece apropiada cuando un mutex está asociado con un dato, y el acceso atómico a ese dato es necesario. Uno debería notar que esto efectivamente significa que cada hilo obtiene el mutex antes de acceder a un objeto, y abandona el mutex después de haber accedido: la operación es idéntica a las discutidas
  • 51. anteriormente, el único cambio está en la terminología, que para esta coyuntura, es más apropiada para un modelo orientado a objetos. En esencia, Objeto.Lock puede ser considerado completamente equivalente a EnterCriticalSection(Objecto.CriticalSection) o quizás WaitForSingleObject(Objeto.Mutex, INFINITE). Tenemos una lista con estructuras de datos que es accedida por varios hilos. Enganchados a la lista hay algunos objetos, cada uno de los cuales tiene su propio mutex. De momento, asumiremos que la estructura de la lista es estática, no cambia, y puede ser leída libremente por los hilos sin ningún tipo de bloqueo. Los hilos que operan en esta estructura de datos quieren hacer alguna de estas cosas: Leer un ítem, bloqueándolo, leyendo los datos, y luego desbloqueándolo. Escribir en un ítem, bloqueándolo, escribiendo los datos, y luego desbloqueándolo. Comparar dos ítems, bloqueándolos primero en la lista, luego realizando la comparación y desbloqueándolo. Un simple pseudo-código para estas funciones, ignorando los tipos, manejos de excepciones y otros aspectos que no son centrales, puede verse como algo así. Imaginémonos por un momento que a un hilo se le pide comparar los ítems X e Y de la lista. Si el hilo siempre bloquea X y luego Y, entonces podría ocurrir un Deadlock si a un hilo se le pide comparar ítems 1 y 2, y a otro hilo se le pide comparar ítems 2 y 1. Una solución sencilla sería bloquear primero el ítem cuyo número sea el menor, u ordenar los índices de entrada, realizar los bloqueos y ajustar los
  • 52. resultados de la comparación apropiadamente. Sin embargo, una situación más interesante es cuando un objeto contiene detalles de otro objeto con el que es necesario hacer la comparación. En esta situación, el hilo puede bloquear el primer objeto, obtener el índice del segundo objeto en la lista, darse cuenta que el índice de este es menor en la lista, bloquearlo y proceder luego con la comparación. Todo muy fácil. El problema ocurre cuando el segundo objeto tiene mayor índice en la lista que el primero. No podemos bloquearlo inmediatamente, porque de hacerlo, estaríamos permitiendo que se produzca un Deadlock. Lo que debemos hacer es desbloquear el primer objeto, bloquear el segundo y luego volver a bloquear el primero. Esto nos asegura que el Deadlock no ocurrirá. Aquí hay un ejemplo de comparación indirecta, representativo de esta discusión. Fuera de la cacerola y ¡en el fuego! Si bien esto evita las situaciones de Deadlock, crea un problema peliagudo. En la demora entre desbloqueo y vuelta a bloquear del primer objeto, no podemos estar seguros que otro hilo no ha modificado el primero objeto antes de que hayamos vuelto. Esto se da porque nosotros realizamos una operación compuesta: la operación en sí no es más atómica. Solucione a este problema son discutidas más abajo, en la página. Evitando el Deadlock al “modo vago” y dejando que Win32 lo haga por ti. Concientes de la gimnasia mental que estos problemas pueden presentar, los adorables diseñadores de Sistemas Operativos en Microsoft, nos han provisto de una manera de solucionar el problema mediante otra función de sincronización de Win32: WaitForMultipleObjects(Ex). Esta función le permite al programador esperar para adquirir muchos objetos de sincronización (incluyendo mutex) de una vez. En particular, esto le permite a un hilo esperar hasta que uno o todo un grupo de objetos estén libres (en el caso de mutex, el equivalente seria “sin propietario”), y luego adquirir la
  • 53. propiedad de los objetos señalados. Esto tiene la gran ventaja de que si dos hilos esperan por los mutex A y B, no importa que orden especificaron en el grupo de objetos para esperar, o ningún objeto es adquirido o todos son adquiridos atómicamente, de modo que es imposible un caso de deadlock de esta manera. Este enfoque también tiene algunas desventajas. La primera desventaja es que como todos los objetos de sincronización deben estar libres antes de que alguno de ellos sea adquirido, es posible que un hilo que espere por un gran número de objetos, no adquiera la propiedad por un largo período de tiempo si otros hilos están adquiriendo los mismos objetos de sincronización de a uno. Por ejemplo, en el diagrama de abajo, el hilo más a la izquierda espera por los mutexes A, B y C, mientras que otros tres hilos adquieren cada mutex en forma individual. En el peor de los casos, el hilo esperando por muchos objetos puede que nunca adquiera la propiedad. La segunda desventaja es que aún es posible caer en trampas de Deadlock, esta vez no con un solo mutex, ¡sino con un grupo de varios mutexes! La tercera desventaja que tiene este enfoque, en común con método de “time-out” para evitar el Deadlock, es que no es posible usar esta función si se están usando secciones críticas, la función EnterCriticalSection no le permite especificar una cantidad de tiempo de espera, ni tampoco devuelve un código de error. Atomicidad en la composición de operaciones – optimismo versus pesimismo en el control de concurrencia. Cuando pensamos en el ordenamiento de mutex anterior, nos encontramos en una situación donde necesitamos desbloquear para luego volver a bloquear un objeto de modo de respetar el ordenamiento de mutex. Esto significa que varias operaciones han ocurrido en un objeto y el bloqueo de ese objeto ha sido liberado entre medio de dichas operaciones. Control de concurrencia optimista.
  • 54. Una manera de lidiar con el problema es asumir que este tipo de interferencia de hilos es poco probable que ocurra, y simplemente verificar el problema y devolver un error si esto es así. Esto es comúnmente un modo válido de lidiar con el problema en situaciones complejas donde la “sobrecarga” de estructuras de datos por varios hilos no es demasiado elevada. En el caso presentado antes, podemos verificar trivialmente esto, guardando una copia local de los datos y verificando que aún son válidos cuando volvemos a bloquear ambos objetos en el orden requerido. Aquí esta la rutina modificada. Con estructuras de datos más complicadas, uno puede recurrir algunas veces a ID’s únicos globales o marcado de versiones en piezas de código. Como nota personal, recuerdo haber trabajado con un grupo de otros estudiantes en un proyecto de fin de año de la universidad, donde este enfoque funcionó muy bien: un número secuencial era incrementado cuando una pieza de datos era modificada (en este caso los datos consistían en anotaciones en un diario multiusuario). Los datos eran bloqueados mientras se leía, luego se mostraban al usuario y si el usuario editaba los datos, el número era comparado con el obtenido por el usuario en la última lectura, y la actualización era abandonada si los números no coincidían. Control de concurrencia pesimista. Podemos tomar un enfoque bastante diferente del problema, considerando que la lista tiende a ser modificada y, por esto, requiere su propio bloqueo. Todas las operaciones que lean o escriban en la lista, incluyendo búsquedas, deberán bloquear primero la lista. Esto provee una solución alternativa al problema de bloquear limpiamente a varios objetos en la lista. Revisemos las operaciones que deseamos realizar nuevamente, con el ojo puesto en este diseño alternativo del bloqueo. Un hilo puede querer leer y modificar los contenidos de un objeto de la lista, pero sin modificar el objeto existente ni su posición en la lista. Esta operación toma mucho tiempo, y no queremos obstaculizar a
  • 55. otros hilos que quieran operar con otros objetos, de modo que el hilo que modifique el objeto debe realizar las siguientes operaciones: Bloquear la lista. Buscar el objeto en la lista. Bloquear el objeto. Desbloquear la lista. Realizar las operaciones en el objeto. Desbloquear el objeto. Esto es fantástico ya que, aún si el hilo realiza operaciones de lectura o escritura en el objeto que tomen mucho tiempo, no tendrá la lista bloqueada por ese tiempo y, por ende, no demorará a otros hilos que quieran modificar otros objetos. Un hilo puede eliminar un objeto llevando a cabo el siguiente algoritmo: Bloquear la lista. Bloquear el objeto. Eliminar el objeto de la lista. Desbloquear la lista. Eliminar el objeto (esto está sujeto a posibles restricciones al borrar un mutex que está bloqueado). Nótese que es posible desbloquear la lista antes de eliminar finalmente el objeto, ya que eliminamos el objeto de la lista, y así sabemos que ninguna otra operación está en progreso en el objeto o la lista (al tener a ambos bloqueados). Aquí viene la parte interesante. Un hilo puede comparar dos objetos llevando a cabo un algoritmo más simple que el mencionado en la sección anterior: Bloquear la lista. Buscar el primer objeto. Bloquear el primer objeto. Buscar el segundo objeto. Bloquear el segundo objeto. Desbloquear la lista. Realizar la comparación.
  • 56. Desbloquear los objetos (en cualquier orden). Como verán, en la operación de comparación, no he hecho ninguna restricción en el orden en que son realizados los bloqueos en los objetos. ¿Podrá esto provocar un Deadlock? El algoritmo presentado no necesita el criterio para evitar los Deadlocks presentados al comienzo del capitulo, porque los Deadlock no ocurrirán nunca. Y no ocurrirán nunca porque cuando un hilo bloquea un objeto mutex, él ya tiene posesión del mutex de la lista, y con esta posesión, puede bloquear varios objetos si no libera el mutex de la lista. El bloqueo compuesto en varios objetos resulta atómico. Como resultado de esto, podemos modificar el criterio de Deadlock de arriba: El Deadlock no ocurrirá ya que para algún mutex arbitrario Mx, los hilos sólo intentarán adquirir el mutex Mx si no tienen posesión de alguno de los mutex de “mayor prioridad”, esto es M(x+1)… Mn. Además, el Deadlock no ocurrirá si los mutex son adquiridos en cualquier orden (rompiendo el criterio de arriba), y para cualquier grupo de mutex involucrados en una adquisición que no lleva un orden, si todas las operaciones de bloqueo en esos mutex son atómicas, normalmente mediante el bloqueo de las operaciones dentro de una sección crítica (obtenida por el bloqueo de otro mutex). Evitando agujeros en el esquema de bloqueo. No es ninguna novedad a estas alturas, que el ejemplo de arriba es típico de un código de bloqueo que es muy sensible al ordenamiento. Más allá de esto, todo esto debe indicarnos que cuando ideamos esquemas de bloqueo que no son triviales, debemos tener mucho cuidado en el orden en que suceden las cosas. Si estás seguro que tu programa funcionará en Windows NT (o 2K, XP, 2003), entonces el API de Windows provee en efecto una solución al problema de operaciones compuestas cuando se desbloquean y vuelven a bloquear objetos. La llamada del APISignalObjectAndWait te permite marcar atómicamente (o liberar) un objeto de sincronización, y esperar por otro. Conservando
  • 57. estas dos operaciones atómicas, se puede transferir un estado de bloqueo de un objeto a otro, mientras que se asegura que ningún otro hilo modifica el estado del objeto durante la transferencia. Esto significa que el control de concurrencia optimista no es necesario en estas situaciones. ¿Ya esta confundido? ¡Puede tirar la toalla! Si pudo permanecer leyendo hasta este punto, lo felicito, ha adquirido un conocimiento básico del los problemas que le dan a los programadores multihilo bastante dolores de cabeza. Es útil destacar que los esquemas complicados en estructuras internas de datos son habitualmente necesarios para sistemas con alta performance. Pequeñas aplicaciones de escritorio pueden funcionar habitualmente con enfoques no tan complicados. Hay varias maneras de “tirar la toalla”. No se preocupe por la eficiencia, y bloquee todo. Meta todos los datos en la BDE. Bloquear todos los datos compartidos es habitualmente útil, si uno está dispuesto a sacrificar eficiencia. La mayoría de los usuarios prefieren un programa que funciona un poco lento que uno que falla en intervalos impredecibles, por errores en el esquema de bloqueo. Si uno tiene una gran cantidad de datos que necesitan ser persistentes de alguna manera, poner todos los datos en la BDE es otro enfoque. Todos los (medianamente decentes) motores de bases de datos son seguros para trabajar con múltiples hilos, lo que significa que puedes acceder a tus datos sin ningún problema desde hilos separados. Si usas un motor de bases de datos, entonces deberás estudiar algo sobre administración de transacciones, por ejemplo, las semánticas reservation, y el uso de premature, commit y rollback, pero recuerda que esto es sólo el enfoque basado en transacciones para solucionar problemas de concurrencia, y sencillamente la otra cara de la misma moneda; la mayor parte de la programación difícil (incluido los dolores de cabeza) lo han hecho por ti. El uso de la BDE con hilos de ejecución será tratado luego.