multithreading a la manera de delphi

228
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.

Upload: mayra-mendieta

Post on 13-Jun-2015

5.579 views

Category:

Documents


16 download

TRANSCRIPT

Page 1: Multithreading a la manera de Delphi

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.

Page 2: Multithreading a la manera de Delphi

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!

Page 3: Multithreading a la manera de Delphi

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."

Page 4: Multithreading a la manera de Delphi

É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.

Page 5: Multithreading a la manera de Delphi

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 VaughtCré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

Page 6: Multithreading a la manera de Delphi

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

Page 7: Multithreading a la manera de Delphi

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

Page 8: Multithreading a la manera de Delphi

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

Page 9: Multithreading a la manera de Delphi

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.

Page 10: Multithreading a la manera de Delphi

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.

Page 11: Multithreading a la manera de Delphi

¿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.

Page 12: Multithreading a la manera de Delphi

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.

Page 13: Multithreading a la manera de Delphi

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ú

Page 14: Multithreading a la manera de Delphi

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

Page 15: Multithreading a la manera de Delphi

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

Page 16: Multithreading a la manera de Delphi

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.

Page 17: Multithreading a la manera de Delphi

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

Page 18: Multithreading a la manera de Delphi

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

Page 19: Multithreading a la manera de Delphi

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,

Page 20: Multithreading a la manera de Delphi

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

Page 21: Multithreading a la manera de Delphi

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.

Page 22: Multithreading a la manera de Delphi

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.

Page 23: Multithreading a la manera de Delphi

Instrucciones ejecutadas por el

hilo X

Instrucciones ejecutadas por el

hilo Y

Valor de la variable A

en memoria

<otras instrucciones>

Hilo suspendido 1

Lee A desde la memoria en un registro del procesador.

Hilo suspendido 1

Incrementa en 1 el registro del procesador.

Hilo suspendido 1

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

Hilo suspendido 2

<otras instrucciones>

Hilo suspendido 2

CAMBIO DE HILO CAMBIO DE HILO 2

Hilo suspendido <otras instrucciones>

2

Hilo suspendido

Lee A desde la memoria en un registro del procesador.

2

Hilo suspendidoIncrementa en 1 el registro del procesador.

2

Hilo suspendido Escribe el contenido 3

Page 24: Multithreading a la manera de Delphi

del registro del procesador en 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:

Instrucciones ejecutadas por el

hilo X

Instrucciones ejecutadas por el

hilo Y

Valor de la variable A

en memoria

<otras instrucciones>

Hilo suspendido 1

Lee A desde la memoria en un registro del procesador.

Hilo suspendido 1

Incrementa en 1 el registro del procesador.

Hilo suspendido 1

CAMBIO DE HILO CAMBIO DE HILO 1

Hilo suspendido <otras instrucciones>

1

Hilo suspendido

Lee A desde la memoria en un registro del procesador.

1

Hilo suspendido Incrementa en 1 el registro del

1

Page 25: Multithreading a la manera de Delphi

procesador.

Hilo suspendido

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

1

CAMBIO DE HILO CAMBIO DE HILO 2

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

Hilo suspendido 2

<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.

Page 26: Multithreading a la manera de Delphi

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

Page 27: Multithreading a la manera de Delphi

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

Page 28: Multithreading a la manera de Delphi

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.

Page 29: Multithreading a la manera de Delphi

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.

Page 30: Multithreading a la manera de Delphi

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.

Page 31: Multithreading a la manera de Delphi

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:

Page 32: Multithreading a la manera de Delphi

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.

Page 33: Multithreading a la manera de Delphi

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.

Page 34: Multithreading a la manera de Delphi

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

Page 35: Multithreading a la manera de Delphi

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

Page 36: Multithreading a la manera de Delphi

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

Page 37: Multithreading a la manera de Delphi

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.

Page 38: Multithreading a la manera de Delphi

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.

Page 39: Multithreading a la manera de Delphi

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.

Page 40: Multithreading a la manera de Delphi

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.

Page 41: Multithreading a la manera de Delphi

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

Page 42: Multithreading a la manera de Delphi

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/MsgWaits.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

Page 43: Multithreading a la manera de Delphi

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.

Page 44: Multithreading a la manera de Delphi

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é

Page 45: Multithreading a la manera de Delphi

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.

Page 46: Multithreading a la manera de Delphi

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.

Page 47: Multithreading a la manera de Delphi

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.

Page 48: Multithreading a la manera de Delphi

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

Page 49: Multithreading a la manera de Delphi

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.

Page 50: Multithreading a la manera de Delphi

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

Page 51: Multithreading a la manera de Delphi

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.

Page 52: Multithreading a la manera de Delphi

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

Page 53: Multithreading a la manera de Delphi

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

Page 54: Multithreading a la manera de Delphi

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

Page 55: Multithreading a la manera de Delphi

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

Page 56: Multithreading a la manera de Delphi

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

Page 57: Multithreading a la manera de Delphi

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

Page 58: Multithreading a la manera de Delphi

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

Page 59: Multithreading a la manera de Delphi

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

Page 60: Multithreading a la manera de Delphi

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) ReleaseMutexEstas 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.

Page 61: Multithreading a la manera de Delphi

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

Page 62: Multithreading a la manera de Delphi

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.

Page 63: Multithreading a la manera de Delphi

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

Page 64: Multithreading a la manera de Delphi

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

Page 65: Multithreading a la manera de Delphi

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í.

Page 66: Multithreading a la manera de Delphi

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

Page 67: Multithreading a la manera de Delphi

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

Page 68: Multithreading a la manera de Delphi

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

Page 69: Multithreading a la manera de Delphi

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

Page 70: Multithreading a la manera de Delphi

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:

Page 71: Multithreading a la manera de Delphi

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).

Page 72: Multithreading a la manera de Delphi

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

Page 73: Multithreading a la manera de Delphi

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.

Capítulo 8. Clases Delphiseguras para entornos multihilo y prioridades.

En este capitulo:

Page 74: Multithreading a la manera de Delphi

¿Porqué escribir clases seguras para la entornos multihilo?

Tipos de clases seguras para entornos multihilo. Encapsulado de clases seguras en entornos

multihilo o derivaciones de clases existentes. Clases para la administración del flujo de los datos. Monitores. Clases Interlock. Soporte multihilo en la VCL. TThreadList TSynchroObject TCriticalSection TEvent y TSimpleEvent TMultiReadExclusiveWriteSincronizer Guía para programadores de clases seguras en

entornos multihilo. Administración de prioridades. ¿Qué hay en una prioridad? El modo de hacerlo de

Win32. ¿De qué prioridad debo hacer mi hilo?

¿Porqué escribir clases seguras para la entornos multihilo?

Las aplicaciones simples en Delphi, escritas por iniciados en la programación multihilo, tienden a incluir su sincronización como parte de la lógica de la aplicación. Como demostró el capitulo anterior, es increíblemente fácil que se generen errores en la lógica de sincronización, y diseñar un esquema de sintonización separado para cada aplicación es mucho trabajo. Un número relativamente pequeño de mecanismos de sincronización son usados una y otra vez: casi todos los hilos destinados a E/S, comunican los datos a través de buffers compartidos, y el uso de listas y colas con sincronización incorporada en

Page 75: Multithreading a la manera de Delphi

situaciones de E/S es muy común. Estos factores indican que hay muchas ventajas si creamos librerías de objetos y estructuras de datos que son seguras en entornos multihilos: los problemas involucrados en la comunicación entre hilos son difíciles, pero un pequeño número de soluciones “en stock” cubren casi todos los casos.

Algunas veces es necesario escribir una clase que sea segura en entornos multihilo porque no es aceptable otro enfoque. Códigos en DLL’s que accede a variables únicas del sistema deben poseer sincronización de hilos, aún si la DLL no posee ningún objeto hilo. Dado que los programadores Delphi usarán las facilidades del lenguaje (clases) para permitir un desarrollo modular y re-utilización de código, estas DLL’s tendrán clases, y estas clases deben ser seguras para entornos multihilo. Algunas pueden ser bastante simples, quizás clases que sean instancias de buffers comunes, como las descriptas antes. De todos modos, es muy deseable que algunas de estas clases hilo puedan implementar el bloqueo de recursos u otro mecanismo de sincronización en un modo totalmente único de modo de resolver un problema en particular.

Tipos de clases seguras para entornos multihilo.

Las clases vienen en muchas formas y tamaños diferentes, programadores con una razonable experiencia en Delphi estarán al tanto que el concepto de clase se usa en muchas formas diferentes. Algunas clases son usadas principalmente como estructuras de datos, otras como abstracciones para simplificar un compleja estructura interior. Algunas veces, familias de clases cooperando son

Page 76: Multithreading a la manera de Delphi

usadas para proveer flexibilidad cuando son usadas para alcanzar un logro importante, como está bien demostrado en el mecanismo de streaming de Delphi. Cuando nos referimos a las clases seguras para los entornos multihilo, se presenta una diversidad similar. En algunos casos, la clasificación puede resultar un poco confusa, pero de todos modos, cuatro tipos distintivos de clases seguras para entornos multihilo pueden ser distinguidas.

Encapsulado de clases seguras en entornos multihilo o derivaciones de clases existentes.

Estas son el tipo más simple de clases para entornos multihilo. Típicamente, la clase que es ampliada, tiene una funcionalidad bastante limitada y está contenida en sí misma. En el caso más simple, hacer que la clase sea segura para entornos multihilo puede consistir simplemente en agregar un mutex, y dos funciones extra, Lock y Unlock. Como alternativa, las funciones que manipulan los datos en la clase pueden realizar las operaciones de bloqueo y desbloqueo automáticamente. Cuál enfoque es usado, depende mucho del tipo de operaciones posibles en el objeto, y la probabilidad de que el programador vaya a usar funciones de bloqueo manual para forzar la atomicidad de operaciones compuestas.

Clases para la administración del flujo de los datos.

Estas son una pequeña extensión de las de arriba y tienden a ser una clase buffer: listas, pilas y colas. Además, para mantener la atomicidad, estas clases pueden realizar un control automático del flujo de datos en los hilos que operan en el buffer. Esto

Page 77: Multithreading a la manera de Delphi

consiste frecuentemente en suspender los hilos que intentan leer de un buffer vacío o escribir en uno que está lleno. La implementación de estas clases se trata con más detalle en el capitulo 10. Un rango de operaciones puede ser soportado por esta clase: en un extremo del buffer se proveerán operaciones que no realizarán ningún bloqueo, y en el otro extremo, todas las operaciones pueden bloquear hilos si no fuera posible completarlas con éxito. Un punto intermedio se da cuando las operaciones son asincrónicas, pero provistas de notificaciones por call-back o mensajería cuando una operación anterior no pueda llegar a completarse con éxito. El API de sockets de Win32 es un buen ejemplo de interfase de flujo de datos, que implementa todas las opciones de arriba en lo que a flujo de datos concierne.

Monitores.

Monitores son un paso lógico en el camino hacia las clases administradoras del flujo de datos. Estos típicamente permiten acceso concurrente a los datos, lo que requiere una sincronización y bloqueo más complejo que un simple encapsulado de clases Delphi para que sean seguras en entornos multihilo. Los motores de bases de datos caen en el fin último de esta categoría: típicamente, un complicado bloqueo y administración de transacciones es provisto para permitir un alto grado de concurrencia cuando se acceden a datos compartidos, con una mínima pérdida de performance por los conflictos entre hilos. Los motores de bases de datos son un caso especial en el sentido de que usan administradores de transacciones para permitir un control fino sobre las operaciones de composición, y también proveen

Page 78: Multithreading a la manera de Delphi

garantías acerca de la persistencia de las operaciones para funcionar hasta completarse. Otro buen ejemplo de monitores es el del sistema de archivos.  El sistema de archivos de Win32 permite que múltiples hilos accedan a múltiples archivos que pueden estar abiertos por varios procesos diferentes en modos muy diferentes al mismo tiempo. Una gran parte de un buen sistema de archivos consiste en la administración de manejadores y esquemas de bloqueo que proveen una óptima performance, mientras aseguran que la atomicidad y la persistencia de las operaciones sea preservada. Como dice Layman: “Todo el mundo puede tener sus dedos en el sistema de archivos, pero éste se asegura de que ninguna operación entrará en conflicto y, una vez que la operación se haya completado, es garantizado que será conservada permanentemente en el disco”. En particular, el sistema de archivos NTFS está “basado en log”, de modo que es garantizado que será consistente, aún cuando haya fallas de energía o en el sistema operativo.

Clases Interlock.

Las clases Interlock son únicas en esta clasificación, porque éstas no contienen ningún dato. Algunos mecanismos de bloqueo son muy útiles en el sentido de que el código que forma parte del sistema de bloqueo puede ser fácilmente separado del código que maneja los datos compartidos. El mejor ejemplo de esto es la clase “Interlock de Muchos lectores y un único escritor”, que permite una lectura compartida y operaciones de escritura atómicas en un recurso. El modo de operación de esto será examinado más

Page 79: Multithreading a la manera de Delphi

abajo, y el funcionamiento interno de la clase será visto en capítulos posteriores.

Soporte multihilo en la VCL.

En Delphi 2, ninguna clase fue provista para asistir al programador multihilo, todas las sincronizaciones fueron hechas en un estricto “hágalo usted mismo”. Desde entonces, el estado de la VCL fue mejorado en este aspecto. Discutiré las clases que se encuentran en Delphi 5, ya que esta es la versión disponible para mí. Los usuarios de Delphi 2 y 3 puede que no tengan algunas de estas clase, y los usuarios de Delphi 5(+) puede que encuentren extensiones a estas clases. En este capitulo, presentaré una breve descripción a estas clases y sus usos. Tenga en cuenta que en sí, muchas de estas clases prefabricadas de Delphi no son terriblemente útiles, en cambio, ofrecen un pequeño valor agregado sobre el mecanismo disponible en el API Win32.

TThreadList

Como se mencionó antes, listas, pilas y colas son muy comunes cuando se implementa la comunicación entre hilos. La clase TThreadList realiza sincronizaciones de las mas básicas requeridas por hilos de ejecución. En adición a los métodos presentes en TList, se agregaron dos métodos extra: Lock y Unlock. El uso de estos debe ser bastante obvio para los lectores que han visto como se trabaja a través de los capítulos anteriores: La lista es bloqueada antes de ser manipulada, y desbloqueada luego. Si un hilo realiza múltiples operaciones en a lista que necesitan ser atómicas, entonces la lista

Page 80: Multithreading a la manera de Delphi

permanece bloqueada. La lista no realiza ninguna sincronización implícita en los objetos que son propiedad de una lista en particular. El programador puede idear mecanismos extra de bloqueo para proveer esta habilidad, o alternativamente, usar el bloqueo en la lista para cubrir todas las operaciones en estructuras de datos que sean propiedad de la lista.

TSynchroObject

Esta clase provee un puñado de métodos virtuales, Adquire y Release que son usados en todas las clases básicas de sincronización en Delphi, dado que la realidad última de los objetos simples de sincronización tienen el concepto de posesión como fue discutido previamente. Las secciones críticas y las clases evento son derivadas de esta clase.

TCriticalSection

 Esta clase no necesita ninguna explicación detallada. Sospecho su inclusión en Delphi como simplemente destinada a aquellos programadores Delphi con fobia al API Win32. No es nada valiosa, ya que provee cuatro métodos: Adquire, Release, Enter y Leave. Los dos últimos no hacen más que llamar a los dos primeros, sólo en caso de que un programador prefiera un tipo de nomenclatura en lugar del otro.

TEvent y TSimpleEvent

Los eventos son un modo ligeramente diferente de bloqueo en la sincronización. En lugar de forzar la exclusión mutua, se usan para hacer que un número variable de hilos esperen hasta que algo suceda, y

Page 81: Multithreading a la manera de Delphi

entonces liberar uno o todos esos hilos cuando ese algo sucede. TSimpleEvent es un caso particular de evento, que especifica varios valores por defecto deseables para ser usados en aplicaciones Delphi. Los eventos están muy relacionados con los semáforos, y son discutidos en capítulos posteriores.

TMultiReadExclusiveWriteSincronizer

Este objeto de sincronización es muy útil en situaciones conde un gran número de hilos pueden necesitar leer un recurso compartido, pero ese recurso es escrito con relativa poca frecuencia. En estas situaciones, no suele ser necesario bloquear completamente el recurso. En capítulos anteriores dije que cualquier uso de recursos compartidos sin sincronizar era un potencial generador de conflictos entre hilos. Si bien esto es cierto, no es necesario seguir con la idea de que una exclusión mutua se necesita siempre. Una exclusión mutua completa insiste en que sólo un hilo puede realizar alguna operación en algún momento. Podemos relajarnos con esto, si nos vemos que hay dos tipos principales de conflictos entre hilos:

Escribir después de que se haya hecho una lectura. Escribir después de que se haya hecho otra

escritura.El conflicto de escribir después de que se haya

hecho una lectura ocurre cuando un hilo escribe en una parte de un recurso después de que otro hilo ha leído ese valor, y asume que es válido. Este es el tipo de conflictos ilustrado en el capítulo tres. El otro tipo de conflicto ocurre cuando dos hilos escriben en un recurso compartido, uno después del otro, sin que el segundo hilo haya percibido la escritura anterior.

Page 82: Multithreading a la manera de Delphi

Esto resulta en que la primera escritura es eliminada. Por supuesto, algunas operaciones son perfectamente legales, como leer después de leer o leer después de escribir. ¡Estas dos operaciones ocurren todo el tiempo en programas con un único hilo! Esto parece indicarnos que podemos relajar un poco el criterio para la consistencia de datos. Los criterios mínimos son:

Varios hilos pueden leer al mismo tiempo. Sólo un hilo puede escribir por vez. Si un hilo está escribiendo, entonces ningún hilo

puede estar leyendo.El sincronizador

TMultiReadExclusiveWriteSincronizer fuerza este criterio al proveer cuatro funciones: BeginRead, BeginWrite, EndRead, EndWrite. Al llamar estas funciones antes y después de escribir, se consigue la sincronización apropiada. En lo que se refiere al programador de aplicaciones, puede verlo más bien como una sección crítica, con la excepción de que los hilos la adquieren para leer o para escribir.

Guía para programadores de clases seguras en entornos multihilo.

Si bien los capítulos posteriores cubren los detalles de la escritura de clases seguras en entornos multihilo, y los muchos beneficios y peligros en los que se puede incurrir cuando se diseñan clases seguras para entornos multihilo, me parece valioso incluir una serie de simples consejos que le ayudarán mucho.

¿Quién hace el bloqueo? Sé económico cuando bloquees recursos.

Page 83: Multithreading a la manera de Delphi

Sé tolerante con las fallas.La responsabilidad del bloqueo de clases seguras en

entornos multihilo puede ser del programador de la clase o del usuario de la clase. Si una clase provee sólo una funcionalidad simple, es normalmente lo mejor entregar esta responsabilidad al usuario de la clase. Seguramente usarán varias instancias de esta clase, y al darle la responsabilidad del bloqueo, un se asegura que los Deadlocks inesperados no ocurrirán, y uno también le da la posibilidad de elegir cuánto bloquea, de modo de maximizar la simplicidad o la eficiencia. Para clases más complicadas, como monitores, es normal que la clase (o grupo de clases) tome la responsabilidad, al ocultar las complejidades del objeto bloqueado del usuario final de la clase.

En todos los casos, los recursos deben ser bloqueados tan poco como sea razonablemente posible, y el bloqueo de recursos debe ser una tarea fina. Si bien los esquemas de bloqueo simplistas reducen las chances de un bug sea sutilmente insertado en el código, pueden en principio limitar sensiblemente los beneficios de usar hilos de ejecución. Por supuesto, no hay nada de malo con empezar haciéndolo simple, pero si hay problemas de performance, el esquema de bloqueo deberá ser examinado con mayor detalle.

Nada funciona perfectamente todo el tiempo. Si se usan las llamadas al API de Win32, tolera las fallas. Si sos del tipo de programadores que es feliz verificando millones de códigos de error, entones este es un enfoque posible. Alternativamente, podrás desear escribir una clase de abstracción que encapsule los objetos de sincronización Win32 que puedan llegar a emitir un mensaje de error cuando esto ocurra. En

Page 84: Multithreading a la manera de Delphi

cualquier caso, siempre ten en cuenta usar el bloque try … finally para asegurarte que en el caso de una falla, los objetos de sincronización son dejados en un estado conocido.

Administración de prioridades.

Todos los hilos son creados igual, pero algunos son más iguales que otros. El administrador de tareas debe dividir el tiempo del microprocesador entre todos los hilos en funcionamiento en la máquina en todo momento. Para hacer esto, necesita tener alguna idea de cuánto tiempo del microprocesador desearía usar cada hilo, y cuán importante es que un hilo en particular sea ejecutado cuando está disponible para correr. La mayoría de los hilos se comportan de dos maneras posibles: su tiempo de ejecución está atado al microprocesador o a la E/S.

Los hilos atados al microprocesador tienden a realizar un gran número de operaciones en segundo plano. Absorberán todos los recursos del microprocesador disponibles para ellos, y raramente se suspenderán para esperar por comunicaciones de E/S con otros hilos. Con bastante frecuencia, su tiempo de ejecución no es crítico. Por ejemplo, un hilo en un programa de gráficos por computadoras puede realizar una operación de manipulación de una imagen muy grande (difuminando o rotando la imagen), lo que puede tomar unos segundos o hasta minutos. En la escala de tiempos de los ciclos del procesador, este hilo no necesita nunca ser corrido con urgencia, ya que el usuario no se molesta si la operación toma doce o treinta segundos para ejecutarse, y ningún otro hilo en el sistema está esperando urgentemente un resultado de este hilo.

Page 85: Multithreading a la manera de Delphi

En el otro extremo de la escala de tiempo tenemos a los hilos atados a E/S. Estos normalmente no usan mucho el microprocesador, y pueden consistir en relativamente pequeñas cantidades de procesamiento. Con mucha frecuencia están suspendidos (bloqueados) en E/S, y cuando reciben una entrada, típicamente corren por un corto período de tiempo, para procesar esa entrada en particular, y en forma prácticamente inmediata se vuelven a suspender cuando no hay más entradas disponibles. Un ejemplo de esto es el hilo que procesa las operaciones de movimiento del ratón y actualiza la posición del cursor. Cada vez que el ratón es movido, el hilo se toma una pequeña fracción de segundos en actualizar el cursor y vuelve a ser suspendido. Hilos de este tipo tienden a ser más críticos con respecto al tiempo: no corren por largos períodos de tiempo, pero cuando corren, es bastante crítico que respondan de inmediato. En la mayoría de los sistemas GUI, es inaceptable que el cursor permanezca son responder, aún por cortos períodos de tiempo, y de hecho el hilo de actualización del cursor del ratón es crítico con respecto tiempo. Los usuarios de WinNT notarán que aún cuando la computadora esté trabajando muy duro en operaciones intensas en el microprocesador, el cursor del ratón sigue respondiendo inmediatamente. Todos los sistemas operativos multihilo que utilizan un mecanismo de preferencia, Win32 incluido, proveen soporte para estos conceptos, permitiéndole al programador asignar “prioridades” a los hilos. Típicamente, los hilos con mayor prioridad tienen a ser los atados a E/S y los hilos con menor prioridad, los que están atados al microprocesador. La implementación de las prioridades de los hilos de

Page 86: Multithreading a la manera de Delphi

ejecución en Win32 es ligeramente diferente de las implementaciones de (por ejemplo) UNIX, de modo que los detalles discutidos aquí son específicos para Win32.

¿Qué hay en una prioridad? El modo de hacerlo de Win32.

La mayoría de los sistemas operativos asignan una prioridad a los hilos de ejecución, para saber cuánta atención del microprocesador debe recibir cada hilo. En Win32, la prioridad de cada hilo de ejecución es calculada en el momento, a partir de un número de factores, algunos de los cuales pueden ser establecidos directamente por el programador. Estos factores son el “Priority Class” (clase de prioridad) del proceso, el “Priority Level” (nivel de prioridad) del hilo, y estos juntos son usados para calcular la “Base Priority” (prioridad base) del hilo, y la “Priority Boost” (prioridad de estímulo) en efecto para ese hilo. La prioridad del proceso es establecida en base al proceso en funcionamiento. Para casi todas las aplicaciones Delphi, esto será la clase de prioridad normal, con la excepción de los salvapantallas, que pueden ser establecidos a la clase de prioridad inactiva. En suma, el programador Delphi no necesitará cambiar la clase de prioridad de un proceso en funcionamiento. El nivel de prioridad de cada hilo puede ser establecido desde adentro de la clase asignada para el proceso. Esto suele ser mucho más útil, y el programador Delphi puede usar la llamada al API SetThreadPriority para cambiar el nivel de prioridad de un hilo. Los valores permitidos para esta llamada son: THREAD_PRIORITY_HIGHEST, THREAD_PRIORITY_ABOVE_NORMAL,

Page 87: Multithreading a la manera de Delphi

THREAD_PRIORITY_NORMAL, THREAD_PRIORITY_BELOW_NORMAL, THREAD_PRIORITY_LOWEST y THREAD_PRIORITY_IDLE. Como la prioridad base del hilo es calculada como resultado de ambos, el nivel de prioridad delhilo y la clase de prioridad del proceso. Hilos con niveles de prioridad por encima del normal en un proceso con una clase de prioridad normal tendrán una prioridad base mayor a los compuestos por un hilo con nivel de prioridad encima del normal pero en un proceso con una clase de prioridad por debajo de lo normal. Una vez que la prioridad base de un hilo fue calculada, este nivel permanece fijo mientras se ejecuta el hilo, o hasta que el nivel de prioridad (o la clase del proceso propietario) sea cambiado. Sin embargo, la prioridad actual usada de un momento a otro en el administrador de tareas cambia ligeramente como resultado de la prioridad de estímulo.

La prioridad de estímulo es un mecanismo que el administrador de tareas usa para probar y tomar cuenta del comportamiento de los hilos en tiempo de ejecución. Pocos hilos serán totalmente atados al microprocesador o a la E/S durante todo su funcionamiento, y el administrador de tareas fomentará la prioridad de los hilos que se bloquean sin llegar a usar por completo un bloque de tiempo asignado. Además, a los hilos que poseen manejadores de ventanas que están como ventanas en segundo plano también se les da un ligero fomento para probar y mejorar la respuesta al usuario.

¿De qué prioridad debo hacer mi hilo?

Page 88: Multithreading a la manera de Delphi

Con una básica comprensión de las prioridades, podemos intentar asignar prioridades realmente útiles a los hilos en nuestra aplicación. Ten en cuenta que, por defecto, el hilo de la VCL se ejecuta en un nivel de prioridad normal. Generalmente, la mayoría de las aplicaciones Delphi están enfocadas en proveer tanta capacidad de respuesta al usuario como sea posible, de modo que uno raramente necesita incrementar la prioridad del hilo por encima de lo normal – al hacerlo, demorará operaciones como el repintado de ventadas mientras el hilo esté en ejecución. La mayoría de los hilos que lidian con E/S o la transferencia de datos en las aplicaciones Delphi, pueden ser dejadas en una prioridad normal, ya que el administrador de tareas fomentará la prioridad del hilo cuando lo necesite, y si el hilo cambia a un estado en que acapara todo el microprocesador, perderá el fomento, resultando en una razonable velocidad de operación del hilo principal de la VCL. A la inversa, prioridades por debajo de lo normal pueden ser muy útiles. Si bajas la prioridad de un hilo que realiza operaciones intensas en el microprocesador en segundo plano, la máquina resultará para el usuario con mucha más capacidad de respuesta que si el hilo fuera dejado a un nivel de prioridad normal. Típicamente, un usuario es mucho más tolerante a sensibles demoras para que se completen las operaciones en hilos de ejecución de baja prioridad: podrá hacer otras cosas mientras se completan estas tareas, y la máquina lo mismo que la aplicación se mantendrán con una capacidad de respuesta normal.

Page 89: Multithreading a la manera de Delphi

Capítulo 9. Semáforos. Administración del flujo de datos. La relación productor - consumidor.

En este capitulo:

Semáforos. ¿Qué hay de los conteos por encima de uno?

Secciones “no tan críticas”. Un nuevo uso para los semáforos: administración

del flujo de datos y control de flujo. El buffer limitado. Una implementación Delphi del buffer limitado. Creación: Inicializando los semáforos

correctamente. Operación: valores correctos de espera. Destrucción: Liberando todo. Destrucción: Las sutilezas continúan. ¡Los accesos a los manejadores de sincronización

deben ser sincronizados! Administración de manejadores Win32. Una solución. Usando el buffer limitado: un ejemplo. Un par de puntos finales…

Semáforos.

Un semáforo es otro tipo de primitiva de sincronización, que es ligeramente más general que el mutex. Usado en el modo más simple posible, puede ser creado para operar del mismo modo que un mutex. En el caso general, le permite a un programa implementar un comportamiento de la sincronización más avanzado.

Page 90: Multithreading a la manera de Delphi

Primero que nada, reconsideremos el comportamiento de los mutexes. Un mutex puede estar marcado o sin marcar. Si esta marcado, un operación de espera en el mutex no provoca bloqueo. Si no esta marcado, una operación de espera en el mutex provoca bloqueo. Si el mutex no está marcado, entonces es propiedad de un hilo en particular, y además, sólo un hilo por vez puede poseer el mutex.

Los semáforos pueden ser creados para actuar precisamente de la misma manera. En lugar de tener el concepto de propiedad, un semáforo tiene un conteo. Cuando ese conteo es mayor que 0, el semáforo es marcado, y las operaciones de espera en él no producen bloqueos. Cuando la cuenta es 0, el semáforo no está marcado, y las operaciones de espera en él serán bloqueadas. Un mutex seria esencialmente un caso especial de semáforo cuya cuenta es sólo 0 o 1. De igual modo, los semáforos pueden ser pensados como fantásticos mutexes que pueden tener más de un propietario por vez. Las funciones en el API Win32 para lidiar con semáforos son muy similares a las que se usan para lidiar con mutexes.

CreateSemaphore. Esta función es similar a CreateMutex. En lugar de una marca indicando que el hilo que está creando el mutex quiere ser su propietario inicialmente, esta función toma un argumento indicando el conteo inicial. Crear un mutex con la propiedadinicial es similar a crear un semáforo con un conteo de 0: en ambos casos, cualquier hilo que espera por el objeto será bloqueado. Del mismo modo, crear un mutex sin la propiedad inicial es similar a crear un semáforo con un conteo de 1: en ambos casos, uno y sólo un

Page 91: Multithreading a la manera de Delphi

hilo no será bloqueado cuando espera para tomar posesión del objeto de sincronización.

Funciones de espera. Las funciones de espera son idénticas en ambos casos. Con mutex, una espera exitosa da la propiedad del mutex al hilo. Con semáforos, una espera exitosa decrementa el conteo del semáforo, o si el conteo es 0, bloquea el hilo en espera.

ReleaseSemaphore. Esto es similar a ReleaseMutex, pero en lugar de liberar la propiedad del objeto, ReleaseSemaphore toma un valor entero extra, como argumento para especificar en cuando debe ser incrementado el conteo. ReleaseSemaphorepuede incrementar el conteo en el semáforo, o activar el número apropiado de hilos bloqueados en el semáforo o ambos.

La siguiente tabla muestra como el código usando mutexes puede ser convertido en código usando semáforos, y las equivalencias entre ambos.

Mutexes Semáforos.MiMutex := CreateMutex(nil, FALSE, <name>);

MiSemaforo := CreateSemaphore(nil, 1, 1, <name>);

MiMutex := CreateMutex(nil, TRUE, <name>);

MiSemaforo := CreateSemaphore(nil, 0, 1, <name>);

WaitForSingleObject(MiMutex, INFINITE);

WaitForSingleObject(MiSemaforo, INFINITE);

ReleaseMutex(MiMutex);

ReleaseSemaphore(MiSemaforo, 1);

CloseHandle(MiMutex); CloseHandle(MiSemaforo);

Page 92: Multithreading a la manera de Delphi

Como un ejemplo sencillo, aquí están las modificaciones necesarias para el código presentado en el capítulo 6, de modo que el programa use semáforos en lugar de secciones críticas.

¿Qué hay de los conteos por encima de uno? Secciones “no tan críticas”.

Permitir que un semáforo tenga conteos mayores que uno es algo análogo a permitir que los mutexes tengan más de un propietario. Los semáforos permiten que sean creadas secciones críticas, lo que permite que un cierto número de hilos esté dentro de una región particular de código, o acceda a un objeto en particular. Esto es principalmente útil en situaciones donde un recurso compartido consiste de un número de buffers o un número de hilos, que pueden ser utilizados por otros hilos en el sistema. Tomemos un ejemplo concreto, y asumamos que hasta tres hilos pueden estar presentes en una región particular del código. Un semáforo es creado con un valor de conteo inicial y máximo de 3, asumiendo que ningún hilo está presente en la región crítica. La ejecución de cinco hilos intentando acceder a un recurso compartido puede verse como algo así:

Page 93: Multithreading a la manera de Delphi

Esta aplicación particular de los semáforos probablemente no sea muy útil para los programadores Delphi, principalmente porque hay muy pocas estructuras estáticamente dimensionadas en nivel de aplicación. Sin embargo, resulta considerablemente más útil dentro del SO, donde los manejadores, o recursos como buffers de un sistema de archivos suelen ser asignados estáticamente cuando arranca de la computadora.

Un nuevo uso para los semáforos: administración del flujo de datos y control de flujo.

Page 94: Multithreading a la manera de Delphi

En el capítulo 6, se perfilaba la necesidad de un control de flujo cuando se pasaban datos entre los hilos. Nuevamente, en elcapítulo 8, se habló de este tema cuando discutimos los monitores. Este capítulo hace un boceto de un ejemplo donde el control de flujo es frecuentemente necesario: un buffer limitado con un único hilo productor colocando ítems en el buffer, y un único consumidor, tomando ítems del buffer.

El buffer limitado.

El buffer limitado es representativo de una simple estructura de datos compartida que provee control de flujo así como datos compartidos. El buffer considerado aquí será una simple cola: Primero Entrado, Primero Salido. Será implementado como un buffer cíclico, es decir, contendrá un número fijo de entradas y tendrá un puñado de punteros “get” y “put” para indicar donde los datos deben ser insertados y removidos en el buffer. Hay típicamente cuatro operaciones permitidas en el buffer:

Create Buffer: El buffer y cualquier mecanismo asociado de sincronización son creados e inicializados.

Put Item: Este intenta colocar un ítem en el buffer de un modo seguro entre hilos. Si no es posible, porque el buffer está lleno, entonces el intento del hilo para colocar un ítem en el buffer es bloqueado (suspendido) hasta que el buffer esté en un estado que permita que sean agregados más datos.

Get Item: Este intenta tomar un ítem fuera del buffer en un modo seguro entre hilos. Si esto no fuera posible, porque el buffer está vacío, el intento del hilo por tomar un ítem será bloqueado

Page 95: Multithreading a la manera de Delphi

(suspendido) hasta que el buffer esté en un estado que permita que sean quitados datos.

Destroy Buffer: Esto desbloquea todos los hilos esperando en el buffer y destruye el buffer.

Obviamente, los mutexes no serán necesarios cuando se manipulan datos compartidos. Sin embargo, podemos usar semáforos para realizar las operaciones de bloqueo necesarias cuando el buffer está lleno o vacío, eliminando la necesidad de chequear rangos o aún conservar una cuenta de cuántos ítems hay en el buffer. Para hacer esto, necesitamos un pequeño cambio de mentalidad. En lugar de esperar por un semáforo y luego liberarlo cuando se realizan operan relacionadas con el buffer, usaremos el contador en el par de semáforos para tomar cuenta de cuántas entradas en el buffer están vacías o llenas. Llamemos a estos semáforos “EntriesFree” y “EntriesUsed”.

Normalmente, dos hilos interactúan en el buffer. El hilo productor (o escritor) intenta colocar ítems en el buffer, y el hilo consumidor (lector) intenta tomarlas afuera, como está representado en el diagrama siguiente. Un tercer hilo (posiblemente el hilo de la VCL) debería intervenir de modo de crear y destruir el buffer.

Page 96: Multithreading a la manera de Delphi

Como puede ver, los hilos lector y escritor se ejecutan en un bucle. El hilo escritor produce un ítem e intenta colocarlo en el buffer. Primero, el hilo espera en el semáforo EntriesFree. Si el conteo en EntriesFree es cero, el hilo será bloqueado, mientras el buffer está lleno y no se pueden agregar datos. Una vez que pasa esta espera, agrega un ítem al buffer y marca el semáforo EntriesUsed, de modo de incrementar la cuenta de las entradas en uso, y si fuera necesario, reanudando al hilo consumidor. De igual modo, el hilo consumidor se bloqueará si el conteo en EntriesFree es cero, pero cuando consigue tomar un ítem fuera del buffer, incrementa el conteo en EntriesFree, permitiéndole al hilo productor agregar otro ítem.

Bloqueando el hilo apropiado, ya fuera que el buffer se torne vacío o lleno, detiene a uno u otro hilo de

Page 97: Multithreading a la manera de Delphi

“pasarse de vueltas”. Dado un tamaño de buffer de N, el hilo productor puede estar sólo a N ítems de distancia del hilo consumidor antes de que éste sea suspendido, y de igual modo, el hilo consumidor no puede estar más de N ítems atrás. Esto nos trae algunos beneficios:

Un hilo no puede sobre-producir, de modo que se evita el problema visto en el capítulo 6, donde teníamos la salida de un hilo colocándose en cola en una lista de un tamaño cada vez mayor.

El buffer es de tamaño finito, a diferencia de la lista del enfoque visto anteriormente, de modo que podemos limitar mejor el uso de memoria.

No hay “esperas ocupadas”. Cuando un hilo no tiene nada que hacer, está suspendido. Esto evita las situaciones donde los programadores escriben pequeños bucles que no hacen nada más que esperar por más datos sin ser bloqueados. Esto debe ser evitado, ya que desperdicia tiempo del microprocesador.

Simplemente para hacer esto absolutamente claro, daré un ejemplo de la secuencia de eventos. Aquí tenemos un buffer con un máximo de 4 entradas en él, y es inicializado de modo que todas las entradas estén libres. Se pueden dar muchos caminos de ejecución, dependiendo del antojo del administrador de tareas, pero ilustraré el camino en el que cada hilo se ejecuta la mayor cantidad de tiempo posible antes de ser suspendido.

Acción del hilo lector

Acción del hilo escritor

Contador de

entradas libres

Contador de

entradas en uso

Page 98: Multithreading a la manera de Delphi

Comienza el hilo. Hilo inactivo (no activado)

4 0

Espera(EntriesUsed) bloquea. Suspendido.

  4 0

  Espera(EntriesFree) pasa.

3 0

 Agrega Item. Marca(EntriesUsed)

3 1

  Espera(EntriesFree) pasa

2 1

 Agrega Item. Marca(EntriesUsed)

2 2

  Espera(EntriesFree) pasa

1 2

 Agrega Item. Marca(EntriesUsed)

1 3

  Espera(EntriesFree) pasa

0 3

 Agrega Item. Marca(EntriesUsed)

0 4

 Espera(EntriesFree) bloquea. Suspendido.

0 4

Espera(EntriesUsed) completa.

  0 3

Quita Item.   1 3

Page 99: Multithreading a la manera de Delphi

Marca(EntriesFree)

Espera(EntriesUsed) pasa.

  1 2

Quita Item. Marca(EntriesFree)

  2 2

Espera(EntriesUsed) pasa.

  2 1

Quita Item. Marca(EntriesFree)

  3 1

Espera(EntriesUsed) pasa.

  3 0

Quita Item. Marca(EntriesFree)

  4 0

Espera(EntriesUsed) bloquea. Suspendido.

  4 0

Una implementación Delphi del buffer limitado.

Aquí está la primer implementación Delphi del buffer limitado. Como es acostumbrado, en la implementación aparecen un par de puntos que llevan una mención, y esto tiene algunos problemas que resolveremos luego.

¿Qué valores deben ser entregados a la llamada de creación del semáforo?

¿Qué tan larga debe ser la espera en el mutex o la sección crítica?

Page 100: Multithreading a la manera de Delphi

¿Qué tan larga debe ser la espera en el semáforo? ¿Cuál es la mejor manera de destruir limpiamente

al buffer?

Creación: Inicializando los semáforos correctamente.

Con esta implementación del buffer limitado, los datos son almacenados como una serie de punteros con índices de lectura y escritura en él. Para facilitar la depuración, he acordado que si el buffer tiene N entradas, será declarado lleno cuando sean llenadas N-1 entradas. Esto se hace con bastante frecuencia en los buffers cíclicos, donde los índices de lectura y escritura son evaluados para determinar si el buffer está absolutamente lleno, de modo que es común en el código de los buffers cíclicos tener siempre una entrada libre de modo que estas dos condiciones puedan ser distinguidas. En nuestro caso, como estamos usando semáforos, esto no es estrictamente necesario. Sin embargo, me adherí a esta convención de modo de facilitar la depuración.

Teniendo esto presente, podemos inicializar el semáforo EntriesUsed a 0. Como no hay entradas usadas, queremos que el hilo lector sea bloqueado inmediatamente. Dado que queremos que el hilo escritor agregue como máximo N-1 ítems al buffer, inicializamos EntriesFree a N-1.

También debemos considerar el conteo máximo permitido en los semáforos. El procedimiento que destruye el buffer siempre realiza una operación de MARCA en ambos semáforos. Entonces, como el buffer fue destruido, podía tener cualquier número de ítems en él, incluyendo estados completamente llenos o completamente vacíos. Establecemos el conteo

Page 101: Multithreading a la manera de Delphi

máximo a N, permitiendo una operación de marcado en el semáforo dados todos los estados posibles del buffer.

Operación: valores correctos de espera.

Usé mutexes en lugar de secciones críticas en esta pieza de software porque éstas le permiten al desarrollador un control fino sobre situaciones de error. Además, soportan situaciones de time-out. Los tiempos antes de que se disparen los time-out en las operaciones de espera deberían ser realmente infinitos; es posible que el buffer permanezca lleno o vacío por un largo período de tiempo, y necesitamos que el hilo esté bloqueado mientras el buffer esté vacío. Los programadores paranoicos o inseguros preferirán un time-out de unos pocos segundos en estas primitivas, para tener en cuenta situaciones donde un hilo se bloquea en forma permanente. Yo estoy bastante confiado en mi código como para no considerarlo necesario, al menos por el momento…

El time-out en el mutex es harina de otro costal (paisano de otro pueblo, sapo de otro pozo, etc.). Las operaciones dentro de la sección crítica son rápidas, hasta las N escrituras en memoria, y dado que N es bastante pequeño (es decir, menos de un millón), estas operaciones no deberían tomar más de 5 segundos. Como beneficio adicional, parte del código de limpieza adquiere este mutex, y en lugar de liberarlo, cierra el manejador. Al establecer un valor de time-out, esto nos asegura que los hilos esperando por el mutex se desbloquearán y devolverán un error.

Destrucción: Liberando todo.

Page 102: Multithreading a la manera de Delphi

Hasta ahora, la mayoría de los lectores han deducido que las operaciones de limpieza son habitualmente la parte más difícil de la programación multihilo. El buffer limitado no es la excepción. El procedimiento ResetState realiza esta limpieza. La primera cosa que hace es verificar el valor de FBufInit. He asumido que no es necesario ningún acceso sincronizado, ya que el hilo que crea el buffer también debe destruirlo. Y ya que FBufInit sólo es escrita por un solo hilo, y todas las operaciones de escritura ocurren en una sección crítica (al menos después de la creación), no habrá conflictos. Ahora, la rutina de limpieza necesita asegurarse que todos los estados son destruidos y que cualquier hilo que esté actualmente esperando o en el proceso de lectura o escritura, salga limpiamente, reportando fallas si fuera apropiado.

La operación de limpieza adquiere primero el mutex de los datos compartidos en el buffer, y luego desbloqueo a los hilos lector y escritor al liberar ambos semáforos. Las operaciones se realizan en este orden, porque cuando los semáforos son liberados, el estado del buffer no es más consistente: el conteo de los semáforos no refleja el contenido del buffer. Al adquirir el mutex primero, podemos destruir el buffer antes de que los hilos desbloqueados puedan leerlo. Al destruir el buffer y establecer FBufInit a falso, podemos asegurarnos de que los hilos desbloqueados devolverán un error, en lugar de la operación en los datos basura (por la inconsistencia del buffer).

Luego, desbloqueamos los dos hilos al liberar ambos semáforos, y entonces cerramos todos los manejadores de sincronización. Luego destruimos el mutex sin liberarlo. Esto está bien, porque como

Page 103: Multithreading a la manera de Delphi

todas las operaciones de espera en el mutex devolvieron time-out, podemos estar seguros de que ambos hilos lector y escritor serán desbloqueados eventualmente. Además, como sólo hay un hilo lector y uno escritor, podemos garantizar que ningún otro hilo pudo haber intentado una espera en el semáforo durante este proceso. Esto significa que una operación de marca en ambos semáforos será suficiente para activar ambos hilos, y como destruimos los manejadores de los semáforos mientras tuvimos propiedad del mutex, cualquier operación futura de lectura o escritura en el buffer fallará cuando intente esperar en alguno de los semáforos.

Destrucción: Las sutilezas continúan.

Se garantiza el funcionamiento de este código solamente con un hilo lector, un hilo escritor y un hilo de control. ¿Por qué?

Si existiera más de un hilo escritor o lector, más de un hilo podría estar esperando en alguno de los semáforos en algún momento. Si esto fuera así, podríamos no llegar a activar todos los hilos lectores o escritores que estén esperando en algún semáforo, cuando reiniciemos el estado del buffer. La primera reacción de un programador a esto sería modificar la rutina de limpieza para continuar marcando uno u otro semáforo hasta que todos los hilos hayan sido desbloqueados, haciendo algo comoesto. Por desgracia, esto es aún insuficiente, porque uno de los bucles de repetición en la limpieza podría terminar justo antes de que otro hilo entre en una operación de lectura o escritura esperando en un semáforo. Obviamente queremos alguna clase de

Page 104: Multithreading a la manera de Delphi

atomicidad, pero no podemos colocar las operaciones en el semáforo dentro de secciones críticas, porque los bloqueos de los hilos en los semáforos se apoyarán en las secciones críticas, y todos los hilos caerán en Deadlocks.

¡Los accesos a los manejadores de sincronización deben ser sincronizados!

La siguiente posibilidad podría ser hacer cero manejador del semáforo poco antes de “desenrollarlo”, haciendo algo como esto. Sin embargo, esto no mejora las cosas. En lugar de tener un problema de Deadlock, hemos introducido un sutil conflicto de hilos. Este conflicto en particular es un conflicto de escritura antes de lectura ¡en el propio manejador del un semáforo! Si… ¡también tienen que sincronizar sus objetos de sincronización! Que podría pasar si un hilo de procesamiento leyera el valor del manejador del mutex desde el objeto buffer, y es suspendido antes de hacer la llamada de espera, momento en el cuál el hilo de limpieza que está destruyendo el buffer marca el mutex la cantidad de veces necesaria, justo a tiempo para que el hilo de procesamiento sea activado y rápidamente realice una operación de espera ¡en el mutex que pensamos que había sido liberado recién! Es muy poco probable que estos sucesos se den así, pero de todos modos, es una solución inaceptable.

Administración de manejadores Win32.

Este problema es lo suficientemente espinoso que vale la pena ver qué pasa exactamente cuando

Page 105: Multithreading a la manera de Delphi

hacemos una llamada a Win32 para cerrar un mutex o un semáforo. En particular, es realmente útil saber:

Al cerrar un manejador, ¿se desbloquean los hilos esperando en ese mutex o semáforo en particular?

En el caso de los mutexes, ¿hace alguna diferencia si uno es propietario del manejador cuando libera el mutex?

Para determinar esto, podemos usar dos aplicaciones de prueba, una aplicación de prueba de mutex y una aplicación de prueba de semáforos. A partir de estas aplicaciones se puede determinar que cuando se cierra el manejador de un objeto de sincronización, Win32 no desbloquea ningún hilo que espere en ese objeto. Esto es así por consecuencia del mecanismo de conteo de referencia que usa Win32 para llevar cuenta de los manejadores: los hilos esperando en un objeto de sincronización pueden conservar el conteo de referencia interna desde que llega a cero y al cerrar el manejador del objeto de sincronización de la aplicación, todo lo que hacemos es perder todo lo parecido a tener un control sobre ese objeto de sincronización. En nuestra situación, esto es un verdadero fastidio. Idealmente, cuando se liberan los recursos, uno esperaría que un intento por esperar en un manejador cerrado desbloquearía los hilos que esperan en ese objeto de sincronización a través de ese manejador en particular. Esto permitiría al programador de la aplicación entrar en la sección crítica, liberar los datos en esa sección crítica, y luego cerrar el manejador, provocando el desbloqueo de los hilos esperando en él, con un valor de error apropiado (¿quizás WAIT_ABANDONED?).

Una solución.

Page 106: Multithreading a la manera de Delphi

Como resultado de esto, hemos determinado que cerrar los manejadores está bien, ya que los hilos no hacen una espera infinita en el manejador. Cuando aplicamos esto al buffer limitado, en el momento de limpieza, podemos garantizar el desbloqueo de los hilos esperando en los semáforos, solamente si sabemos cuántos hilos están esperando en el mutex. En general, necesitamos asegurarnos que los hilos no realizan una espera infinita en los mutex. Aquí hay un buffer reescrito, que puede arreglárselas con un número arbitrario de hilos. En él, las funciones de espera en los semáforos han sido modificadas, y a las rutinas de limpieza se les ha hecho pequeños cambios.

En vez de realizar una espera infinita en el mutex apropiado, el hilo lector y escritor llaman ahora a una función “Controlled Wait” (Espera controlada). En esta función, cada uno de los hilos espera en los semáforos sólo por una finita cantidad de tiempo. Esta espera por el semáforo puede devolver tres valores posibles, como es documentado en el archivo de ayuda de Win32.

WAIT_OBJECT_0 (Éxito) WAIT_ABANDONED WAIT_TIMEOUTPrimero que nada, si el semáforo es liberado, la

función devuelve WAIT_OBJECT_0, y no se requiere ninguna otra acción. En segundo lugar, en el caso donde la función WaitFor de Win32 devuelva WAIT_ABANDONED, la función devuelve error; este valor de error en particular indica que un hilo ha salido sin liberar apropiadamente un objeto de sincronización. El caso en el que estamos más

Page 107: Multithreading a la manera de Delphi

interesados es donde la espera devuelve time-out. Esto puede ser por dos razones posibles:

El hilo podría estar bloqueado por un largo período de tiempo.

El buffer interno fue destruido sin que se haya reactivado ese hilo en particular.

Para verificar esto, intentamos entrar a la sección crítica y verificar que la variable que indica si el buffer está inicializado continúa siendo verdadera. Si alguna de estas operaciones falla, entonces sabremos que el buffer interno fue reiniciado y la función termina devolviendo un mensaje de error. Si en cambio se puede verificar y la variable que indica si el buffer está inicializado es verdadera, volvemos al bucle, para esperar nuevamente por el mutex (en este caso, sólo fue una demora inesperada en un hilo).

La rutina de limpieza también fue ligeramente modificada. Ahora marca los dos semáforos y libera el mutex de la sección crítica. Al hacer esto, se asegura de que el primer hilo lector y escritor serán desbloqueados inmediatamente mas allá de que el estado del buffer sea reiniciado. Por supuesto, los hilos adicionales tendrían que esperar hasta el tiempo especificado de time-out antes de salir.

Usando el buffer limitado: un ejemplo.

Para facilitarle una estructura a este ejemplo, se concibió una simple aplicación usando dos hilos. Esta aplicación busca números primos palíndromos (palíndromos son los números que se leen igual de derecha a izquierda y de izquierda a derecha). Un par de primos palíndromos existirá cuando dos números, X e Y sean los dos primos, y además Y sea el

Page 108: Multithreading a la manera de Delphi

palíndromo de X. Ni X ni Y necesitan ser números palíndromos en sí mismos, sin embargo si uno de ellos lo es, entonces X = Y, lo que es un caso especial. Ejemplos de primos palíndromos incluyen: (101, 101), (131, 131), que son ambos casos especiales y (16127, 72161), (15737, 73751) y (15683, 38651), que no son casos especiales.

En esencia, los dos hilos (aquí está el código) realizan tareas bastante diferentes. El primer hilo (el hilo “adelantado”) busca números primos. Cuando encuentra alguno, lo coloca en el buffer limitado. El segundo hilo espera por las entradas en el buffer limitado. Cuando encuentra una entrada, la quita del buffer, invierte los dígitos, verifica si el número invertido es primo, y si es el caso, envía una cadena de texto con los dos números al formulario principal (aquí el código).

Si bien hay bastante código en este ejemplo, hay muy pocas cosas nuevas para discutir. Se le recomienda al lector echarle un vistazo a los métodos execute de cada hilo, ya que estos proveen una visión bastante clara de lo que está pasando. La transferencia de datos del segundo hilo al hilo de la VCL, y el formulario principal de la VCL, es como se discutió en los capítulos anteriores. Y el último punto para preocuparnos es… ¡lo has adivinado! Liberación de recursos y limpieza.

Un par de puntos finales…

¿Y pensabas que no se podía decir nada más acerca de la destrucción? Hay un tema final para mencionar. El código del buffer limitado asume que los hilos intentarán acceder a los campos del objeto después

Page 109: Multithreading a la manera de Delphi

de que el buffer haya sido destruido. Esto está bien, pero significa que cuando destruimos los dos hilos y el buffer entre ellos, debemos reiniciar el estado del buffer, luego esperar a que todos los hilos terminen, y sólo entonces liberar el buffer, liberando la memoria que ocupa el objeto. Fallar al hacer esto, pueden resultar en violaciones de accesos. La función StopThreads hace esto correctamente, asegurando una salida limpia.

Tampoco hay algo trascendente en el hecho de que otra sincronización dada salga con el procedimiento SetSize. En el ejemplo, he asumido que el buffer está establecido, una vez y para todos, antes de que cualquier hilo use el buffer. Podría establecer el tamaño del buffer cuando está en uso. Esto es generalmente una mala idea, ya que significa que, si más de dos hilos están usando el buffer, uno escritor y uno lector, podrían detectar incorrectamente la destrucción del buffer. Si el buffer debe ser redimensionado, entonces todos los hilos que usan el buffer deberían ser, o bien terminados, o bien suspendidos en un punto que se sabe que es seguro. Entonces el buffer debe ser redimensionado y los hilos consumidor y productor reiniciados. Los programadores ambiciosos podrían desear escribir una versión del buffer que maneje las operaciones de redimensionado en forma transparente.

Capítulo 10. E/S y flujo de datos: del bloqueo a lo asincrónico, ida y vuelta.

En este capítulo:

Page 110: Multithreading a la manera de Delphi

Diferencias en los hilos VCL y diseño de interfaces de E/S.

Mapa de ruta. Implementando una conversión de bloqueo a

asincrónico. Agregando operaciones de observación en el buffer

limitado. Creando un buffer limitado bi-direccional. El buffer de bloqueo a asincrónico en detalle. Construcción del BBA (Buffer de Bloqueo a

Asincrónico) Destrucción del BBA. Un ejemplo de programa usando el BBA. ¡Hemos alcanzado nuestro objetivo! ¿Has notado el agujero en la memoria? Evitando agujeros en la memoria. Problemas al echar un vistazo en el buffer. Haciendo a un lado el buffer intermedio. Miscelánea de limitaciones. La otra cara de la moneda: buffer de flujos de

datos.

Diferencias en los hilos VCL y diseño de interfaces de E/S.

Con hilos de procesamiento, tiene sentido el bloqueo de E/S, ya que en general, el bloqueo de E/S es el más sencillo. Desde el punto de vista de un hilo usando un recurso de E/S a través de llamadas con bloqueo, el éxito o fracaso es inmediatamente evidente después de hacer la llamada de E/S, y la lógica del programa nunca debe preocuparse acerca del período de tiempo entre las operaciones de E/S que están siendo invocadas ni cuando serán completadas.

Page 111: Multithreading a la manera de Delphi

Las operaciones que involucran al hilo de la VCL, no se les suele permitir bloqueos por largos períodos de tiempo: el hilo debe estar siempre disponible para procesar nuevos mensajes con una demora mínima. En general, la E/S a disco tiende a usar bloqueo, ya que las demoras involucradas son cortas desde el punto de vista del usuario, pero todas las operaciones de E/S tienden a ser asincrónicas, especialmente las operaciones que involucran comunicación entre hilos, procesos o maquinas, ya que el tiempo de demora involucrado en la operación no puede ser conocido de antemano. El beneficio de las operaciones asincrónicas, como fue discutido anteriormente, es que el hilo de la VCL siempre permanece con capacidad para responder a nuevos mensajes. La principal desventaja es que el código que se ejecuta en el hilo de la VCL tiene que desconocer el estado de evolución de todas las operaciones de E/S pendientes. Esto puede volverse un poco complicado, y significar el almacenamiento de grandes cantidades potenciales de estados. Algunas veces, esto involucra construir una máquina de estados; especialmente cuando se implementan protocolos bien definidos como HTTP, FTP o NNTP. Con mayor frecuencia, el problema es simple, y se puede resolver de igual manera. En estos casos, una solución bajo demanda será suficiente.

Cuando diseñamos un grupo de funciones de transferencia de datos, esta diferencia debe ser tenida en cuenta. Tomando las comunicaciones como un ejemplo, el más frecuente grupo de operaciones soportas en un canal de comunicación son: Open,Close, Read y Write. Las interfaces de bloqueo de E/S ofrecen estas facilidades como funciones simples. Las interfaces asincrónicas

Page 112: Multithreading a la manera de Delphi

ofrecen cuatro funciones básicas, y además, proveen hasta cuatro notificaciones, ya sea por call-back o porevento. Estas notificaciones indican que  una operación previa que estaba pendiente se ha completado, o que es posible repetir la operación o un mezcla de ambas. Un ejemplo de interfaz podría ser:

Una función Open y su evento asociado OnOpen, que indica que la apertura se ha completado, y reporta el éxito o fracaso de la operación.

Una función Read y su evento asociado CanRead (o OnRead). El evento típicamente indica que una llamada a Read leerá algunos datos nuevos, y/o que algún otro dato ha llegado desde la última lectura.

Una función Write, y su evento asociado CanWrite (o OnWrite). El evento típicamente indica que una llamada a Write escribirá mas datos, y/o que algunos de los datos en la escritura anterior fueron enviados, y ya hay espacio libre en el buffer para más operaciones Write. Dependiendo de la semántica, este evento puede o puede no ser disparado después de una llamada a Open que haya tenido éxito.

Una función Close, y su evento asociado OnClose. El evento típicamente indica que el canal de comunicación fue cerrado finalmente, y ningún otro dato puede ser enviado o recibido. Este evento normalmente existe en situaciones donde es posible leer datos de la otra punta del canal de comunicación después de haber llamado a Close, y suele funcionar bien para establecer y romper comunicaciones con mecanismos que usan autentificación de tres vías (por ejemplo, TCP).

Page 113: Multithreading a la manera de Delphi

Mapa de ruta.

Antes de seguir adelante en este capítulo, parece apropiado revisar los mecanismos existentes para transferencia de datos entre hilos y hacer un bosquejo de los métodos por los que se extenderán. Sin más, se podría persuadir a algunos lectores a completar este capítulo sin dejar de leer, mas allá del hecho de que hay un montón de código para estudiar. El punto más importante en esta coyuntura es que muchos de los detalles de implementación, al tiempo que son útiles para aquellos que quieran escribir programas funcionales que incluyan estas técnicas, no son de prima importancia para quienes desean tener un conocimiento general de los conceptos descriptos. Hasta ahora, el único mecanismo de transferencia que hemos visto es el buffer limitado, representado en el siguiente diagrama:

En este capítulo se mostrarán varias extensiones a este buffer. El primer puñado de modificaciones será bastante simple: colocar dos buffer vuelta y vuelta, y agregar una operación sin bloqueo en ambos lados del buffer bi-direccional resultante.

Hasta ahora vamos bien. Esto no debería ser ninguna sorpresa para cualquier lector en este punto,

Page 114: Multithreading a la manera de Delphi

y todos los que han seguido este tutorial hasta aquí no deberían tener problemas en implementar este tipo de construcción. La siguiente modificación es un poco más ambiciosa: en lugar de hacer todas las lecturas y escrituras en el buffer mediante bloqueos de buffer, haremos una serie de operaciones asincrónicas.

Específicamente, crearemos un componente que convierta operaciones de bloqueo en asincrónicas y viceversa. En su personificación natural, simplemente encapsulará operaciones de lectura y escritura en el buffer bi-direccional, pero implementaciones futuras pueden sobrescribir esta funcionalidad para conertir diferentes operaciones de E/S entre semánticas de bloqueo y asincónicas.

La pregunta aquí es: ¿Porqué? La respuesta debería ser obvia: Si podemos hacer un buffer que provea comunicación bi-direccional entre dos hilos, donde un hilo usa operaciones de bloqueo, y el otro usa operaciones asincrónicas, entonces:

Page 115: Multithreading a la manera de Delphi

Podemos usarlo para comunicaciones entre el hilo de la VCL y el hilo de procesamiento en nuestra aplicación sin bloquear el hilo de la VCL.

Todas las complejidades quedarán ocultas dentro del código del buffer: ningún número mágico, ni uso de sincronize, ni secciones críticas visibles públicamente.

Realizará control de flujo entre el hilo de la VCL y el hilo de procesamiento; una tarea que aún no es posible hacer.

Podría ser usado como una solución “en sí misma” para comunicaciones entre el hilo de la VCL y otros hilos por cualquier persona que no tiene idea de los problemas de sincronización.

Implementando una conversión de bloqueo a asincrónico.

El Componente que crearemos asume que un solo hilo de la VCL está ejecutándose, y por consiguiente, una interfaz asincrónica será provista para solamente un hilo. Las operaciones de bloqueo provistas por este buffer funcionarán con exactamente las mismas limitaciones que las presentadas para el ejemplo del buffer limitado en el capítulo anterior, y por ende, cualquier número de hilos con bloqueo podrán acceder a la interfaz de bloque en forma concurrente. Del mismo modo que el buffer limitado permite simples operaciones de Tomar (Get) y poner (Put), involucrándose solamente un elemento, el bloqueo a un buffer asincrónico (también llamado el “BAB”) también permitirá simples operaciones que involucran un solo elemento. La semántica de la interfaz será:

Creación: En el momento de la creación, el componente BAB creará las estructuras de datos

Page 116: Multithreading a la manera de Delphi

internas y los hilos requeridos por el buffer y generará eventos de OnWrite para indicar que los datos pueden ser escritos al buffer por el hilo principal de la VCL.

Lectura: El componente BAB proveerá dos funciones de lectura: BlockingRead (lectura por bloqueo) y AsyncRead (lectura asincrónica). BlockingRead será usada por hilos de procesamiento, mientras que AsyncRead la usará el hilo de la VCL.

Notificaciones de Lectura: El BAB proveerá un evento OnRead al hilo principal de la VCL cuando una operación de lectura asíncrona se pueda ejecutar bien, vale decir, cuando los datos están aguardando para que el hilo de la VCL los lea.

Escritura: El BAB proveerá dos funciones; BlockingWrite (escritura por bloqueo) y AsyncWrite (escritura asincrónica). BlockingWrite será usado por los hilos de procesamiento, mientras que AsyncWrite será usado por el hilo de la VCL.

Notificaciones de escritura: El BAB proveerá un evento OnWrite al hilo principal de la VCL cuando se pueda ejecutar correctamente una operación de escritura, es decir, hay suficiente espacio libre en el buffer en el que podría ser escrito un item. Nuevamente, una relación uno a uno se mantiene entre las notificaciones y las escrituras exitosas, y el hilo de la VCL debe intentar hacer exactamente una escritura antes de esperar por otra notificación.

Operaciones de observación: Cualquier hilo podrá echar una mirada al buffer para saber cuantas entradas están vacías o usadas en el buffer en una cierta dirección. Esta operación podría ser

Page 117: Multithreading a la manera de Delphi

muy útil para el hilo de procesamiento para determinar si una operación de BlockingRead o BlockingWrite va realmente a producir un bloqueo. El hilo de la VCL no debe usar estas funciones para determinar si una lectura o escritura se va a producir con éxito, y debe en cambio depender de las notificaciones.

Agregando operaciones de observación en el buffer limitado.

Aquí hay una mejora al buffer limitado para permitir operaciones de observación. Nótese que si bien es posible leer la cuenta que lleva los semáforos durante ciertas operaciones, preferí mantener estos conteos manualmente usando un par de variables extra FEntryCountFree y FEntryCountUsed. Un par de métodos extra fueron provistos para leer estas variables. Muchos programadores Delphi pensarán inmediatamente en exponer estos atributos del buffer limitado como propiedades. Desgraciadamente, necesitamos tener en mente que las operaciones de sincronización necesarias para acceder esas variables podrían fallar. En lugar de devolver conteos de -1 en una propiedad Integer, parece más apropiado dejar las operaciones de observación como funciones, informando al programador que requiere algún trabajo acceder a los datos requeridos, y que la función podría fallar. Algunos podrían argumentar que, siguiendo este razonamiento, también se debería programar la lectura del atributo Size (tamaño) del buffer como una función explícita de lectura. Esto es más que nada un tema de estilo, ya que el tamaño del buffer puede ser leído directamente sin que se necesite algún tipo de sincronización.

Page 118: Multithreading a la manera de Delphi

Creando un buffer limitado bi-direccional.

Esta operación es casi trivial y no requiere explicaciones complejas. Lo he implementado como una simple encapsulación de dos objetos buffer limitados. Todas las operaciones soportadas por el buffer limitado también son soportadas por el buffer limitado bi-direccional, con la pequeña modificación que el hilo usando este objeto debe especificar con que lado del buffer desea operar. Típicamente, un hilo opera con el lado A, y el otro con el lado B. Aquí está el código fuente. Esta clase implementa la funcionalidad descrita pictóricamente en el diagrama de abajo representando el buffer limitado bi-direccional.

El buffer de bloqueo a asincrónico en detalle.

Habiendo hecho todo el trabajo previo de preparación, ahora puede ser explicado el BAB con mayor detalle. El BAB posee un buffer bi-direccional y dos hilos, uno lector y otro escritor. Los hilos de lectura y escritura realizan operaciones de lectura y escritura en el buffer limitado en nombre del hilo de la VCL. La ejecución de todos estos hilos puede ser representada pictóricamente, con sólo un mínimo abuso de las convenciones existentes:

Page 119: Multithreading a la manera de Delphi

Este diagrama se ve un poco intimidatorio; quizá resulte más fácil de entender si presentamos un ejemplo de funcionamiento. Vamos a considerar el caso en el que el hilo de procesamiento realiza una escritura por bloqueo en el BAB.

1. El hilo de procesamiento hace una escritura por bloqueo.

2. El hilo de lectura del BAB está actualmente bloqueado, tratando de leer del buffer bi-direccional. Como resultado de la escritura, éste se desbloquea y puede leer el buffer.

Page 120: Multithreading a la manera de Delphi

3. El hilo copia los datos leídos en un buffer intermedio y local para la clase hilo, y dispara un evento de flujo de datos, manejado por el BAB.

4. El código de manejo del flujo de datos del BAB, ejecutándose en el contexto del hilo de lectura, envía un mensaje a su propio manejador de ventanas indicando que los datos fueron leídos por el hilo de lectura.

5. El hilo de lectura espera entonces en un semáforo que indicará que los datos fueron leídos por el hilo principal de la VCL.

6. En algún momento posterior, el hilo principal de la VCL procesa los mensajes pendientes para el componente, del mismo modo que lo hace para todos los componentes con un manejador de ventanas.

7. Entre estos mensajes que esperan por el componente está el mensaje de notificación enviado por el hilo de la VCL. Este mensaje es manejado y genera un evento de OnRead para el componente.

8. El evento OnRead es manejado por la lógica del resto de la aplicación (probablemente por el formulario principal) y esto resultará seguramente en que el hilo de la VCL intente leer datos.

9. El hilo de la VCL llamará el método AsyncRead del BAB.

10. AsyncRead copia los datos desde el buffer interno y se los devuelve al hilo de la VCL. Este entonces libera el semáforo en el que está bloqueado el hilo de lectura, permitiéndole intentar y realizar otra operación de lectura en el buffer bi-direccional.El BAB funciona exactamente de la misma manera

cuando escribe. La escritura es realizada

Page 121: Multithreading a la manera de Delphi

asincrónicamente por el hilo de la VCL, el hilo de escritura interno del BAB es reactivado y realiza una escritura por bloqueo en el buffer bi-direccional, y una vez que esa escritura se completa, el hilo de la VCL es notificado por un evento que puede intentar más operaciones de escritura.

En esencia, la interfaz entre operaciones de bloqueo y asincrónicas a través del envío de mensajes es idéntico al introducido informalmente en ejemplos anteriores. La diferencia con este componente es que los detalles son encapsulados para el usuario final, y el problema es resuelve de un modo más formal y de una manera mejor definida.

Aquí está el código para este componente. Algunos puntos pueden ser destacados provechosamente. En suma, el descendiente de TThread hace poco uso de la herencia. Sin embargo, en este caso particular, el hilo lector y escritor tienen una gran cantidad de funcionalidad en común, lo que es implementado en la case base TBlockAsyncThread. Esta clase contiene:

El buffer intermedio, que guarda sólo un único puntero.

Una sección crítica para permitir un acceso atómico al buffer interno.

Un puntero al buffer bi-direccional para usarlo en operaciones de bloqueo. Este es fijado por el BAB al buffer bi-direccional usado internamente en el BAB.

Un evento “OnDataFlow” que es manejado por el componente BAB.

Un semáforo inactivo. Este semáforo es usado para implementar las operaciones “Wait for VCL write” (espera para la escritura de la VCL) y “Wait for

Page 122: Multithreading a la manera de Delphi

VCL read”  (esperar a la lectura de la VCL) de una manera genérica.

La case base del hilo también implementa las imprescindibles funcionalidades comunes: creación del hilo, destrucción, y el disparador del evento OnDataFlow. La clase base tiene dos hijos: TBAWriterThread y TBAReaderThread. Estas implementan los métodos actuales de ejecución de los hilos y también proveen métodos de lectura y escritura que serán ejecutados en forma indirecta por el hilo de la VCL. El componente BAB en sí mismo almacena el buffer bi-direccional y los dos hilos. Además, almacena el manejador de ventana FHWND, que es usado para el procesamiento especializado de mensajes.

Construcción del BAB

Echémosle ahora un vistazo a la implementación. Desde la creación, el componente BAB asigna un manejador de ventanas usando AllocateHWnd. Esta es una función muy útil mencionada en el libro de Danny Thorpe, “Delphi Component Design”. El componente BAB es un poco inusual en el sentido de que necesita un manejador de ventanas para realizar el procesamiento de mensajes, aunque no es realmente un componente visual. Se puede dar la componente BAB un manejador de ventanas haciéndolo hijo de un TWinControl. Sin embargo, éste no es el padre apropiado para el componente, por no es una ventana de control. Usando AllocateHWnd, el componente puede ejecutar su propio procesamiento de mensajes sin cargar también con una gran cantidad de cosas extra que no necesita. También hay una pequeña mejora de eficiencia, ya que el

Page 123: Multithreading a la manera de Delphi

procedimiento de manejado de mensajes en el componente realiza una mínima cantidad de procesamiento requerido, lidiando con un mensaje en particular e ignorando el resto.

Durante la creación, el componente BAB también inicializa una serie de manejadores de eventos desde los hilos del componente mismo. Estos manejadores de eventos se ejecutan en el contexto de los hilos lectores y escritores, y realizan la notificación publicando estas interfaces entre los hilos lectores y escritores y el hilo principal de la VCL.

Como resultado de la creación del componente, los hilos se inicializan. Todo el trabajo aquí es común a ambos hilos lectores y escritores y, de igual modo, en el constructor del TBlockAsyncThread. Esto simplemente inicializa una sección crítica necesaria para mantener un acceso atómico al buffer intermedio en cada hilo, y éste también crea el semáforo inactivo para cada hilo, que asegura que el hilo de procesamiento esperará al hilo de la VCL antes de leer o escribir algún dato.

Destrucción del BAB.

La destrucción del componente es ligeramente más complicada, pero usa principios que ya discutimos en capítulos anteriores. El buffer bi-direccional contenido en el BAB es similar al buffer limitado discutido en capítulos anteriores, en el sentido de que la destrucción es un proceso compuesto por tres etapas. La primera etapa es desbloquear todos los hilos que realizan operaciones de E/S en el buffer, mediante una llamada a ResetState. La segunda etapa es esperar a que todos los hilos terminen, o por

Page 124: Multithreading a la manera de Delphi

lo menos estén en un estado en el que no puedan realizar ninguna otra operación en el buffer. Una vez que esta condición se haya alcanzado, la tercera etapa comienza, que es la destrucción de las estructuras físicas de datos.

El modo de destrucción del BAB en líneas generales:

El estado del BAB es reiniciado. Esto involucra terminar los dos hilos internos, y luego reiniciar el estado del buffer bi-direccional, desbloqueando cualquier operación en el buffer que esté en progreso.

El destructor para ambos hilos es llamado. Esto desbloquea en cada hilo sus semáforos inactivos, y luego espera a que el hilo se complete antes de destruir la sección crítica y el semáforo inactivo. Algunos lectores pueden sorprenderse de que un destructor de un hilo pueda llamar a WaitFor. Esto está bien, ya que podemos estar seguros de que un hilo nunca llamará a su propio destructor. En este caso, el destructor para los hilos lector y escritor será llamado por el hilo de la VCL, de modo que no hay problema de Deadlock.

Los hilos lector y escritor son puestos a nil para permitir múltiples llamadas a ResetState.

El buffer bi-direccional es destruido, y el manejador de ventana es liberado.

Ya que los hilos son internos del BAB, estos procedimientos de limpieza se ejecutan de modo que el BAB puede desbloquear y liberar todos los hilos y objetos de sincronización internos del componente sin que el usuario del componente ni siquiera tenga que preocuparse por los problemas potenciales de ordenamiento inherentes a la operación de limpieza.

Page 125: Multithreading a la manera de Delphi

Una simple llama al Free del BAB será suficiente. Esto es obviamente deseable.

Mas allá de esto, el componente todavía expone su método ResetState. La razón para esto es que el componente no tiene control sobre los hilos en funcionamiento que pueden realizar operaciones por bloqueo en el buffer. En situaciones como estas, la aplicación principal debe terminar los hilos de procesamiento, reiniciar el estado del BAB y esperar a que el hilo de procesamiento termine antes de destruir físicamente el BAB.

Un programa de ejemplo usando el BAB.

Aquí hay una nueva variante del tema de los números primos. El formulario principal le pide al usuario dos números –el número de comienzo y fin de un rango. Estos números son dados, colocados en una estructura de pedido, y un puntero hacia esta estructura es escrito asincrónicamente en el BAB. En algún momento posterior, el hilo de procesamiento realizará una lectura por bloqueo y tomará el pedido. Entonces tomará una cantidad variable de tiempo procesando el pedido, determinando cuáles números en el rango son primos. Una vez que ha terminado, realiza una escritura por bloqueo, pasando el puntero a una lista de strings con los resultados. El formulario principal es notificado que hay datos listos para leer, y entonces leer la lista de strings desde el BAB y copia los resultados en un memo.

Hay dos puntos principales para notar en el formulario principal. La primera es que la interfaz de usuario es actualizada en forma elegante alineada

Page 126: Multithreading a la manera de Delphi

con el control de flujo del buffer. Una vez que un pedido es generado, el botón de pedido es deshabilitado. Solamente es re-habilitado cuando recibe un evento OnWrite del BAB indicando que se pueden escribir más datos en forma segura. La implementación actual establece el tamaño del buffer bi-direccional a 4. Esto es suficientemente pequeño como para que el usuario pueda verificar que luego de enviar cuatro pedidos que tomen mucho tiempo en procesar, el botón permanece deshabilitado permanentemente hasta que uno de los pedidos sea procesado. Del mismo modo, si el formulario principal no puede procesar notificaciones de lectura lo suficientemente rápido desde el BAB, el hilo de procesamiento permanecerá bloqueado.

El segundo punto para notar es que cuando el formulario es destruido, el destructor usa el método ResetState del BAB como fue descripto anteriormente para asegurarse que se limpia el hilo y la liberación del buffer se produce de manera ordenada. Una falla en esto podría resultar en una violación de acceso.

El código del hilo de procesamiento es bastante simple. No es muy interesante, ya que usa operaciones de lectura y escritura por bloqueo, sólo usa la CPU cuando está procesando un pedido: si no puede recibir un pedido o enviar una respuesta, debido a una congestión en el buffer, entonces está bloqueado.

¡Hemos alcanzado nuestro objetivo!

Un pequeño resumen de las cosas que hemos conseguido con este componente:

Page 127: Multithreading a la manera de Delphi

Una transferencia de datos armoniosa entre el hilo de la VCL y los hilos de procesamiento.

Todos los detalles de sincronización quedaron ocultos dentro del BAB (con la excepción de los detalles de ResetState).

Un completo control de flujo entre el hilo de la VCL y los hilos de procesamiento.

Ningún ciclo ocupado o sometido: la CPU es usada con eficiencia.

Ningún uso de synchronize. Los hilos no son bloqueados en forma innecesaria.

El lector podría haber olvidado que estos problemas existían…

¿Has notado el agujero en la memoria?

Durante los dos capítulos anteriores y este capítulo, un aspecto importante fue dejado de lado; los ítems en los varios buffer que hemos diseñado no se destruyen de forma apropiada cuando el buffer es destruido. Cuando diseñamos inicialmente estas estructuras de buffer, adoptamos una implementación similar a TList: la lista o buffer simplemente provee almacenamiento y sincronización. La correcta asignación y liberación del objeto es responsabilidad del hilo usando el buffer.

Esta implementación simplista tiene dificultades mayores. En el más común de los casos, es excesivamente difícil asegurarse que el buffer está vacío en ambas direcciones antes de que sea destruido. En el ejemplo de arriba, que es el uso más simple posible del buffer, hay cuatro hilos, cuatro mutex o secciones críticas, y seis semáforos en el sistema completo. Determinar el estado de todos los hilos y orquestar una salida con una limpieza

Page 128: Multithreading a la manera de Delphi

perfecta, en estas situaciones es obviamente imposible.

En el programa de ejemplo, esto fue resuelto conservando el conteo de cuántos pedidos hay sin responder en cualquier momento. Si hemos recibido tantas respuestas como pedidos hemos hecho, podemos estar seguros de que los varios buffer están vacíos.

Evitando agujeros en la memoria.

Un enfoque sería permitir que los buffer implementen call-backs que destruyan los objetos que contienen al momento de la limpieza. Esto funcionaría en el caso general, pero abre una puerta al abuso, y el uso de implementaciones de este tipo suelen terminar siendo difícil en la práctica.

Otra posibilidad es tener un esquema de administración general del buffer que lleva cuenta de los tipos específicos de objetos, tomando cuenta cuando entran y salen de los varios buffer en la aplicación. Una vez más, una implementación de este tipo se volvería difícil y requeriría un mecanismo de seguimiento potencialmente complicado para hacer un trabajo que realmente debería ser simple.

La mejor solución es hacer la estructura de los buffers análogas a TObjectList; es decir, todos los itemas colocados en el buffer son clases. Esto permitirá a los hilos realizar la operación de limpieza llamando al destructor apropiado en todos los ítems del buffer. Aún mejor, usando tipos de referencia de clases, podemos realizar verificaciones automáticas en tiempo de ejecución en los objetos pasados a

Page 129: Multithreading a la manera de Delphi

través del buffer, y producir un paquete de buffers de tipos seguros.

La implementación de un esquema semejante se deja como ejercicio para el lector. Ningún cambio es necesario para el mecanismo básico de sincronización, pero la armonía en los procedimientos de lectura y escritura necesitarán modificaciones, como también lo necesitarán el destructor del buffer limitado, y el de las clases hilo.

Problemas al echar un vistazo en el buffer.

Cuando implementamos el buffer bi-direccional, todavía era posible proveer un mecanismo razonablemente constante para echar un vistazo a los buffer y ver cuántos ítems hay en ellos. Es posible que cuando le echemos un vistazo al buffer bi-direccional, la liberación y el conteo de uso no se agregue a la misma figura, ya que ambas operaciones no pueden realizarse atómicamente. Sin embargo, se asegura que con sólo un hilo lector y uno escritor en cada dirección, las ojeadas pueden ser utilizadas como indicación razonable de que una operación tendría éxito sin el bloqueo.

Con el buffer asincrónico, el problema es peor en el sentido de que no es posible asegurarse una buena mirada en el estado del buffer con la implementación actual. Esto es así porque hay esencialmente dos buffer en cada dirección, el buffer limitado y el interno, que almacena un solo ítem. Ningún mecanismo es provisto para bloquear globalmente ambos buffer y, en una operación atómica, determinar el estado de los dos.

Page 130: Multithreading a la manera de Delphi

El componente un tajo al proveer alguna posibilidad de mirar al llevar una cuenta rigurosa de los ítems en transito en el buffer. ¡Esto es tan deliberadamente vago que no se puede ni engañar al programador haciéndolo pensar que los resultados podrían ser exactos! ¿Es posible hacerlo de otra manera?

Haciendo a un lado el buffer intermedio.

La mejor manera de superar la situación es quitando completamente el buffer intermedio. Si nos detenemos a pensar un momento, esto es posible, pero requiere la reescritura de todo el código para el buffer. Necesitaremos implementar un nuevo buffer limitado con una semántica ligeramente diferente. El nuevo buffer deberá:

Implementar una lectura y escritura por bloqueo en un extremo, como el anterior.

En el otro extremo, implementar una lectura y escritura asíncrona (un simple éxito/fracaso sin bloqueo), y además implementar un par de métodos “Block Until” (bloquear hasta que se dé una condición). Estos métodos bloquearán un hilo hasta que a una operación de lectura o escritura pueda asegurársele el éxito.

De esta manera, los hilos lector y escritor pueden ser usados para enviar notificaciones por bloqueo hasta que una operación sea posible, y el hilo de la VCL pueda realizar la operación de lectura y escritura sobre el buffer limitado, sin bloqueo.

Con esta semántica, sólo tenemos un grupo de buffer que deben ser administrados, y es comparativamente más fácil proveer una operación para ver el estado del buffer que provea resultados

Page 131: Multithreading a la manera de Delphi

exactos. Un vez más, esto se deja como ejercicio para el lector…

Miscelánea de limitaciones.

Todas las estructuras del buffer introducidas en los últimos capítulos han asumo que el programador envía punteros a direcciones de memoria válidas, y no NIL. Algunos lectores puede que hayan notado que parte del código en los hilos lector y escritor asumen implícitamente que NIL es un valor null válido que no será enviado a través del buffer. Esto podría naturalmente ser solucionado con algunas marcas de validación en el buffer, pero a costa de que el código quede un poco desprolijo.

Una limitación más teórica es que el usuario final de este componente podría crear una gran cantidad de buffer. La guía de programación de Win32 para la programación con hilos establece que generalmente es una buena idea limitar el número de hilos de procesamiento a alrededor de dieciséis por aplicación, lo que podría permitir ocho componentes BAB. Ya que no hay limitación en el número de hilos de procesamiento que pueden realizar operaciones de bloqueo en el BAB, parece apropiado tener sólo un BAB por aplicación y usarlo para comunicarse entre un hilo VCL y todos los hilos de procesamiento. Esto, por supuesto, asume que todos los hilos de procesamiento están realizando el mismo trabajo. En suma, esto debe ser aceptable, porque la mayoría de las aplicaciones Delphi deberían compartir su tiempo de ejecución con un puñado de hilos para consumir el tiempo de las operaciones en segundo plano.

La otra cara de la moneda: buffer de flujos de datos.

Page 132: Multithreading a la manera de Delphi

Hasta ahora, todas las estructuras de buffer discutidas han implementado buffer de punteros para transferencia de datos. Mientras esto es muy útil para operaciones discretas, la mayoría de las operaciones de E/S involucran flujo de datos. Todas las estructuras de buffer tienen una parte de conteo muy parecida que involucra el flujo, que, por sus características, pueden ser tratados de manera similar. Hay un par de diferencias muy significativas que vale la pena mencionar:

Cuando se utiliza un buffer de flujo, no es posible usar semáforos para llevar cuenta de un número concreto de ítems en el buffer. En lugar de eso, los semáforos se usan en un estilo binario, esto es, con conteos de sólo 1 o 0. Cuando leemos o escribimos con buffer de flujo, un cálculo debe hacerse para saber si el buffer será llenado o vaciado por la operación. Si pasa alguna de estas cosas, entonces se transmiten tantos bytes como sea posible, y el hilo luego es bloqueado si es necesario.

Ya que el estado de bloqueo de los hilos lector y escritor es calculado en tiempo de ejecución, el estado no se mantiene en ningún lado, grabando el estado de bloqueo o ejecución de los hilos. Este estado se usa luego en subsecuentes operaciones de lectura o escritura de modo de saber si alguno de los “pares” en los hilos involucrados en alguna operación de lectura o escritura debe ser desbloqueado. Esto complica un poco la detección de bloqueo y desbloqueo, pero el principio general es el mismo.

Esquemas de notificación para buffer de flujo son modificados de manera similar. El actual esquema

Page 133: Multithreading a la manera de Delphi

de notificación envía una notificación para todas las lecturas o escrituras. Los componentes BAB que operan con flujos envían notificaciones basándose en si el buffer intermedio (o su equivalente) sigue estando lleno o no. Como las notificaciones pueden ser consideradas como el equivalente asincrónico a las operaciones Signal o ReleaseSemaphore, esta modificación es análoga a los puntos de arriba.

Hay mucho más que debería ser mencionado al respecto. Si el lector quiere ver un ejemplo funcional de buffer de flujo, puede consultar el código en el capítulo final.

Capítulo 11. Sicronizadores y Eventos.

En este capítulo.

Mas mecanismos de sicronización. Cuando la eficiencia óptima es imprescindible. Un MREWS Simple. Puntos sobre la implementación a resaltar. Un uso de ejemplo de MREWS simple. Una introducción a los Eventos. Simulación de eventos usando semáforos. El MREWS simple usando eventos. El MREWS de Delphi.

Mas mecanismos de sicronización.

El material introducido en capítulos anteriores ha cubierto todos los mecanismos básicos de sincronización. En el conjunto, los semáforos y los mutexes permiten que el programador cree el resto de los mecanismos de sincronización, no obstante con

Page 134: Multithreading a la manera de Delphi

un cierto esfuerzo. A pesar de esto, hay algunas situaciones que son muy comunes adentro la programación multihilo, pero no fáciles de ocuparse de usar los mecanismos demostrados hasta ahora. Dos nuevas primitivas serán introducidos para solucionar estos problemas: La Multi Read Exclusive Write Synchronizer, y la Event. La primera viene en algunas versiones de Delphi como parte de la VCL, y la última es proporcionada por el Win32 API.

Cuando la eficiencia óptima es imprescindible.

Hasta ahora, todas las operaciones en un valor compartido han sido mutuamente exclusiva. Todas las operaciones de lectura y escritura han sido protegidas hasta el punto de solamente una lectura o una escritura suceda en cualquier momento. Sin embargo, en muchas situaciones del mundo real donde un recurso crítico se debe acceder con frecuencia por una gran cantidad de hilos, esto puede resultar ser ineficiente. El bloqueo exclusivo es, de hecho, más cuidadoso que absolutamentenecesario. Recordar el capítulo 6, observa que la sincronización mínima requerida es ésa:

Las operaciones de lectura pueden ejecutarse concurrentemente.

Las operaciones de escritura no pueden ejecutarse al mismo tiempo que las operaciones de lectura.

Las operaciones de escritura no pueden ejecutarse al mismo tiempo que las operaciones de escritura.

Al permitr un mínimo control absoluto de la concurrencia, es posible producir un aumento significativo en el funcionamiento. Se observan los mejores aumentos de funcionamiento cuando muchas operaciones de lectura ocurren

Page 135: Multithreading a la manera de Delphi

de un número relativamente grande de hilos, operaciones de escritura son relativamente infrecuentes, y solamente un número pequeño de hilos las realizan.

Estas condiciones permanecen en numerosas situaciones del mundo real. Por ejemplo, la base de datos de stock para una compañía puede contener una gran cantidad de artículos, y numerosas lecturas pueden ocurrir para calcular la disponibilidad de ciertas mercancías. Sin embargo, la base de datos es solamente actualizada cuando los artículos se piden o se envían realmente. Tambien, los registros de miembros de un club se pueden comprobar muchas veces para encontrar direcciones, enviar correos y suscripciones, pero los miembros se unen al club, lo dejan o cambian sus direcciones relativamente muy poco. Lo mismo ocurre en situaciones de computación: las listas maestras de recursos globales en un programa se pueden leer a menudo, pero se escriben con poca frecuencia. El nivel requerido de control de concurrencia se proporciona con una primitiva conocida comoMultipleReadExclusiveWriteSynchronizer, en adelante referenciado como MREWS.

La mayoría de los sincronizadores soportan cuatro operaciones principales: StartRead, StartWrite, EndRead y EndWrite. Un hilo llama a StartRead en un sincronizador particular cuando desea leer el recurso compartido. Entonces realizará unas o más operaciones de lectura, que se garantizan serán atómicas y consistentes. Una vez que haya acabado la lectura, llama a EndRead. Si dos operaciones de lectura se realizan entre un par dado de llamadas a StartRead y

Page 136: Multithreading a la manera de Delphi

EndRead, los datos obtenidos en esos pares son siempre consistentes: ninguna operación de escritura habrá ocurrido entre las llamadas a StartRead y EndRead.

Asimismo, al realizar una serie de operaciones de escritura, un hilo llamará StartWrite. Puede entonces realizar una o más operaciones de escritura, y puede estar seguro que todas las operaciones de escritura son atomicas. Después de las operaciones de escritura, el hilo llama a EndWrite. Las operaciones de escritura no serán sobreescritas por otras operaciones, y ninguna lectura obtendrá resultados inconsistentes debido a estas operaciones cuando están en progreso.

Un MREWS Simple.

Hay varias maneras de implementar un MREWS. La VCL contiene una implementación bastante sofisticada. Para familiarizar al usuario con los principios basicos, aquí hay una implementación más simple pero levemente menos funcional usando los semáforos. El MREWS simple contiene los puntos siguientes:

Una sección crítica para asegurar el acceso a datos compartidos (DataLock).

Un contador del número de los lectores activos (ActRead).

Un contador del número de los lectores que están leyendo (ReadRead).

Un contador del número de los escritores activos (ActWrite).

Un contador del número de los escritores que están escribiendo (WriteWrite).

Page 137: Multithreading a la manera de Delphi

Un par de semáforos, conocido como los semáforos del lector y del escritor (ReaderSem y WriterSem).

Una sección crítica para forzar la exclusión de escritura (WriteLock).

La lectura y la escritura se pueden resumir así:

Hay dos etapas en la lectura o la escritura. La primera es la etapa activa, donde un hilo indica su intención de leer o de escribir. Una vez que haya ocurrido esto, el hilo se puede bloquear, dependiendo de si hay otra operación de lectura o escritura en progreso. Cuando se desbloquea, ingresa a la segunda etapa, realiza las operaciones de lectura o escritura, y después libera el recurso, estableciendo las cuentas de lectores o de escritores activos a los valores apropiados. Si es el último lector o escritor activo, desbloquea todos los hilos que fueron bloqueados previamente como resultado de la

Page 138: Multithreading a la manera de Delphi

operación que el hilo realizaba (leído o escriba). El diagrama siguiente ilustra esto más detalladamente.

En este punto, una implementación de esta clase particular de sincronización debe ser obvia. Aquí está. Si en este punto el lector todavía está confundido, ¡entonces no se asuste! ¡Este objeto de sincronización no se entiende fácilmente a primera vista! Observe atentamente por algunos minutos, y si comienzas a ver doble antes de que lo entiendas, entonces no te preocupes, y ¡continuemos!

Puntos sobre la implementación a resaltar.

Page 139: Multithreading a la manera de Delphi

Hay una asimetría en el esquema de la sincronización: los hilos que potencialmente quieren leer se bloquearán antes de la lectura si hay algunos escritores activos, mientras que los hilos que desean escribir se bloquean antes de la escritura si hay algunos lectores leyendo. Esto da prioridad a los hilos lectores; un acercamiento sensible, dado que las escrituras son menos frecuentes que las lecturas. Esta necesidad no es necesariamente el caso, dados todos los cálculos, si un hilo debe ser bloqueado o no ocurre en la sección crítica, es perfectamente permisible hacer el sincronizador simétrico. Lo malo de esto es que, si ocurren muchas operaciones de lectura concurrentes, pueden impedir que todas las escrituras ocurran. Por supuesto, la situación opuesta, con muchas escrituras deteniendo operaciones de lecturas también se puede dar.También es digno de observar el uso de semáforos cuando se adquieren recursos de lectura o escritura: Operaciones de espera en semáforos se deben realizar siempre fuera de la sección crítica que guarda los datos compartidos. Así la señalización condicional de un semáforo dentro de la sección crítica está puramente para asegurarse de que la operación de espera resultante no bloquea.

Un uso de ejemplo de MREWS simple.

Para demostrar lo que hace el MREWS, es necesario separarlo levemente de los ejemplos presentados hasta ahora. Imagínese que es necesario que una gran cantidad de hilos no pierdan de vista el estado de un número de archivos en cierto directorio. Estos hilos desean saber si un archivo ha cambiado desde que el hilo tuvo acceso a ese archivo por última vez.

Page 140: Multithreading a la manera de Delphi

Desafortunadamente, los archivos pueden ser modificados por un número diverso de programas en el sistema, así que no es posible que un solo programa no pierda de vista las operaciones que son realizadas en todos los archivos. Este ejemplo tiene un hilo en ejecución que itera a través de todos los archivos en un directorio, calculando una suma de comprobación (checksum) simple para cada archivo. Hace esto repetidamente, con eficacia ad infinitum. Los datos se almacenan en una lista que contiene un sincronizador MREW, permitiendo así que a una gran cantidad de hilos lectores lean las sumas de comprobación en unos o más archivos.

Primero, vamos mirar la fuente para la lista de la suma de comprobación. Aquí está. Las operaciones básicas son:

Establecer la suma de comprobación para un archivo particular. Esto agrega una entrada para el archivo en la lista si no existe.

Obtener la suma de comprobación para un archivo particular. Esto vuelve 0 si el archivo no se encuentra.

Quitar un archivo de la lista. Obtener una lista de cadenas con todos los

nombres de archivo. Obtener una lista de cadenas con todos los

nombres de archivo seguidos por sus sumas de comprobación.

Todas estas operaciones publicamente accesibles tienen llamadas de sincronización apropiadas al comienzo y al final de la operación.

Page 141: Multithreading a la manera de Delphi

Observe que hay un par de métodos los cuales comienzan con el nombre "NoLock". Estos métodos son los métodos que necesitan ser invocados desde más de un método visible publicamente. La clase se ha escrito de esta manera debido a una limitación de nuestro sincronizador actual: Las llamadas anidadas para comenzar a leer o a escribir no se permiten. Todas las operaciones que utilizan el sincronizador simple deben llamar solamente a StartRead o StartWrite si han terminado todas las operaciones de lectura o escritura anteriores. Esto será discutida más detalladamente más adelante. Aparte de esto, la mayoría del código para la lista de la suma de comprobación es bastante mundano, consistiendo sobre todo en el manejo de la lista, y no debe presentar ninguna sorpresa para la mayoría de los programadores de Delphi.

Ahora demos una mirada al código del hilo en ejecusión. Este hilo parece levemente diferente de la mayoría de los hilos de ejemplo que he presentado hasta ahora porque se pone en ejecución como una máquina de estado. El método Execute simplemente ejecuta una función para cada estado, y dependiendo del valor de retorno de la función, busca el siguiente estado requerido en una tabla de transición. Una función lee la lista de archivos desde el objeto lista de sumas de comprobación, el segundo quita sumas de comprobación innecesarias de la lista, y el tercero calcula la suma de comprobación para un archivo particular, y la actualiza en caso de ser necesario. La belleza de usar una máquina de estado es que hace mucho más limpia la terminación del hilo. El método Execute llama a las funciones, busca el siguiente estado y comprueba en un ciclo while si el hilo debe

Page 142: Multithreading a la manera de Delphi

terminar. Puesto que a cada función le toma normalmente un par de segundos terminar, la terminación del hilo es normalmente bastante rápida. Además, una sola verificación de terminación del hilo es necesaria, haciendo al código más limpio. También me gusta el hecho de que la lógica entera de la máquina de estado está implementada en una línea de código. Hay cierta pulcritud en esto.

Finalmente, hecharemos una ojeada el código del form principal. Esto es relativamente simple: el hilo y la lista de sumas de comprobación se crean al iniciar, y se destruyen cuando el programa se cierra. La lista archivos y sus sumas de comprobación se muestra regularmente como resultado un contador de tiempo (timer). El directorio que está siendo observado es fijo en el código; los lectores que deseen ejecutar el programa pueden cambiar este directorio, o posiblemente modificar el programa para poder especificar lo al inicio del mismo.

Este programa no realiza operaciones en datos compartidos en una manera estrictamente atómica. Hay varios lugares en el hilo de la actualización en donde los datos locales se asume implicitamente que son correctos, cuando el archivo subyacente pudo haber sido modificado. Un buen ejemplo de esto está en la función "check file" del hilo. Una vez que se haya calculado la suma de comprobación del archivo, el hilo lee la suma de comprobación almacenada para ese archivo, y lo actualiza si no coincide con la actual suma de comprobación calculada. Estas dos operaciones no son atómicas, puesto que las llamadas múltiples al objeto lista de sumas de comprobación no son atómicas. Esto proviene principalmente del hecho que llamadas anidadas al sincrinizador no trabaja con

Page 143: Multithreading a la manera de Delphi

nuestro sincronizador simple. Una solución posible es dar al objeto lista de sumas de comprobación, dos nuevos métodos: "bloquearse para la lectura" y "bloquearse para la escritura". Un bloqueo se podría adquirir en los datos compartidos, para la lectura o la escritura, y operaciones de lecturas y escrituras realizadas. Sin embargo, esto todavía no soluciona todos los posibles problemas de sincronización. Soluciones más avanzadas serán discutidas más adelante en este capítulo.

Puesto que el funcionamiento internos del sincronizador ocurre a nivel de Delphi, es posible obtener una estimación de cómoocurren a menudo los conflictos del hilo realmente. Poniendo un punto de parada (breakpoint) en los ciclos while de los procedimientos EndRead y EndWrite, el programa se detendrá si un hilo lector o escritor fue bloqueado mientras intentaba tener acceso al recurso. El punto de parada ocurre realmente cuando se desbloquea el hilo que espera, pero se puede hacer una cuenta exacta de conflictos. En el programa de ejemplo, estos conflictos son absolutamente raros, especialmente bajo poca carga, pero si el número de archivos y de sumas de comprobación llega a ser grande, los conflictos son cada vez más comunes, puesto que mas tiempo se pierde accediendo y copiando datos compartidos.

Una introducción a los Eventos.

Los eventos son quizás una de las primitivas de sincronización más simples de entender, pero una explicación de ellos se ha dejado para este punto, simplemente porque se utilizan mejor conjuntamente con otras primitivas de sincronización. Hay dos tipos

Page 144: Multithreading a la manera de Delphi

de eventos: eventos manuales y eventos automáticos. Por el momento, consideraremos eventos manuales. Un evento trabaja exactamente como un semáforo (o luz de parada para los lectores de ESTADOS UNIDOS)[1].

Tiene dos estados posibles: señalado (análogo a un semáforo en verde) o no-señalado (análogo a un semáforo en rojo). Cuando el evento está señalado, los hilos que están en espera en el evento no se bloquean y continúan en ejecución. Cuandoel evento no está señalado, los hilos que estan en espera en el evento se bloquean hasta que se señala el evento. El Win32 API proporciona una gama de funciones para ocuparse de eventos.

CreateEvent/OpenEvent: Estas funciones son similares a las otras funciones Win32 para crear o abrir objetos de sincronización. Así como permitir que el evento sea creado en un estado señalado o no-señalado, una bandera boleana indica si el evento es un evento manual o automático.

SetEvent: Esto fija el estado del evento a señalado, así reanudando todos los hilos que estén esperando en el evento, y permitiendo que últimos hilos pasen sin bloquearse.

ResetEvent: Esto fija el estado del evento a no-señalado, así bloqueando todos los hilos que realicen posteriormente una espera en el evento.

PulseEvent: Esto realiza un "set-reset" en el evento. Por lo tanto, todos los hilos esperando en el evento cuando el evento es reajustado se reanudan, pero últimos hilos que esperan en el evento todavía quedan bloqueados.

Los eventos automáticos son un caso especial de los eventos manuales. En un evento automático, el estado

Page 145: Multithreading a la manera de Delphi

de un evento señalado se fija de nuevo a no-señalado una vez que ha pasado exactamente un hilo en el evento sin bloqueo, o se ha lanzado un hilo que estaba bloqueado. En este sentido, trabajan de una manera casi idéntica a los semáforos, y si un programador está utilizando eventos automáticos, deben considerar usar semáforos en su lugar, para hacer el comportamiento del mecanismo de sincronización más obvio.

Simulación de eventos usando semáforos.

Una primitiva de evento de hecho puede ser creada usando semaforos: Es posible utilizar un semáforo para bloquear condicionalmente todos los hilos que esperan en la primitiva de evento y para desbloquear hilos cuando se señala la primitiva. Para hacer esto, se utiliza un acercamiento muy similar al algoritmo de sincronización. El evento guarda dos piezas de estado: un boleano indicando si el evento está señalado o no, y una cuenta del número de hilos bloqueados actualmente en el semáforo en el evento. Aquí está cómo se ponen en ejecución las operaciones:

CreateEvent: Se crea el objeto del evento, la cuenta de hilos bloqueados se fija a cero, y el estado de la señal se fija según lo especificado en el constructor.

SetEvent: El estado de la señal se fija para no bloquear los hilos entrantes. Además, la cuenta de hilos bloqueados en el semáforo se examina, y si esta arriba de cero, entonces el semáforo se señala repetidamente hasta que se desbloquean todos los hilos bloqueados.

Page 146: Multithreading a la manera de Delphi

ResetEvent: El estado de la señal se fija para bloquear los hilos entrantes.

PulseEvent: Todos los hilos bloqueados actualmente en el semáforo se desbloquean, pero no se realiza ningún cambio al estado de la señal.

WaitForEvent: El estado de la señal del evento se examina. Si indica que el evento está señalado, entonces se señala el semáforo interno, y la cuenta de hilos bloqueados en el semáforo se decrementa. La cuenta de hilos bloqueados se incrementa, y una espera se realiza en el semáforo interno.

Aquí está el código para un evento simulado usando semáforos. Si el lector ha entendido el sincronizador simple, entonces este código debe ser bastante auto explicativo. La implementación podría ser simplificada levemente substituyendo los ciclos while que desbloquean los hilos con una sola sentencia que incremente la cuenta en el semáforo por la cantidad requerida, no obstante el acercamiento implementado aquí es más consistente con la implementación del sincronizador presentado anteriormente.

El MREWS simple usando eventos.

Las estructuras del control requeridas para simular un evento que usa semáforos son notablemente similares a las estructuras usadas en el sincronizador simple. Así se parece posible tratar de crear un sincronizador usando eventos en vez de semáforos. Esto no es particularmente difícil: aquí está. Como es normal, la conversión atrae la atención sobre algunos puntos de la implementación dignos de mirar.

Primero que nada, el sincronizador simple calculaba si los hilos se deben bloquear en la sección crítica de los procedimientos StartRead y StartWrite, y después

Page 147: Multithreading a la manera de Delphi

realizar las acciones de bloqueo requeridas fuera de la sección crítica. Lo mismo se necesita para nuestro nuevo sincronizador de eventos. Para hacer esto, asignamos un valor a una variable local llamada "Block" (recuerda, las variables locales son inmunes a los hilos). Esto se hace dentro de la sección crítica de DataLock, para garantizar resultados consistentes, y las acciones de bloqueo se realizan fuera de la sección crítica para evitar Deadlocks.

En segundo lugar, este sincronizador particular es simétrico, y permite operaciones de escritura o lectura con igual prioridad.Desafortunadamente, puesto que hay solamente un sistema de contadores en este sincronizador, es algo más difícil hacerlo asimétrico.

El MREWS de Delphi.

El problema principal con los sincronizadores existentes es que no son reentrantes. Es totalmente imposible anidar llamadas a StartWrite, un Deadlock ocurrirá de inmediato. Es posible anidar llamadas a StartRead, a condición de que ningún hilo llame a StartWrite en el medio de una secuencia de llamadas anidadas a StartRead. Una vez más, si esto ocurre, un Deadlock será una consecuencia inevitable. Lo ideal sería que pudieramos anidar operaciones de lectura y de escritura. Si un hilo es un lector activo, entonces las llamadas repetidas a StartRead no deberían tener ningún efecto, con tal que sean emparejadas por un número igual de llamadas a EndRead. Semejantemente, llamadas anidadas a StartWrite deben ser posibles también, y todas pero el par externo de las llamadas a StartWrite y EndWrite no deberían tener ningún efecto.

Page 148: Multithreading a la manera de Delphi

El segundo problema es que los sincronizadores ilustrados hasta ahora no permiten operaciones atómicas de leer-modificar-escribir. Lo ideal sería que un simple hilo pudiese llamar a StartRead, StartWrite, EndWrite, EndRead; así permitiendo que un valor sea leído, modificado y escrito atomicamente. A los otros hilos no se les deben permitir escribir en cualquier parte de la secuencia, y no se les deben permitir leer durante la operación de escritura de la secuencia. Con los sincronizadores actuales, es perfectamente posible hacer esto simplemente realizando operaciones de lectura y escritura dentro de un par de llamadas a StartWrite y EndWrite. Sin embargo, si las llamadas de la sincronización se encajan en un objeto compartido de los datos (como en el ejemplo) puede ser muy difícil proporcionar un interfaz conveniente a ese objeto que permita operaciones de lectura-modificación-y-escritura sin también proveer llamadas separadas de sincronización para bloquear el objeto en la lectura o escritura.

Para hacer esto, se requiere una implementacion en conjunto más sofisticada, por el que cada operación de comienzo y fin se fije en cuál hilo esta realizando la operación de lectura o escritura actualmente. De hecho esto es lo que hace el sincronizador de Delphi. Desafortunadamente, debido a los acuerdos que licenciasno es posible exhibir el código de fuente de VCL aquí y discutir exactamente que lo hace. Sin embargo, sea suficiente decir que el Delphi MREWS:

Permite operaciones de lectura anidadas. No permite operaciones de escritura anidadas. Permite que las operaciones de lectura sean

promovidas a operaciones de escritura,

Page 149: Multithreading a la manera de Delphi

permitiendo operaciones de leer-modificar-y-escribir se hagan con bloqueos mínimos en cada etapa de los procedimientos.

Está escrito tendiendo mucho a la eficiencia: Se utilizan las secciones críticas solamente donde son absolutamente necesarias, y se prefieren operaciones de interbloqueo. Esto obscurece el código un poco, pero el aumento en eficiencia es más que valioso.

Puede ser intercambiado con las clases de sincronizador presentadas arriba sin cambio en la semántica.

Nota del traductor [1]: El autor hace notar la diferencia de nombres que tienen los semaforos de calle entre EEUU e Inglaterra, stopping ligth y traffic ligthrespectivamente.

Capítulo 12. Más dispositivos Win32 para la sincronización.

En este capítulo:

Mayor eficiencia vía operaciones de interbloqueo. Atomicidad desde la nada. Eventcounts y secuenciadores. Otros dispositivos Win32 para la sincronización.

Mayor eficacia vía operaciones de interbloqueo.

Las primitivas convencionales para la sincronización pueden ser un gasto considerable en simples sistemas multihilo, particularmente para los hilos que se sincronizan firmemente el uno al otro. Un alternativa posible es utilizar operaciones de interbloqueo.

Page 150: Multithreading a la manera de Delphi

Las operaciones de interbloqueo fueron concebidas originalmente como mecanismo para la sincronización de bajo nivel en los sistemas con multiples procesadores simétricos y memoria compartida. En sistemas con multiples procesadores, la memoria compartida es una manera extremadamente eficiente de transferir datos entre los procesos y los hilos. Una manera tuvo que ser encontrada para prevenir problemas de atomidad cuando dos o más procesadores intentan utilizar el mismo pedazo de la memoria. Casi todos los procesadores introdujeron recientemente soporte para operaciones de interbloqueo para permitir esto. éstas son operaciones por el que un procesador puede leer un valor de la memoria, modificarla y después escribirla atómicamente, mientras que se asegura de que ningún otro procesador tenga acceso a la misma memoria, y el procesador que realiza la operación no se interrumpe. Win32 proporciona las siguientes operaciones de interbloqueo:

InterlockedCompareExchange (Win NT/2K unicamente).

InterlockedDecrement. InterlockedExchange. InterlockedExchangeAdd (Win NT/2K unicamente). InterlockedIncrement.¿Por qué uno debería usar operaciones de

interbloqueo después de todo? Un buen ejemplo es el de una cerradura de vueltas. De vez en cuando uno desea crear algo similar a una sección crítica. Sin embargo, puede haber código muy pequeño en la sección crítica, y el código en la sección crítica puede ser accedido muy a menudo. En casos tales como este, un objeto totalmente basado en la

Page 151: Multithreading a la manera de Delphi

sincronización puede probar ser ¿overkill?. La cerradura de vueltas permite que hagamos una cosa similar, y trabaja así. Un hilo adquiere la cerradura si, al realizar un incremento interbloqueado, encuentra que, después del incremento, el valor de la cerradura es 0. Si encuentra que el valor es mayor de 0, entonces otro hilo tiene la cerradura, y realiza otro intento. La llamada a dormir es incluida de modo que un hilo no de vueltas por períodos largos en la cerradura mientras que un hilo de más baja prioridad tiene la cerradura. En planificadores simples, si las prioridades del hilo son iguales, después la llamada a dormir no será necesaria. La operación de interbloqueo es necesaria, porque si un hilo realizó una lectura de memoria, incremento, comparación y posterior escritura, entonces dos hilos podrían adquirir la cerradura simultáneamente.

El gasto se reduce porque apenas un par de las instrucciones de la CPU se requieren para entrar y para salir de la cerradura, con tal que un hilo no tenga que esperar. Si los hilos tienen que esperar algún tiempo apreciable, entonces la CPU de desperdicia, así que son solamente útiles para poner secciones críticas pequeñas. Las cerraduras de vuelta son útiles al hacer cumplir las secciones críticas que son ellos mismos parte de las estructuras de la sincronización. Los datos compartidos dentro de primitivas o de planificadores de sincronización son protegidos a menudo por las cerraduras de esta clase: las cerraduras son a veces necesarias porque las primitivas de sincronización a nivel del OS no pueden ser usadasr para implementar primitivas de sincronización a nivel del OS. Las cerraduras de vuelta tienen todos los mismos problemas de

Page 152: Multithreading a la manera de Delphi

concurrencia que los mutexes, con salvedad de que la adquisición cíclica da lugar ya no a deadlocks, si no a livelocks. Esta es una situación levemente peor que un deadlock porque aunque hilos "bloqueados" no están ejecutando ningún código útil, están funcionando como un bucle infinito, están utilizando la CPU y están degradando el funcionamiento del sistema entero. Las cerraduras de vuelta no deben ser utilizadas como semáforos para "suspender" un hilo.

Atomicidad desde la nada.

Con cuidado, Es de hecho posible crear una cerradura de vueltas que sea atómica sin asumir ningún interbloqueo en absoluto, a condición de que las interrupciones ocurren solamente entre instrucciones de la CPU. Considere esto. Veamos con pascal primero para tener una idea general. Tenemos una cerradura entera en memoria. Al intentar entrar en la cerradura, primero incrementamos la cerradura en memoria. Entonces leemos el valor de la memoria en una variable local, y verificamos, como antes, para ver si es mayor de cero. Si es, entonces algún otro tiene la cerradura, y vamos otra vez, si no, tenemos la cerradura.

Lo importante sobre este sistema de operaciones es que, dado ciertas claúsulas, un cambio de hilo puede ocurrir en cualquier momento, ésto todavía sigue siendo seguro contra hilos. El primer incremento de la cerradura es un incremento indirecto del registro. El valor está siempre en memoria, y el incremento es atómico. Entonces leemos el valor de la cerradura en un vairable local. Esto no es atómico. El valor leído dentro de la variable local puede ser diferente del

Page 153: Multithreading a la manera de Delphi

resultado del incremento. Sin embargo, la cosa realmente astuta sobre esto es que porque el incremento se realiza antes de la operación de lectura, los conflictos de hilo que ocurren significarán siempre que el valor leído es demasiado alto en vez de demasiado bajo: los conflictos de hilo resultan en una estimación conservadora de si la cerradura está libre.

A veces es útil escribir operaciones como esto en ensamblador, para estar totalmente seguro que los valores correctos se están dejando en memoria, y no se están depositando en registros. Mientras que resulta, en Delphi 4 al lo menos, pasando la cerradura como parámetro var, e incluyendo la variable local, el compilador Delphi genera el código correcto que trabajará en máquinas de processor único. En las máquinas con multiples procesadores, los incrementos y los decrementos indirectos no son atómicos. Esto ha sido solucionada en la versión ensamblador codificada a mano agregando el prefijo de la cerradura delante de las instrucciones que manipulan la cerradura. Este prefijo manda a un procesador bloquear el bús de memoria exclusivamente mientras dura la instrucción, haciendo atómicas estas operaciones así.Las malas noticias son que aunque en teoría ésto es correcto, la máquina virtual Win32 no permite que los procesos a nivel de usuario ejecuten instrucciones con prefijo de cerradura. Los programadores que se proponen utilizar este mecanismo deben utilizarlo solamente en código con privilegios de Ring 0. Otro problema es que desde esta versión de la cerradura de vueltas no llama a dormir, es posible que los hilos monopolicen el procesador mientras esperan la

Page 154: Multithreading a la manera de Delphi

cerradura, algo que está garantizado para traer la máquina a un cuelgue total.

Eventcounts y secuenciadores.

Una propuesta alternativa a los semáforos es usar dos nuevos tipos de primitivas: eventcounts y secuenciadores. Ambas contienen contadores, pero a diferencia de los semáforos, los contadores aumentan indefinidamente a partir del tiempo de su creación. Alguna gente es más feliz con la idea que es posible distinguir individualmente entre las 32da y 33ra ocurrencias de un acontecimiento en el sistema. Los valores de estos contadores se ponen a disposición los hilos para que los usen, y los valores se pueden utilizar por procesos para pedir sus acciones. Los Eventcounts soportan tres operaciones:

EVCount.Advance(): Esto incrementos el contador, y devuleve el nuevo valor después del incremento.

EVCount.Read(): Esto vuelve la cuenta actual. EVCount.Await(WaitCount:integer): Esto suspende

el hilo llamador hasta que la cuenta interna es mayor que o igual a WaitCount.

Los secuenciadores tienen solo una operación:

Sequencer.Ticket(): Vuelve el contador interno actual en el secuenciador, y lo incrementa.

Una definición de las clases implicadas se debería ver a algo como esto. Es entonces relativamente fácil utilizar eventcounts y secuenciadores para realizar todas las operaciones que se pueden realizar usando semáforos:

Hacer cumplir una exclusión mutua. Buffer limitado con un productor y un consumidor.

Page 155: Multithreading a la manera de Delphi

Buffer limitado con un número arbitrario de productores y de consumidores.

Una ventaja particular de este tipo de primitiva de sincronización es que las operaciones de avanzar y pedir turnos se pueden implementar de forma muy sensilla, usando la instrucción de comparación de bloqueos mutuos. Esto se deja como ejercicio levemente más difícil para el lector.

Otros dispositivos Win32 para la sincronización.

Waitable Timers (Temporizadores con Tiempo de espera). Windows NT y Win2K proporcionan objetos Waitable Timers. éstos permiten a un hilo o a un número de hilos esperar por una cantidad de tiempo particular dentro de un objeto temporizador. Los temporizadores se pueden utilizar para lanzar un solo hilo o cierto número de hilos sobre una base sincronizada; una manera de controlar el flujo de los hilos. Además, el retardo que los temporizadores con tiempo de espera (Waittables Timers) proporcionan se pueden fijar a valores muy exactos: el valor más pequeño disponible es alrededor 100 nanosegundos, haciendo a los temporizadores más deseables que usar Sleep() si un hilo tiene que ser suspendido por cierta cantidad de tiempo.

MessageWaits (Espera de mensajes). Cuando las aplicaciones Delphi están esperando que los hilos terminen, el hilo principal de VCL se bloquea permanentemente. Esto es una situación potencialmente problemática, porque el hilo de la VCL no puede procesar mensajes. Win32 proporciona la función MsgWaitForMultipleObjects para solucionar esto. Un hilo espera un mensaje se bloquea también hasta que los objetos de

Page 156: Multithreading a la manera de Delphi

sincronización se señalan, o un mensaje se pone en la cola de mensaje de los hilos. Esto significa que usted puede conseguir que el hilo principal de la VCL espere por los hilos actualmente en ejecusión mientras que también permite que responda a los mensajes de las ventanas. Un buen artículo sobre el tema se puede encontrar en: http://www.midnightbeach.com/jon/pubs/MsgWaits/MsgWaits.html (en inglés).

Capítulo 13. Usar hilos conjuntamente con el BDE, las excepciones y las DLLs.

En este capítulo:

Programación de DLL y Multiprocesos. Alcance del hilo y del proceso. Una sola DLL dentro

de un hilo. Escribir una DLL multihilo. Puesta a punto e implementación de la DLL. Trampa 1: La encapsulación de Delphi de la

función de punto de entrada. Escribir un DLL con multiproceso. Objetos globales con nombre. La DLL en detalle. Inicialización de la DLL. Una aplicación usando la DLL. Trampa 2: Contexto del hilo en las funciones de

punto de entrada. Control de Excepciones. El BDE.

Programación de DLL y Multiprocesos.

Page 157: Multithreading a la manera de Delphi

Las bibliotecas de enlace dinámicas, o los DLL permiten que un programador comparta código ejecutable entre varios procesos. Se utilizan comúnmente para proporcionar el código de la librería compartida para varios programas. El código de la escritura para los DLL está en la mayoría es similar al código de escritura para los executables. A pesar de esto, la naturaleza compartida de las DLL significa que los programadores familiarizados con programación multihilos los utilizan a menudo para proporcionar servicios a nivel del sistema: ése es el código que afecta varios procesos que tengan el DLL cargado. En este capítulo, miraremos cómo escribir código para una DLL que funciona a través de más de un proceso.

Alcance del hilo y del proceso. Una sola DLL dentro de un hilo.

Las variables globales en las DLL tienen ambito en todo el proceso. Esto significa que si dos procesos separados tienen una DLL cargada, todas las variables globales en el DLL son locales a ese proceso. Esto no se limita a las variables en el código de los usuarios: también incluye todas las variables globales en las bibliotecas runtime de Borland, y cualquier unidad usada por código en la DLL. Esto tiene la ventaja que los programadores principiantes de DLLs pueden tratar la programación de DLLs de la misma manera que la programación de ejecutables: si una DLL contiene una variable global, entonces cada proceso tiene su propia copia. Además, esto también significa que si una DLL es invocada por los procesos que contienen solamente un hilo, entonces no se requieren ninguna técnica en especial: la DLL no necesita ser segura frente a hilos, puesto que todos

Page 158: Multithreading a la manera de Delphi

los procesos tienen instancias totalmente aisladas de la DLL.

Podemos demostrar esto con una DLL simple que no haga nada mas que almacenar un número entero. Exporta un par de funciones que permiten a una aplicacion leer y escribir el valor de ese número entero. Podemos entonces escribir; una simple aplicación de prueba que utilice esta DLL. Si varias copias de la aplicación se ejecutan, uno observa que cada aplicación utiliza su propio número entero, y ninguna interferencia existe entre ellas.

Escribir una DLL multihilo.

Escribir una DLL multihilo es sobre todo igual que la escritura código multihilo en una aplicación. El comportamiento de hilos múltiples dentro de la DLL es igual que el comportamiento de hilos múltiples en una aplicación cualquiera. Como siempre, hay un par de trampas para el distraido:

La trampa principal en que uno puede caer en es el comportamiento del administrador de memoria de Delphi. Por omisión, el administrador de memoria de Delphi no es seguro frente a hilos. Esto está por razones de eficacia: si un programa contiene solamente siempre un hilo, entonces es un gasto de recursos incluira sincronización en el administrador de memoria. El administrador de memoria de Delphi puede ser seguro frente a hilos fijando la variable IsMultiThread a true. Esto se hace automáticamente si se crea una clase TThread para un módulo dado.

El problema es que un ejecutable y la DLL consisten de dos módulos separados, cada uno con su propia copia del administrador de memoria de Delphi. Así, si

Page 159: Multithreading a la manera de Delphi

un ejecutable crea varios hilos, su administrador de memoria es multihilo. Sin embargo, si esos dos hilos llaman una DLL cargada por el ejecutable, el administrador de memoria de la DLL no está enterado del hecho de que está siendo llamado por los hilos múltiples. Esto puede ser solucionado estableciendo la variable IsMultiThread a true. Es mejor establecer esto usando la función Entry Point de la DLL, discutido más adelante.

La segunda trampa ocurre como resultado del mismo problema; el de tener dos administradores de memoria separados. La memoria asignada por el administrador de memoria de Delphi que se pasa desde la DLL al ejecutable no se puede asignar en uno y liberar en el otro. Esto ocurre más a menudo con los strings largos, pero puede ocurrir al usar asignación de memoria con New o GetMem, y liberarla usando Dispose o FreeMem. La solución en este caso es incluir ShareMem, una unidad que mantiene dos administradores de memoria en conjunto usando las técnicas discutidas más adelante.

Puesta a punto e implementación de la DLL.

Atento al hecho de que los programadores de DLL necesitan a menudo estar enterados de cuántos hilos y procesos están activos en una DLL en cualquier momento, los arquitectos del sistema Win32 proporcionan un método para los programadores de DLL para no perder de la cuenta de los hilos y procesos en una DLL. Este método se conoce como la función de punto de entrada (Entry Point) de la DLL.

Page 160: Multithreading a la manera de Delphi

En un ejecutable, el punto de entrada (según lo especificado en el encabezado del módulo) indica donde la ejecución del programa debe comenzar. En una DLL, señala a una función que se ejecuta siempre que un ejecutable cargue o descargue la DLL, o siempre que un ejecutable que está utilizando actualmente la DLL cree o destruya un hilo. La función toma un solo parmámetro integer que puede ser uno de los siguientes valores:

DLL_PROCESS_ATTACH: Un proceso se ha unido a la DLL. Si éste es el primer proceso, entonces acaba de cargarse la DLL

DLL_PROCESS_DETACH: Un proceso se ha separado de la DLL. Si éste es el único proceso usando la DLL, entonces la DLL será descargada.

DLL_THREAD_ATTACH: Un hilo se ha unido a la DLL. Esto sucederá una vez cuando el proceso carga la DLL, y posteriormente siempre que el proceso cree un hilo nuevo.

DLL_THREAD_DETACH: Un hilo se ha separado de la DLL. Esto sucederá siempre que el proceso destruya un hilo, y finalmente cuando el proceso descarga la DLL.

A su turno, los puntos de entrada de la DLL tienen dos características que pueden conducir a malentendidos y problemas al escribir códigos de punto de entrada. La primera característica ocurre como resultado de la encapsulación de Delphi de la función Entry Point, y es relativamente simple de solucionar. La segunda ocurre como resultado de contexto del hilo, y será discutido más adelante.

Trampa 1: La encapsulación de Delphi de la función de punto de entrada.

Page 161: Multithreading a la manera de Delphi

Delphi utiliza la función del punto de entrada de la DLL para manejar inicialización y finalización de unidades dentro de una DLL así como la ejecución del cuerpo principal del código de la DLL. El escritor de la DLL puede poner un gancho en el manegador de Delphi asignando una función apropiada a la variable DLLProc. El manejador por omisión de Delphi funciona así:

Se carga la DLL, la función del punto de entrada se llama con DLL_PROCESS_ATTACH.

Delphi utiliza esto para llamar la inicialización de todas las unidades en la DLL, seguido por el cuerpo principal del código de la DLL.

La DLL se descarga, dando por resultado dos llamadas a la función del punto de entrada, con los argumentos DLL_PROCESS_DETACH.

Ahora, el escritor de la aplicación solamente consigue código para ejecutarse en respuesta a la función del punto de entrada cuando la variable DLLProc apunta a una función. El punto correcto para establecer esto está en el cuerpo principal de la DLL. Sin embargo, esta está en respuesta a la segunda llamada a la función del punto de entrada. Resumiendo, lo que esto significa es que al usar la función del punto de entrada en la DLL, el programador de Delphi nunca verá la primera unión del proceso a la DLL. A la postre, éste no es un problema serio: uno puede asumir simplemente que el cuerpo principal de la DLL se llama en respuesta a un proceso de carga de la DLL, y por lo tanto el proceso y la cuenta del hilo es 1 en ese punto. Puesto que la variable DLLProc se copia proceso a proceso, incluso si más procesos se unen más adelante, el mismo argumento se aplica, puesto que cada

Page 162: Multithreading a la manera de Delphi

instancia de la DLL tiene variables globales separadas.

En caso de que todavía confundan al lector, presentaré un ejemplo. Aquí está una DLL modificada que contiene una unidadcon una función que muestra un mensaje. Como usted puede ver, el cuerpo principal, la inicialización de la unidad y la función de punto de entrada de la DLL contienen las llamadas a "ShowMessage" que permiten a uno seguir la pinsta a lo que está ocurriendo. Para probar esta DLL, aquí hay una aplicación de prueba. Consiste de una ventana con un botón encendido. Cuando se hace click en el botón, se crea un hilo, el cual llama al procedimiento en la DLL, y después se destruye. ¿Así pues, qué sucede cuando ejecutamos el programa?

La DLL avisa de la inicialización de las unidades. La DLL avisa de la ejecución del cuerpo principal

de la DLL. Cada vez que se hace click en el botón la DLL

informa:o Punto de entrada: unión de un hilo.o Procedimiento de la Unidad.o Punto de entrada: separación del Hilo

Note que si disparamos más de un hilo desde la aplicación, mientras que dejamos los hilos existentes bloqueados con MessageBox del procedimiento de la unidad, la cuenta total de hilos unidos a la DLL puede aumentar más allá de una.

Cuando el programa se cerra, la DLL informa el punto de entrada: separación del proceso, seguido por la finalización de la unidad.

Escribiendo una DLL multiproceseso.

Page 163: Multithreading a la manera de Delphi

Armado con el conocimiento de cómo utilizar la función de punto de entrada, ahora escribiremos una DLL multiprocesos. Esta DLL almacenará cierta información a nivel de sistema usando memoria compartida entre los procesos. Vale recordar que cuando el código tiene acceso a los datos compartidos entre los procesos, el programador debe proporcionar la sincronización apropiada. Pues los hilos múltiples en un solo proceso intrínsecamente no se sincronizan, así que los hilos principales en diversos procesos tampoco se sincronizan. También miraremos algunas delicadezas que ocurren al intentar utilizar la función de punto de entrada para poder seguirles la pista a los hilos globales.

Esta DLL compartirá un solo número entero entre los procesos, así como mantener un contador del número de procesos e hilos en la DLL en cualquier momento. Consiste en un archivo de cabecera compartido entre la DLL y las aplicaciones que utilizan la DLL, y el archivo de proyecto de la DLL. Antes de que miremos más de cerca al código, vale repasar cómo se comporta la Win32.

Objetos globales con nombre.

El API Win32 permite que el programador cree varios objetos. Para algunos de estos objetos, pueden ser creados anónimos o con cierto nombre. Los objetos creados anónimos son, en el todo, limitado para utilizar por un solo proceso, la excepción es que pueden ser heredados por procesos hijos. Los objetos creados con un nombre se pueden compartir entre los procesos. Típicamente, un proceso creará el objeto, especificando un nombre para ese objeto, y otros

Page 164: Multithreading a la manera de Delphi

procesos abrirán un manejador (handle) a ese objeto especificando su nombre.

La cosa encantadora sobre objetos con nombre es que los manejadores a estos objetos tienen un contador de referencias a nivel de sistema. Es decir, varios procesos pueden adquirir manejadores de un objeto, y cuando todos los manejadores de ese objeto se cierran, el objeto sí mismo se destruye, y no antes. Esto incluye cuando la aplicación se cae: muchas veces Windows hace un buen trabajo de limpieza de los manejadores después de un desplome.

La DLL en detalle.

Nuestro DLL utiliza esta propiedad para mantener un archivo mapeado en memoria. Normalmente, los archivos mapeados en memoria se utilizan para crear un área de memoria que es una imagen espejo de un archivo en disco. Esto tiene muchos usos útiles, no solo para paginación "a pedido" de imágenes de ejecutables en disco. Sin embargo para esta DLL, se utiliza un caso especial por el que un archivo mapeado en memoria se crea sin imagen correspondiente en el disco. Esto permite que el programador asigne una porción de la memoria que se compartirá entre varios procesos. Esto es asombrosamente eficiente: una vez que se instale el archivo mapeado, no se hace ningún copiado de memoria entre los procesos. Una vez que se haya instalado el archivo mapeado en memoria, un mutex con nombre global se utiliza para sincronizar el acceso a esa porción de la memoria.

Inicialización de la DLL.

Page 165: Multithreading a la manera de Delphi

La inicialización consiste en cuatro etapas principales:

Creación de los objetos de sincronización (globales y otros).

Creación de datos compartidos. Incremento inicial de los contadores de hilo y de

proceso. Enganchar la función de punto de entrada de la

DLL.En la primera etapa, se crean dos objetos de

sincronización, un mutex global, y una sección crítica. Poco necesita ser dicho acerca de la sección crítica. El mutex global se crea vía la llamada a la API CreateMutex. Esta llamada tiene la característica beneficiosa que si se nombra el mutex, y ya existe el objeto con nombre, entonces se devuelve un manejador de objeto con nombre existente. Esto ocurre atómicamente. Si esto no es el caso, entonces podrían ocurrir toda una serie de condiciones de carrera (race conditions). Determinar de forma precisa toda la serie de problemas y sus posibles soluciones (involucrando principalmente control de concurrencia optimista) se deja como ejercicio al lector. Sea suficiente decir que si las operaciones en los manejadores de los objetos compartidos globales no fueran atómicas, el programador de aplicaciones Win32 estaría mirando fijamente en un abismo...

En la segunda etapa se instala el área de la memoria compartida. Puesto que hemos instalado ya el mutex global, se utiliza al instalar el archivo mapeado. Una vista del "archivo" mapeado, que mapea el archivo (virtual) en el espacio de dirección del proceso que llama. También comprobamos si es el proceso que creó originalmente el archivo mapeado, si éste es el

Page 166: Multithreading a la manera de Delphi

caso, entonces ponemos a cero los datos en nuestra vista mapeada. Esta es la razón por la cual el procedimiento se envuelve en un mutex: CreateFileMapping tiene las mismas características de atomicidad que CreateMutex, asegurándose de que nunca ocurrirán las condiciones de carrera en los manejadores. En el caso general, sin embargo, igual no es necesariamente cierto para los datos en el mapeado. Si el mapeado tenía un archivo físico, entonces podemos asumir la validez de los datos compartidos desde el inicio. Para los mapeos virtuales esto no está asegurado. En este caso necesitamos inicializar los datos en el mapeado atomicamente estableciendo un manejador al archivo mapeado, por lo tanto al mutex.

En la tercera etapa, realizamos nuestra primera manipulación en los datos global compartidos, incrementando los contadores de proceso s y de hilos, puesto que la ejecución del cuerpo principal de la DLL es consistente con la adición de otro hilo y proceso a aquellos que usan la DLL. Observe que el procedimiento AtomicIncThreadCount incrementa ambos contadores locales y globales de los hilos mientras se han adquirido el mutex global y la sección crítica del proceso local. Esto asegura que los hilos múltiples del mismo proceso vean una vista completamente consistentes de ambas cuentas.

En la etapa final, se engancha el DLLProc, así se asegura que la creación y la destrucción de otros hilos en el proceso es monitoreada, y la salida final del proceso también es registrada.

Una aplicación usando la DLL.

Page 167: Multithreading a la manera de Delphi

Una aplicación simple que utiliza el DLL se presenta aquí. Consiste en la unidad compartida global, una unidad que contiene la ventana principal, y una unidad subsidiaria que contiene un hilo simple. Existen cinco botones en la ventana, permitiendo que el usuario lea los datos contenidos en la DLL, incrementar, decrementar y establecer el valor del número entero compartido, y crean unos o más hilos dentro de la aplicación, solo para verificar que los contadores locales del hilo funcionan. Según lo esperado, los contadores de hilo se incrementan siempre que una nueva copia de la aplicación se ejecute, o uno de las aplicaciones crea un hilo. Observe que el hilo no necesita utilizar directamente la DLL para que la DLL esté al tanto de su presencia.

Trampa 2: Contexto del hilo en las funciones de punto de entrada.

En vez de usar una aplicación simple, intentemos uno que haga algo avanzado. En esta situación, el DLL se carga manualmente por el programador de la aplicación, en vez de ser cargado automáticamente. Esto es posible substituyendo la unidad con la ventana anterior por ésta. Se agrega un botón adicional que carga la DLL, e instala el procedimiento manualmente. Intente ejecutar el programa, crear varios hilos de rosca y después cargar la DLL. Debe apreciar que la DLL ya no le sigue la pista correctamente al número de hilos en los variados procesos que lo utilizan. ¿Por qué es esto?. El archivo de la ayuda Win32 indica eso al usar la función punto de entrada con los parámetros DLL_THREAD_ATTACH y DLL_THREAD_DETACH:

Page 168: Multithreading a la manera de Delphi

DLL_THREAD_ATTACH indica que el proceso actual está creando un hilo nuevo. Cuando ocurre esto, el sistema llama a la función entry-point de todas las DLLs unidas actualmente al proceso. La llamada se hace en el contexto del nuevo hilo. Las DLLs pueden utilizar esta oportunidad de inicializar una ranura de TLS para el hilo. Un hilo que llama a la función entry-point de la DLL con el valor DLL_PROCESS_ATTACH no llama a la función entry-point de la DLL con el valor DLL_THREAD_ATTACH.

Observe que la función entry-point de una DLL es llamada con este valor solamente por los hilos creados después de que la DLL se una al proceso. Cuando una DLL es cargada con LoadLibrary, los hilos existentes no llaman a la función entry-point de la DLL recientemente cargada.

Lo que conduce a:

DLL_THREAD_DETACH indica que un hilo ha terminado limpiamente. Si la DLL ha almacenado un puntero a la memoria asignada en una ranura de TLS, utiliza esta oportunidad para liberar la memoria. El sistema operativo llama a la función entry-point de todas las DLLs que estan cargadas actualmente con este valor. La llamada se hace en el contexto del hilo que termina. Hay casos en los cuales la función entry-point es llamada por un hilo que termina incluso si el DLL nunca se ha unido al hilo en cuestión.

El hilo era el hilo inicial en el proceso, así que el sistema llamó a la función entry-point con el valor DLL_PROCESS_ATTACH.

El hilo ya funcionaba cuando fue hecha una llamada a la función, así que el sistema nunca llamó a la función entry-point para ella"

Page 169: Multithreading a la manera de Delphi

Este comportamiento tiene dos efectos secundarios potencialmente desagradables.

No es posible, por lo general no perder de vista cuántos hilos están en la DLL sobre una base global, a menos que uno pueda garantizar que una aplicación carga la DLL antes de crear cualquier hilo hijo. Uno podría asumir equivocadamente que una aplicación que carga una DLL tendría el punto de entrada de DLL_THREAD_ATTACH llamado para los hilos ya existentes. éste no es el caso porque, garantizando que las uniones y las separaciones del hilo están notificadas a la DLL en el contexto del hilo que se une o que se separa, es imposible llamar al punto de entrada de la DLL en el contexto correcto de los hilos que están funcionando ya.

Puesto que el punto de entrada de la DLL puede ser llamado por varios hilos, las condiciones de carrera pueden ocurrir entre la función del punto de entrada y la inicialización de la DLL. Si un hilo se crea casi al mismo tiempo que la DLL es cargada por una aplicación, entonces es posible que el punto de entrada de la DLL se pudo llamar para el accesorio del hilo mientras que el cuerpo principal del hilo todavía se está ejecutando. Esta es la razón por la cual es siempre una buena idea instalar la función del punto de entrada como la última acción en la inicialización del DLL.

Los lectores se beneficiarían al observar que ambos efectos secundarios tienen repercusiones al decidir cuando fijar la variable IsMultiThread.

Control de Excepciones.

Page 170: Multithreading a la manera de Delphi

Al escribir aplicaciones robustas, el programador debe prepararse siempre para las cosas que van a ir mal. Lo mismo es cierto para la programación multihilo. La mayoría de los ejemplos presentados en esta tutorial en particular han sido relativamente simples, y el control de excepciones se ha omitido sobre todo para mantener claridad. En aplicaciones del mundo real, esto es probablemente inaceptable.

Recuerde que los hilos tienen su propia pila de llamadas. Esto significa que una excepción en un hilo no cae dentro de los mecanismos de control de excepción estándares de la VCL. En vez de levantar una caja de diálogo, una excepción no controlada abortará la aplicación. Como resultado de esto, el método Execute de un hilo es uno de los pocos lugares en donde puede ser útil crear a un contolador de excepciones que capture todas las excepciones. Una vez que una excepción se haya capturado en un hilo, trabajar con ella es también un poco diferente al manejo ordinario que hace la VCL. Puede no ser apropiado demostrar una caja de diálogo siempre. Muy frecuentemente, una táctica válida es dejar que el hilo comunique al hilo principal de la VCL el hecho de que una falla ha ocurrido, usando cualquiera de los mecanismos de comunicación usuales, y después dejar que el hilo de la VCL decida qué hacer. Esto es particularmente útil si el hilo de la VCL ha creado el hilo hijo para realizar una operación en particular.

A pesar de esto, hay algunas situaciones con los hilos donde el tratar casos de error puede ser particularmente difícil. La mayoría de estas situaciones ocurren cuando se usan hilos para realizar operaciones de fondo continuas. Recordando el capítulo 10, el BAB tiene un par de hilos con

Page 171: Multithreading a la manera de Delphi

operaciones de lectura y escritura en el hilo de la VCL a un buffer bloqueante. Si un error ocurre en cualquiera de estos hilos, puede mostrar una relación no muy clara con ninguna operación dentro del hilo de la VCL, y puede ser difícil comunicar la falla inmediatamente de regreso al hilo de la VCL. No solamente esto, una excepción cualquiera en éstos hilos probablemente romperan con el bucle de lectura y escritura en el que están, planteando la difícil pregunta de si estos hilos pueden ser recomenzados provechosamente. Lo mejor que puede hacerse es fijar un cierto estado que indique que todas las operaciones futuras fallarán, forzando al hilo principal que destruya y para volva a iniciar el buffer.

La mejor solución es incluir la posibilidad de tales problemas en el diseño original de la aplicación, y determinar las mejores tentativas de recuperación que se puedan hacer.

La BDE.

En el capítulo 7, indiqué que una solución potencial a los problemas de bloqueo es poner datos compartidos en una base de datos, y utilizar La BDE para realizar control de concurrencia. El programador debe observar que cada hilo debe mantener una conexión separada de la base de datos para que esto trabaje correctamente. Por lo tanto, cada hilo debe utilizar un objeto TSession separado para manejar su conexión a la base de datos. Cada aplicación tiene un componente TSessionList llamado Sessions para permitir que esto se haga fácilmente. La explicación detallada de sesiones múltiples está más allá del alcance de este documento.

Page 172: Multithreading a la manera de Delphi

Capítulo 14. Un problema del mundo real, y su solución.

En este capítulo:

El problema. La solución. Los archivos de la DLL y de interfaz. Los hilos lectores y escritores. Una interfaz basada en sockets.

El problema.

En los últimos años he estado escribiendo un raytracer distribuido. Este utiliza TCP/IP para enviar descripciones de las escenas que se renderizarán a través de una red desde un servidor central a un grupo de clientes. Los clientes renderizan la imagen, y después devuelven los datos al servidor. Algunos beta testers estaban interesados en probar el programa, pero mencionaron que no tenian el protocolo TCP/IP en su máquina. Decidí que sería útil escribir cierto código que emulara los seckets TCP, permitiendo la comunicación entre dos aplicaciones (cliente y servidor) en la máquina local.

Varias soluciones potenciales fueron investigadas. La más prometedora al principio parecía ser usar cañerías con nombre (named pipes). Desafortunadamente surgió un problema: Los protocolos que estaba usando encima de TCP/IP asumian que la semántica de la conexión se podía realizar en base a conexiones punto-a-punto (peer-to-peer): cualquier programa podría iniciar una conexión con la otra, y cualquier programa podría

Page 173: Multithreading a la manera de Delphi

desconectarse en cualquier momento. La conexión y desconexión eran perfectamente simétricas: Los protocolos usados encima de TCP realizaban tres formas de inicialización encima de aquella realizada en la capa TCP para negociar si una conexión podría ser cerrada, y si eso ocurría, cualquier extremo podría cerrar la conexión. Desafortunadamente, las cañerias con nombre, no proporcionaban la semántica correcta para la desconexión, y no se las arreglaban bién frente a varias situaciones de error.

La solución.

No tengo la intención de explicar la solución detalladamente, pero lectores más avanzados pueden encontrar interesante la lectura del código. Al final, decidí utilizar memoria compartida para la transferencia de datos, y poner toda la sincronización desde el principio. La solución fue implementada en 3 etapas.

Una DLL fue escrita la cual proporcionó una cañería bloqueante bidireccional entre las dos aplicaciones.

Se escribieron dos hilos, uno lector y otro escritor, para permitir el acceso asincrónico a las cañerías bloqueantes.

Una envoltura alrededor de los hilos fue escrita para proporcionar un interfaz asincrónico similar a los sockets nonblocking.

Los archivos de la DLL y de interfaz.

MCHPipe.dpr MCHPipeInterface2.pas MCHPipeTypes.pas

Page 174: Multithreading a la manera de Delphi

Este DLL es similar al ejemplo de buffer limitado que se encuentra en el capítulo 9. Volviendo a ver este código, puedo presumir solamente que lo debí escribír después de un par de semanas de hackear freneticamente con el C en el trabajo, porque está mucho más revuelto de lo que necesita ser. Un punto de interés es que los semáforos usados para bloquear operaciones no asumen que los buffers limitados son de cualquier tamaño en particular; en lugar de eso el estado se mantiene si los hilos lector o escritor están bloqueados o no.

Los hilos lectores y escritores.

MCHPipeThreads.pas MCHMemoryStream.pasLos hilos de la cañería son exactamente análogos a

los hilos lector y escritor en el BAB del capítulo 10. Las notificaciones no se utilizan para operaciones de escritura, en lugar de eso, el hilo escritor almacena temporariamente los datos internamente. Esto era permisible dado la semántica de protocolos de capa más altos.

Una interfaz basada en sockets.

MCHPipeSocket.pas MCHTransactions.pas MCHPipeTransactions.pasEsta es una interfaz de sockets no muy pura, y debe

ser razonablemente obvio a esos familiarizados con la programación de sockets de TCP. Puesto que esta implementación fue diseñada para trabajar específicamente con otros protocolos que escribí, es valioso incluyendo la capa de transacción de los

Page 175: Multithreading a la manera de Delphi

protocolos sobrepuestos así que usted puede ver cómo encaja el socket dentro del en el esquema de cosas.