Está en la página 1de 136

Multithreading - A la manera de Delphi

Esta gua fue escrita para quien est interesado en mejorar la respuesta en sus aplicaciones Delphi mediante el uso de hilos de ejecucin (Threads). Cubre aspectos desde los ms simples (para el novato) hasta algunos ms sofisticados en un nivel intermedio y algunos ejemplos traen aspectos que rozan el nivel avanzado. Se asume que el lector conoce la programacin en Object Pascal, incluyendo la programacin orientada a objetos y una comprensin del trabajo con eventos de programacin. Introduccin Captulo 1. Qu son los hilos de ejecucin? Porqu usarlos? Captulo 2. Crear un hilo de ejecucin en Delphi. Captulo 3. Sincronizacin bsica. Captulo 4. Destruccin simple de hilos. Captulo 5. Ms sobre destrucciones de hilos. Deadlock. Captulo 6. Ms sincronizacin: Secciones crticas y mutexes. Captulo 7. Gua de programacin de mutex. Control de concurrencia. Captulo 8. Clases Delphi seguras para entornos multihilo y prioridades. Captulo 9. Semforos. Administracin del flujo de datos. La relacin productor - consumidor. Captulo 10. E/S y flujo de datos: del bloqueo a lo asincrnico, ida y vuelta. Captulo 11. Sicronizadores y Eventos. Captulo 12. Ms dispositivos Win32 para la sincronizacin. Captulo 13. Usar hilos conjuntamente con el BDE, las excepciones y las DLLs. Captulo 14. Un problema del mundo real, y su solucin.

Introduccin
Esta gua fue escrita para quien est interesado en mejorar la respuesta en sus aplicaciones Delphi mediante el uso de hilos de ejecucin (Threads). Cubre aspectos desde los ms simples (para el

novato) hasta algunos ms sofisticados en un nivel intermedio y algunos ejemplos traen aspectos que rozan el nivel avanzado. Se asume que el lector conoce la programacin en Object Pascal, incluyendo la programacin orientada a objetos y una comprensin del trabajo con eventos de programacin.

Dedicatorias
Dedicado a tres miembros del departamento de Ciencias de la Computacin 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. Adems merece agradecimiento como directora de estudios, por explicar la ciencia de la computacin a mi propio ritmo. Me tom tres aos darme cuenta por mi mismo! Muchas gracias a Simons como tutor, por mostrarme que apesar de que los modernos sistemas operativos pueden ser endemoniadamente complicados los principios en los que se basan son muy simples. Merece adems 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 disertacin del proyecto. Arthur Norman nunca me enseo nada acerca de multitarea. Sin embargo me ense muchas otras cosas que me ayudaron a escribir las partes ms complicadas de esta gua. No hay limites a la excentricidad de los lectores universitarios. A pesar de que la mayora de la gente prefiere la simplicidad, hay cierto perverso placer en hacer las cosas de la forma complicada, especialmente si eres un cinico. Tambin merece una mencin por algunas de las mejores citas nunca ledas por un lector de ciencias de la computacin:

"Hay algo en los cursos lo cual no debe haber sido evidente hasta ahora, es la realidad..."

"Los tericos han probado que esto no tiene solucin, pero nosotros somos tres, y somos listos..." "La gente que no usa computadoras son ms sociables, rasonables y menos... retorcidos." "(Si la teora de la complejidad se sostiene por su ttulo) si eso se prueba ser as, ser el ganador como no muchos de ustedes intentarn las preguntas del examen." l hasta tiene su propia pgina de fans.

Lecturas recomendadas.
Ttulo: 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 ttulos tiles.

Ayuda para la navegacin.


Los escritos y los diagramas de esta gua estn contenidos en paginas HTML simples, una por cada captulo. Los cdigos fuente de ejemplo aparecen en ventanas emergentes. Necesitars habilitar javascript en tu navegador para verlos. Para facilitar la vista de los escritos y el cdigo 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.
Versin 1.1

Correccin de ortografa y errores de puntuacin en la prosa, y reescritura de explicaciones poco claras. Captulos 1-9 y 12 modificados.

Agregado historial de cambios y otros crditos a la tabla de contenidos. Captulo 12 renombrado. Agregado el captulo 14.

Crditos.
Muchas gracias a las siguientes personas por revisar, sugerir, corregir y mejorar esta gua.

Tim Frost Conor Boyd Alan Lloyd Bruce Roberts Bjrge Sther Craig Stuntz Jim Vaught Crditos de esta traduccin Andrs Galluzzi. Diego Romero. Descargar el tutorial completo (340 KB).

Captulo 1. Qu son los hilos de ejecucin? Porqu usarlos?


En este captulo:

Historia Definiciones Un ejemplo Tiempo compartido Porqu usar hilos de ejecucin?

Historia
En los primeros das de la computacin, toda la programacin era esencialmente tratada en un solo hilo. Los programas se creaban

perforando tarjetas o cintas, con las que formabas tu grupo de tarjetas que enviabas luego al centro local de computacin y, tras de un par de das, recibas otro grupo de tarjetas que, si estabas de suerte, contenan los resultados solicitados. Todo el procesamiento era por lotes, de ningn modo crtico, basado en la premisa de que el primero que llegaba era el primero en ser servido y cuando tu programa estaba corriendo, tena uso exclusivo del tiempo de la computadora. Las cosas han cambiado. El concepto de mltiples hilos de ejecucin aparece por primera vez con los sistemas de tiempo compartido, donde ms de una persona poda conectarse a una computadora central a la vez. Era importante asegurarse que el tiempo de procesamiento de la mquina era dividido adecuadamente entre todos los usuarios; los sistemas operativos de ese tiempo comienzan a usar los conceptos de proceso (process) e hilos de ejecucin (threads). Las computadoras de escritorio han visto un progreso similar. Los primeros DOS y Windows funcionaban con un nico hilo de ejecucin. Los programas, o funcionaban en forma exclusiva en la mquina, o no funcionaban. Con la creciente sofisticacin de las aplicaciones y la creciente demanda de computadoras personales, especialmente en lo relativo a la performance grfica y el trabajo en red, los sistemas operativos multiproceso y multihilo se volvieron algo comn. Las aplicaciones multihilo en las PCs fueron principalmente conducidas por la bsqueda de una mejor performance y usabilidad.

Definiciones
El primer concepto a definir es el del proceso. La mayora de los usuarios de Windows 95, 98 y NT intuyen bastante bien lo que es un proceso. Lo ven como un programa que corre en la computadora, coexistiendo y compartiendo el microprocesador, la memoria y otros recursos con otros programas. Los programadores saben que un proceso es invocado por un cdigo ejecutable, como tambin saben que ese cdigo 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 virtualizacin. Los procesos ejecutan mdulos de cdigo. Estos pueden ser independientes, en el sentido de que, los mdulos ejecutables de cdigo que competen al Windows Explorer son independientes de los del Microsoft Word. Sin embargo, stos tambin pueden ser compartidos, como es el caso de las DLLs. El cdigo de una DLL tpicamente es ejecutado en el contexto de muchos procesos diferentes, y habitualmente en forma simultnea. La ejecucin de instrucciones no es totalmente ordenada por los procesos: Microsoft Word no deja de abrir un documento sencillamente porque la cola de impresin est enviando algo a la impresora! Por supuesto, cuando diferentes procesos interactan entre s, el programador debe establecer un orden, un problema central que ser tratado luego. Nuestro prximo concepto es el del hilo de ejecucin (Thread). Los hilos de ejecucin fueron desarrollados cuando se vio claramente el deseo de tener aplicaciones que realizaran varias acciones con mayor libertad en cuanto al orden, posiblemente, realizando varias acciones en el mismo momento. En situaciones donde algunas acciones pudieran causar una demora considerable a un hilo de ejecucin (por ejemplo, cuando se espera que el usuario haga algo), era ms deseable que el programa siguiera funcionando, ejecutando otras acciones concurrentemente (por ejemplo, verificacin ortogrfica en segundo plano, o procesamiento de los mensajes que arriban desde la red). Sin embargo, crear todo un nuevo proceso para cada accin 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 programacin 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 operacin 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 dems ventanas son perfectamente usables. Obviamente, varias cosas se estn haciendo en el mismo momento, pero slo una copia de explorer.exe est corriendo. Esa es la esencia de la programacin multihilo.

Tiempo compartido.
En la mayora de los sistemas que soportan varios hilos de ejecucin, puede haber muchos usuarios haciendo llamadas simultneas al sistema. Para responder a todas estas demandas, se suele necesitar una cantidad de hilos de ejecucin que suele ser superior al nmero de procesadores que existen fsicamente en el sistema. Esto es posible gracias a que la mayora de los sistemas permiten compartir el tiempo del procesador, y as solucionar este problema. En un sistema con tiempo compartido, los hilos de ejecucin corren por un corto espacio y luego son invalidados; es decir, un temporizador en el hardware de la mquina se dispara, lo que causa que el sistema operativo re-evale qu hilos de ejecucin deben correr, pudiendo detener la ejecucin de los hilos en funcionamiento y continuando la ejecucin de otros hilos que haban quedado detenidos. Esto permite que las mquinas, an con un solo procesador, puedan correr muchos hilos de ejecucin. En las PCs, los tiempos compartidos tienden a ser de alrededor de 55 milisegundos.

Porqu usar hilos de ejecucin?


Los hilos de ejecucin no deben alterar la semntica de un programa. Ellos cambian simplemente los tiempos de operacin. Como resultado, son casi siempre usados como una solucin elegante a problemas de performance. Aqu hay algunos ejemplos de situaciones donde puedes usar hilos de ejecucin:

Realizar largos procesamientos: Cuando una aplicacin de Windows est realizando clculos, no puede procesar ningn mensaje. Como resultado, la pantalla no puede ser actualizada.

Realizar procesamientos en segundo plano: Algunas tareas pueden no ser crticas, 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 ejecucin permiten asegurar que la demora de E/S no demora otras partes no relacionadas con esto en tu aplicacin. Todos estos ejemplos tienen una cosa en comn: 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 aplicacin con slo un hilo de ejecucin haga uso de dos o ms procesadores. El captulo 3 explica esto con ms detalles. Compartir el tiempo con eficiencia: Usar hilos de ejecucin y prioridades en los procesos asegura una correcta justa del tiempo del microprocesador. El uso adecuado de los hilos de ejecucin convierte a lentas, duras y no muy disponibles aplicaciones en unas que tienen una brillante respuesta, eficiencia y velocidad, adems de que puede simplificar radicalmente varios problemas de performance y usabilidad.

Captulo 2. Crear un hilo de ejecucin en Delphi.


En este captulo:

Un diagrama de intervalos. Nuestro primer hilo no-VCL. Qu hace exactamente este programa? Cuestiones, problemas y sorpresas. Cuestiones en la inicializacin. Cuestiones en la comunicacin. Cuestiones de terminacin.

Un diagrama de intervalos.

Antes de meterse en los detalles de crear hilos de ejecucin, y ejecutar cdigo independiente del hilo principal de la aplicacin, es necesario introducir un nuevo tipo de diagrama ilustrativo de la dinmica de la ejecucin de hilos. Esto nos ayudar cuando comencemos a disear y crear programas multihilo. Considera esta simple aplicacin. La aplicacin tiene un hilo de ejecucin: 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 aplicacin a travs del tiempo. El progreso de este hilo est representado por una lnea, y el tiempo fluye en forma descendente en la pgina. Inclu una referencia en este diagrama que se aplica a todos los subsecuentes diagramas de hilos de ejecucin.

Ntese que este diagrama no indica mucho acerca de los algoritmos que se ejecutan. En cambio, ilustra el orden de los eventos a travs del tiempo y el estado de los hilos de ejecucin entre ese tiempo. La distancia entre diferentes puntos del diagrama no es importante, pero s

el ordenamiento vertical de esos puntos. Hay mucha informacin que se puede extraer de este diagrama.

El hilo en esta aplicacin no se ejecuta continuamente. Puede haber largos perodos de tiempo durante los cuales no recibe estmulos externos y no est llevando ningn clculo ni ningn otro tipo de operacin. La memoria y los recursos ocupados por la aplicacin existen y la ventana est an en la pantalla, pero ningn cdigo est siendo ejecutado por el microprocesador. La aplicacin es inicializada y el hilo principal es ejecutado. Una vez que se crea la ventana principal, no tiene ms trabajo que hacer y se reposa sobre una pieza de cdigo VCL conocida como el bucle de mensajes de la aplicacin que espera ms mensajes del sistema operativo. Si no hay ms mensajes para ser procesados, el sistema operativo suspende el hilo y el hilo de ejecucin est ahora suspendido. En un momento posterior, el usuario hace clic en el botn, para mostrar el mensaje de texto. El sistema operativo despierta (o reanuda) el hilo principal, y le entrega un mensaje indicando que un botn ha sido presionado. El hilo principal est ahora activo nuevamente. Este proceso de suspensin reanudacin ocurre varias veces en el tiempo. Ilustr una espera de confirmacin del usuario para cerrar la caja de mensajes y espera que el botn de cerrar sea presionado. En la prctica, 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 ejecucin, el VCL tiene una clase muy til, TThread, que abstrae la mayora de las tcnicas para crear un hilo, provee una simplificacin 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 gua razonable para crear diferentes tipos de hilos, de modo que no voy a mencionar mucho sobre las secuencias de men

necesarias para crear un hilo de ejecucin 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 nmero en particular es un nmero primo o no. Contiene dos units, una con un formulario convencional, y otra con un objeto hilo. Ms o menos funciona; de hecho tiene algunos rasgos indeseables que ilustran algunos de los problemas bsicos que los programadores multihilo deben considerar. Discutiremos el modo de evitar estos problemas ms tarde. Aqu est el cdigo fuente del formulario y aqu est el cdigo fuente del objeto hilo.

Qu hace exactamente este programa?


Cada vez que el botn Spawn es presionado, el programa crea un nuevo objeto hilo, inicializa algunos campos en el objeto y luego hace andar al hilo. Tomando el nmero ingresado, el hilo se aparta calculando si el nmero es primo y una vez que ha terminado el clculo, muestra una caja de mensajes indicando si el nmero es primo. Estos hilos son concurrentes, mas all de que se tenga una mquina uniprocesador o multiprocesador; desde el punto de vista del usuario, estos se ejecutan en forma simultnea. Adems, este programa no limita el nmero 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 nmero es primo, el tiempo que corre el hilo es directamente proporcional al tamao del nmero ingresado. He notado que con un valor de aproximadamente 224, el hilo necesita entre 10 y 20 segundos en completarse. Encuentra un valor que produzca una demora similar en tu mquina. Ejecuta el programa, introduce un nmero grande y haz clic en el botn. Inmediatamente introduce un nmero pequeo (digamos, 42) y haz clic en el botn nuevamente. Notars que el resultado para el nmero pequeo se produce antes que el resultado para el nmero

grande, an cuando comenzamos el hilo con el nmero grande primero. El diagrama de abajo ilustra la situacin.

Cuestiones, problemas y sorpresas.


Hasta este punto, el tema de la sincronizacin se ve espinoso. Una vez que el hilo principal llam a Resume en un hilo funcionando, el programa principal no puede asumir absolutamente nada sobre el estado del hilo en funcionamiento y viceversa. Es completamente posible que el hilo en funcionamiento complete su ejecucin antes de que el progreso del hilo principal de VCL termine. De hecho, para nmeros pequeos 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 Iniciacin, cuestiones de Comunicacin y cuestiones de Terminacin.

Cuestiones de iniciacin.
Delphi hace que lidiar con las cuestiones de iniciacin de hilos de ejecucin sea cosa fcil. 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 cdigo 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 garanta de que sern 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 quedara indefinido. Si no deseas crear el hilo suspendido, entonces estars pasndole el problema de la inicializacin a la siguiente categora: cuestiones de comunicacin.

Cuestiones de comunicacin.
Esto ocurre cuando tienes dos hilos que estn ambos corriendo y necesitas comunicarte entre ellos de algn modo. Este programa evade el problema simplemente no teniendo nada que comunicar entre los hilos separados. De ms esta decir que si no proteges todas tus operaciones en datos compartidos (en el ms estricto sentido de proteccin), tu programa no ser confiable. Si no tienes una adecuada sincronizacin o un slido 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 grficas en hilos independientes. An 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 resultarn en muchas horas de tensos debugueo, considerable confusin y eventuales internaciones en el hospital mental ms cercano. Hasta que aprendas la tcnica apropiada para hacer esto en los captulos 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 difcil! Veremos un modo sencillo de resolver aspectos de comunicacin a travs de la VCL en el prximo capitulo, y ms elegantes (y complicados) mtodos luego.

Cuestiones de terminacin.
Los hilos de ejecucin, al igual que otros objetos de Delphi, involucran la asignacin de memoria y recursos. No debera 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 liberacin 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 ejecucin del hilo al hilo principal de la VCL antes de terminar o no poseen ninguna informacin 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 captulo 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 ningn tipo de comunicacin 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 liberacin del hilo, dejando que el hilo se libere a s mismo. A pesar de

esto, el programa de ejemplo ilustra una caracterstica 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 aplicacin termina sin siquiera asegurarse de que no haya hilos funcionando. El cdigo de liberacin de Delphi y Windows hace que la aplicacin termine bien. Lo segundo que podra pasar no es tan bellamente manejable, ya que el hilo est suspendido en algn lugar dentro de las entraas del sistema de mensajera de Win32. Cuando la aplicacin termina, parece que Delphi hace una buena liberacin en ambas circunstancias. Sin embargo, no es un buen estilo de programacin 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 razn por la que es una buena idea tener una buena coordinacin de la salida del hilo hijo desde el hilo principal de

la VCL, an cuando no haga falta transferir ningn 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.

Captulo 3. Sincronizacin bsica.


En este capitulo:

Qu datos son compartidos entre los hilos? Atomicidad cuando se accede a datos compartidos. Problemas adicionales con la VLC. Diversin con mquinas multiprocesador. La solucin Delphi: TThread.Synchronize. Cmo 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 estn almacenados en un proceso y en un hilo bsico. Cada hilo tiene su propio contador de programa y estado del procesador. Esto quiere decir que los hilos progresan en forma independiente a travs del cdigo. Cada hilo tiene, a su vez, su propia pila, de modo que las variables locales son intrnsecamente locales para cada hilo y no poseen formas de sincronizarse por s estas de variables. Los datos globales del programa pueden ser libremente compartidos entre los hilos de ejecucin, por lo que, desde luego, existirn problemas de sincronizacin con estas variables. Es claro que, si una variable es globalmente accesible, pero slo un hilo de ejecucin la usa, no habr problemas con esto. La misma situacin 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 slo un hilo tiene un puntero a un objeto en particular, entonces slo 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 caracterstica no se usa mucho, porque es generalmente ms 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 cmo es que los hilos funcionan juntos, es necesario entender el concepto de atomicidad. Una accin o secuencia de acciones es atmica si la accin o secuencia es indivisible. Cuando un hilo realiza una accin atmica, esto lo ven los otros hilos como que la accin o no empez o ya se complet. No es posible para un hilo atrapar al otro en el acto. Si no se realiza ningn tipo de sincronizacin entre los hilos, entonces casi ninguna operacin es atmica. Tomemos un ejemplo sencillo. Considera este fragmento de cdigo. Qu podra ser ms sencillo? Desgraciadamente, an un fragmento de cdigo 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. An en una mquina uniprocesador, la ejecucin del este cdigo por mltiples hilos puede causar problemas. La razn por la que esto es as, es la administracin de tareas. Cuando existe slo un procesador, entonces slo un hilo se ejecuta por vez, pero el administrador de tareas de Win32 cambia el hilo en ejecucin 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 funcin, y an a medio camino en la ejecucin de una sentencia en particular. Imaginemos que dos hilos (X e Y) estn ejecutando el cdigo del ejemplo en una mquina uniprocesador. En un caso deseable, el programa puede estar corriendo y el administrador de tareas puede pasar el punto crtico, entregando el resultado esperado: A es incrementado por dos. Valor de la Instrucciones ejecutadas Instrucciones variable A en por el hilo X ejecutadas por el hilo Y memoria <otras instrucciones> Hilo suspendido 1 Lee A desde la memoria en Hilo suspendido 1 un registro del procesador. Incrementa en 1 el registro Hilo suspendido 1 del procesador. Escribe los contenidos del registro del procesador en Hilo suspendido 2 A (2) en memoria. <otras instrucciones> Hilo suspendido 2 CAMBIO DE HILO CAMBIO DE HILO 2 Hilo suspendido <otras instrucciones> 2 Lee A desde la memoria Hilo suspendido en un registro del 2 procesador. Incrementa en 1 el registro Hilo suspendido 2 del procesador. Escribe el contenido del Hilo suspendido registro del procesador en 3 A (3) en memoria. Hilo suspendido <otras instrucciones> 3 Sin embargo, este funcionamiento no es seguro y es una chance ms de cmo podra darse la ejecucin de los hilos. La ley de Murphy existe y la siguiente situacin puede ocurrir:

Valor de la Instrucciones ejecutadas Instrucciones variable A en por el hilo X ejecutadas por el hilo Y memoria <otras instrucciones> Hilo suspendido 1 Lee A desde la memoria en Hilo suspendido 1 un registro del procesador. Incrementa en 1 el registro Hilo suspendido 1 del procesador. CAMBIO DE HILO CAMBIO DE HILO 1 Hilo suspendido <otras instrucciones> 1 Lee A desde la memoria Hilo suspendido en un registro del 1 procesador. Incrementa en 1 el registro Hilo suspendido 1 del procesador. Escribe el contenido del Hilo suspendido registro del procesador en 1 A (2) en memoria. CAMBIO DE HILO CAMBIO DE HILO 2 Escribe los contenidos del registro del procesador en Hilo suspendido 2 A (2) en memoria. <otras instrucciones> Hilo suspendido 2 En este caso, A no es incrementado en dos, sino slo en uno. Oh, diablos! Si A fuera la posicin de una barra de progreso, entonces quizs esto no sera un problema, pero si es algo ms importante, como un contador de nmero 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 condicin de carrera.

Problemas adicionales con la VLC.

La VCL no posee proteccin para estos conflictos. Esto significa que los cambios de hilos en ejecucin, puede suceder cuando uno o ms hilos estn ejecutando cdigo 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 ejecucin. Un cambio de hilo en ejecucin en un momento inadecuado puede provocar estragos, corrompiendo los contadores de referencia de manejadores compartidos, destruyendo no slo datos, sino tambin las conexiones entre los componentes. An cuando los hilos no estn ejecutando cdigo 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 cdigo en la VCL que (de momento) muestra una caja de dilogo 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 mgicamente como resultado de mostrar la caja de dilogo o escribir en un archivo. Esto es obviamente inaceptable; solo un hilo puede ejecutar cdigo VCL, o un mecanismo debe ser encontrado para asegurarse de que los hilos separados no interfieran entre s.

Diversin con mquinas multiprocesador.


Por suerte para los programadores, el problema no es ms complejo para mquinas con ms de un microprocesador. Los mtodos de sincronizacin que proveen Delphi y Windows funcionan igual de bien ms all del nmero de procesadores. Los que hicieron el sistema operativo Windows tuvieron que escribir cdigo extra para lidiar con mquinas multiprocesador: Windows NT 4 informa al usuario en el momento de arranque si est usando un kernel multiprocesador o uniprocesador. Como sea, para el programador, todo esto queda oculto. No necesitas preocuparte acerca de cuntos procesadores tiene la mquina, ms de lo que te tienes que preocupar por que chipset utiliza el mother.

La solucin Delphi: TThread.Synchronize.


Delphi provee una solucin que es ideal para que principiantes escriban hilos de ejecucin. Es simple y evita todos los problemas mencionados antes. TThread tiene un mtodo llamado Synchronize. Este mtodo toma como parmetro otro mtodo que no lleva parmetros, que tu desees ejecutar. Con esto tienes la garanta de que el cdigo en el mtodo sin parmetros 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 cdigo en el mtodo sin parmetros sucede en el momento en que es llamado synchronize. Umm. Suena confuso? Puede ser. Lo ilustrar con un ejemplo. Modificaremos nuestro programa de nmeros primos, de modo que en vez de mostrar una caja de mensajes, ste indicar si el nmero 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 mtodo (UpdateResults) en nuestro hilo que mostrar el resultado en el memo, y en vez de llamar a ShowMessage, llamaremos a Synchronize, pasando el nuevo mtodo como parmetro. La declaracin del hilo y las partes modificadas, ahora se ven as. Ntese que UpdateResults accede a ambos, el formulario principal y la variable con el resultado. Desde el punto de vista del hilo principal, el formulario principal parece haber sido modificado en respuesta a un evento. Desde el punto de vista del hilo que calcula los nmeros primos, la variable de resultado es accedida durante la llamada a Synchronize.

Cmo funciona esto? Qu hace Synchronize?


El cdigo que es invocado cuando se llama a Synchronize, puede realizar cualquier cosa que el hilo principal de VCL pueda hacer. Adems, puede modificar datos asociados con su propio objeto hilo de manera segura, sabiendo que la ejecucin de su propio hilo est en un

punto particular (el llamado a synchronize). Lo que realmente ocurre es bastante elegante, y es ilustrado mejor por otro diagrama.

Cuando se llama a synchronize, el hilo de clculo de nmeros 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 operacin, o puede que se est ejecutando. Si no esta suspendido en un estado totalmente inactivo (en el bucle de espera de mensajes de la aplicacin principal), entonces el hilo de clculo de nmeros primos espera. Una vez que el hilo principal se vuelve inactivo, la funcin sin parmetros pasada a synchronize se ejecuta en el contexto del hilo principal de VCL. En nuestro caso, la funcin sin parmetros se llama UpdateResults y acta sobre un memo. Esto asegura que no habr conflictos con el hilo principal de VCL, y en esencia, el procesamiento de este cdigo es parecido a cualquier cdigo de Delphi que ocurriera en el hilo principal de VCL en respuesta a un mensaje enviado por la aplicacin. 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 cdigo 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 funcin. De hecho, una llamada a Synchronize parece ser un mensaje ms al hilo principal de VCL, y una llamada a la funcin de clculo de nmeros primos. Los hilos estn en posiciones conocidas y no se ejecutan concurrentemente. No hay ninguna condicin de carrera. Problema resulto.

Sincronizado a hilos no-VCL.


El ejemplo anterior mostr como se puede hacer un simple hilo para interactuar con el hilo principal de VCL. De hecho, ste le roba tiempo al hilo principal de VCL para hacerlo. Esto no es as arbitrariamente entre los hilos. Si tienes dos hilos no VCL, X e Y, no puedes llamar a synchronize en X solamente, y luego modificar datos almacenados en Y. Es necesario llamar a synchronize en ambos hilos cuando se est leyendo o escribiendo datos compartidos. En efecto, esto significa que los datos son modificados por el hilo principal de VCL, y todos los dems hilos sincronizan con el hilo principal de VCL cada vez que necesitan acceder a sus datos. Esto podra 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.

Captulo 4. Destruccin simple de hilos.


En este captulo

Consideraciones de completado, terminacin y destruccin de hilos. Terminado prematuro de hilos. El evento OnTerminate.

Terminacin controlada de hilos Efoque 1.

Consideraciones de completado, terminacin y destruccin de hilos.


En el capitulo 2 se dio un lineamiento de algunos de los problemas relacionado con la finalizacin 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 estn fuertemente relacionados. Si un hilo no tiene que comunicar nada al hilo principal de la VCL cuando haya terminado, o si uno usa la tcnica descripta en el captulo 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 terminacin de todos los hilos en l, con posibles consecuencias indeseables. Si el hilo slo escribe en la memoria, o se comunica con otras partes de la aplicacin, 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 informacin 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 cdigo para liberar el hilo). Dos mecanismos sern presentados luego. Hay un punto ms para tener en cuenta: Terminar un hilo antes de que su curso de ejecucin 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 ms 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 operacin muy larga, y el usuario decide salir de la aplicacin, o la operacin debe ser abortada. TThread provee un mecanismo simple para soportar esto en la forma del mtodo Terminate, y la propiedad Terminated. Cuando un hilo es creado su propiedad terminated se establece a false. Cuando se llama al mtodo terminate de un hilo, la propiedad terminated para ese hilo es ahora true. Es la responsabilidad de todos los hilos de verificar peridicamente si se les ha solicitado terminar, y si as fuera, salir limpiamente. Ntese 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 ledo el valor de la propiedad terminated y comenz su finalizacin. La propiedad Terminated es simplemente una seal, diciendo por favor termina tan rpido como sea posible. El diagrama de abajo ilustra esta situacin.

Cuando se disean los objetos hilos, se deber considerar leer la variable terminated cuando sea necesario. Si el hilo se bloquea, como resultado de algn mecanismo de sincronizacin de los que discutiremos luego, podra tener que sobrecargar el mtodo terminate para desbloquear el hilo. En particular, recodar llamar primero al mtodo heredado (inherited) terminate, antes de desbloquear el hilo, si espera que su prxima verificacin de terminated devuelva verdadero. Pronto veremos ms de esto. Como ejemplo, aqu hay una pequea modificacin al hilo que calcula los nmeros primos del capitulo anterior, para asegurarnos de que verifica el valor de terminated. He asumido que es aceptable para el hilo devolver un resultado incorrecto cuando se establece la propiedad terminated.

El evento OnTerminate.
El evento OnTerminate ocurre cuando un hilo realmente ha terminado su ejecucin. No ocurre cuando es llamado el mtodo 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 mtodos pasados a synchronize. Adems, si uno desea ejecutar algunas operaciones de la VCL con un hilo que se libera

automticamente a s mismo, entonces este es el lugar de hacerlo. La mayora de los nuevos programadores de hilos de ejecucin van a encontrar esto como la mejor manera de lograr que un hilo no-VCL transfiera sus datos de vuelta al VCL, con un mnimo de alboroto, y sin requerir llamadas explcitas a synchronize.

Como pueden ver en el diagrama de arriba, OnTerminate trabaja bastante parecido a como lo hace Synchronize, y es prcticamente idntico semnticamente a poner una llamada a Synchronize al final del hilo. El principal uso de esto es que, mediante el uso de indicadores, como La aplicacin 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 slo cuando todos los dems hilos han terminado. Aqu hay algunos detalles de sincronizacin 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 ms tarde.

Terminacin controlada de hilos Efoque 1.


En este ejemplo, tomaremos el cdigo del programa de nmeros primos del captulo 3 y lo modificaremos de modo que el usuario no pueda cerrar la aplicacin cuando hay otros hilos ejecutndose. Esto se vuelve simple. De hecho, no necesitamos modificar el cdigo del hilo ni en lo ms mnimo. Nosotros simplemente agregaremos una referencia a un campo de conteo en el hilo principal, incrementndolo 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 aplicacin, mostraremos una caja de dilogo de alerta si fuera necesario. El ejemplo muestra lo simple de este enfoque: todo el cdigo concerniente con tomar cuenta de los nmeros de hilos en ejecucin sucede en el hilo principal de VCL, y el cdigo es esencialmente disparado por un evento, lo mismo que como sera con cualquier otra aplicacin Delphi. En el prximo capitulo, vamos a considerar un enfoque sensiblemente ms complicado, que es beneficioso cuando se usan mecanismos de sincronizacin ms avanzados.

Captulo 5. Ms sobre destrucciones de hilos. Deadlock.


En este capitulo:

El mtodo WaitFor. Terminacin controlada de hilos Enfoque 2. Una rpida introduccin al pasaje de mensajes y notificaciones. WaitFor puede resultar en largas demoras. Haz notado el bug? Evitando esta particular manifestacin de Deadlock.

El mtodo WaitFor.

El evento OnTerminate, discutido en el captulo anterior, es muy til si ests usando hilos que inicializas y luego los olvidas, con destruccin automtica. Que pasa si, en cierto punto de la ejecucin del hilo principal de la VCL, quieres asegurarte de que todos los dems hilos hayan terminado? La solucin a esto es el mtodo WaitFor. Este mtodo es til si: El hilo principal de VCL necesita acceder al objeto hilo en funcionamiento antes de que su ejecucin haya terminado, y ya no se pueda leer o modificar datos en el hilo. Forzar la terminacin de un hilo cuando se termina el programa no es una opcin viable. Bastante sencillo. Cuando el hilo A llama al mtodo WaitFor del hilo B, el hilo A queda suspendido hasta que el hilo B termina su ejecucin. 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. Tpicamente esto ocurre cuando el programa termina, donde el hilo principal de VCL llamar el mtodo Terminate en todos los hilos no-VCL y luego al mtodo WaitFor en todos los hilos no-VCL antes de salir.

Terminacin controlada de hilos Enfoque 2.


En este ejemplo, modificaremos el cdigo del programa de nmeros primos de modo que slo un hilo se ejecute por vez, y el programa espere hasta que el hilo complete su ejecucin 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 cdigo del formulario principal. Como puede ver, hay varias diferencias con el ejemplo anterior:

Tenemos un nmero mgico declarado al inicio del unit. Este es un nmero arbitrario de mensaje, y su valor no es importante; es el nico mensaje en la aplicacin con este nmero.

En vez de tener un conteo de hilos, mantenemos una referencia explcita a un hilo y slo un hilo, apuntado por la variable FThread del formulario principal. Slo queremos que un hilo se ejecute por vez, ya que slo tenemos una nica variable apuntando al hilo que realizar el trabajo. Por este motivo, el cdigo de creacin del hilo verifica si hay hilos ejecutndose, antes de crear otros. El cdigo de creacin del hilo no establece la propiedad FreeOnTerminate a verdadero. En cambio, el hilo principal de VCL liberar el hilo en funcionamiento ms tarde. El hilo principal tiene un manejador de mensajes definido que espera que el hilo en ejecucin se complete y entonces lo libera. De igual modo, el cdigo ejecutado cuando el usuario desea liberar el formulario espera que el hilo en ejecucin se complete y lo libera. Habiendo notado estos puntos, aqu esta el hilo que har el trabajo. Nuevamente, hay algunas diferencias con el cdigo presentado en el capitulo 3.

La funcin IsPrime verifica ahora si se solicit que el hilo termine, resultando en una rpida salida si la propiedad terminated es establecida. La funcin Execute verifica si se produjo una terminacin anormal. Si la terminacin fue normal, entonces usa synchronize para mostrar los resultados, y enva un mensaje al formulario principal solicitando que el formulario principal lo libere.

Una rpida introduccin al pasaje de mensajes y notificaciones.


Bajo circunstancias normales, el hilo es ejecutado, corre por su curso, usa synchronize para mostrar los resultados y luego enva un mensaje al formulario principal. Este envo de mensaje es asincrnico: el formulario principal toma el mensaje en algn punto en el futuro. PostMessage no suspende el trabajo del hilo en ejecucin, 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 ms. En cambio, esto simplemente acta como una

notificacin, un gentil recordatorio para el formulario principal de que debe liberar el hilo tan rpido 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 an existe y, si existe, espera a que se complete su ejecucin. Este paso es necesario porque si bien es sabido que el hilo en ejecucin est terminando (no hay muchas sentencias ms luego del PostMessage), esto no es una garanta. 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 operacin de Synchronize del diagrama. Adems, la llamada a PostMessage se muestra como que ocurre en algn momento antes de que el hilo completa su funcionamiento de modo de ilustrar el funcionamiento de la operacin WaitFor.

En captulos posteriores se va a cubrir la ventaja de enviar mensajes con mayor detalle. Es suficiente decir hasta este punto que esta tcnica es muy til cuando se trata de comunicarse con el hilo VCL. En un caso anormal de funcionamiento, el usuario intentar salir de la aplicacin, y confirmar que desea salir inmediatamente. El hilo principal establecer la propiedad terminated del hilo en proceso, lo que se espera que provoque una terminacin 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 liberacin es como el caso anterior. El diagrama de abajo ilustra el nuevo caso.

Muchos lectores estarn perfectamente felices a estas alturas. Sin embargo, los problemas vuelven a aparecer, y como es comn cuando consideramos la sincronizacin multihilo, el diablo est en los detalles.

WaitFor puede resultar en largas demoras.


El beneficio de WaitFor es tambin su mayor desventaja: suspende el hilo principal en un estado en el que no puede recibir mensajes. Esto significa que la aplicacin no puede realizar ninguna de las operaciones

normalmente asociadas con el procesamiento de mensajes: la aplicacin no re-dibujar, no se re-dimensionar ni responder a ningn estmulo externo cuando est esperando. Tan pronto como el usuario lo note, pensar que la aplicacin se colg. Esto no es un problema en el caso de un hilo que termina normalmente; llamando a PostMessage, la ltima operacin en el hilo en funcionamiento, nos aseguramos de que el hilo principal no tendr que esperar mucho. Sin embargo, en el caso de una terminacin anormal del hilo, la cantidad de tiempo que el hilo principal pierde en este estado depende de que tan frecuentemente verifique el hilo de ejecucin la propiedad terminate. El cdigo fuente para PrimeThread tiene una lnea marcada Line A. Si se le quita el fragmento and not terminated, podr experimentar que sucede al finalizar la aplicacin durante la ejecucin de un clculo que dure mucho tiempo. Hay algunos mtodos avanzados para suprimir este problema que involucra a las funciones Win32 de espera de mensajes, una explicacin de este mtodo se puede encontrar visitando http://www.midnightbeach.com/jon/pubs/MsgWaits/Msg Waits.html. En suma, es simple escribir hilos que verifican la propiedad Terminated con cierta regularidad. Si esto no es posible, entonces es preferible mostrarle algunas advertencias al usuario acerca de la potencial irresponsabilidad de la aplicacin (a la Microsoft Exchange).

Haz notado el bug? WaitFor y Synchronize: una introduccin 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 aplicacin caiga en un Deadlock. Deadlock es un fenmeno donde no hay problemas de algoritmos en la aplicacin, pero toda la aplicacin se detiene, muerta en el agua. El caso general es que Deadlock ocurra cuando un hilo espera por el otro en forma cclica. El hilo A esta esperando por el hilo B para completar algunas operaciones, mientras que el hilo C espera por el hilo D, etc. etc. Al final de la lnea, el hilo D estar esperando por el hilo A para completar algunas operaciones.

Desgraciadamente el hilo A no puede completar la operacin porque est suspendido. Esto es el equivalente en computacin 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 ejecucin si el hilo de clculo llama a Synchronize poco tiempo antes de que el hilo principal llame a WaitFor. Si esto sucediera, entonces el hilo de clculo 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 clculo se complete. Deadlock ocurrir. Tambin es posible que el hilo principal de VCL llame a WaitFor poco tiempo antes de que el hilo de clculo llame a Synchronize. Dando una implementacin simplista, esto tambin resultara 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 excepcin en el hilo de clculo, rompiendo el Deadlock y finalizando el hilo.

La programacin del ejemplo, como est, se vuelve bastante indeseable. El hilo de clculo llama a Synchronize si verifica que Terminated est es falso poco antes de terminar su ejecucin. El hilo principal de la aplicacin establece terminated poco antes de llamar a

WaitFor. De modo que, para que ocurra un Deadlock, el hilo de clculo 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. Ms 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 mquina. El 99.9% de las veces, un cierre forzado funcionar, y una en mil veces, todo se bloquear: exactamente el tipo de problema que necesitamos evitar a toda costa. El lector recordar que anteriormente le mencion que ninguna sincronizacin 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. Algn lector interesado en duplicar el problema del Deadlock, puede hacer relativamente fcil, modificando los siguientes fragmentos del cdigo 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 ejecucin demore cerca de 20 segundos, y forzar la salida de la aplicacin poco tiempo despus de que el hilo fue creado. El lector puede desear tambin ajustar el tiempo que el hilo principal de la aplicacin se suspende, de modo de saber el correcto ordenamiento de los eventos:

El usuario comienza cualquier hilo de clculo. El usuario intenta salir y dice: S, quiero salir ms all de que haya un hilo en funcionamiento. El hilo principal de la aplicacin se suspende (Line C) El hilo de clculo eventualmente llega al final de la ejecucin y llama a Synchronize. (asistido por las modificaciones en las lneas A y B).

El hilo principal de la aplicacin se reactiva y llama a WaitFor.

Evitando esta particular manifestacin de Deadlock.


El mejor modo de evitar esta forma de Deadlock, es no usar WaitFor y Synchronize en la misma aplicacin. 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 ms 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 clculo. Las modificaciones necesarias para esto son: Quitar el mtodo DisplayResults del hilo. Agregar una propiedad al hilo de clculo. Modificar el manejador de mensajes en el formulario principal. Aqu hay cambios relevantes. Con esto termina la discusin de los mecanismos de sincronizacin comunes a todas las versiones Win32 de Delphi. An no he discutido dos mtodos: TThread.Suspend y TThread.Resume. Estos son discutidos en el capitulo 10. Los siguientes captulos exploran las facilidades del API Win32, y posteriores versiones de Delphi. Sugiero que, una vez que el usuario haya asimilado los aspectos bsicos de la programacin multihilo en Delphi, se tome el tiempo de estudiar estos mecanismos ms avanzados, ya que son una buena manera, ms flexible, que trabajar con los mecanismos nativos de Delphi, y permiten al programador coordinar hilos de ejecucin en un modo ms elegante y eficiente, as como reducir las posibilidades de escribir cdigo que pueda caer en Deadlocks.

Captulo 6. Ms sincronizacin: Secciones crticas y mutexes.

En este captulo:

Limitaciones de la sincronizacin. Secciones crticas. Qu significa todo esto para el programador Delphi? Puntos de inters. 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 sincronizacin.
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 an cuando esto no es estrictamente necesario. Si las aplicaciones hacen un uso frecuente de Synchronize, el hilo principal de VCL se vuelve un cuello de botella y no una verdadera ganancia de performance. Si Synchronize es usado para comunicar indirectamente dos hilos en ejecucin, ambos hilos pueden quedar suspendidos esperando por el hilo principal de VCL. Synchronize puede causar Deadlock si el hilo principal de VCL espera por algn otro hilo. En la parte de las ventajas, Synchronize tiene una por sobre la mayora de los dems mecanismos de sincronizacin:

Casi cualquier cdigo puede ser pasado a Synchronize, incluso cdigo VCL inseguro entre hilos. Es importante recordar porque los hilos son usados en la aplicacin. La principal razn para la mayora de los programadores Delphi es que quieren que sus aplicaciones permanezcan siempre con capacidad de respuesta, mientras se estn realizando otras operaciones que pueden

llevar ms tiempo o usan transferencias de datos con bloqueo o E/S. Esto generalmente significa que el hilo principal de la aplicacin 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 mltiples hilos. Los hilos que realizan el trabajo pueden realizar operaciones con archivos, bases de datos, pero rara vez usarn descendentes de TControl. A la vista de esto, Synchronize es un caso perdido. Muchos hilos necesitan comunicarse con la VCL de una manera sencilla, como realizar transferencias de cadenas de datos, o ejecutar querys de bases de datos y devolver una estructura de datos como resultado del query. Volviendo atrs, al capitulo 3, notamos que slo 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 leda peridicamente 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 slo un hilo por vez modifica este recurso compartido, de modo de eliminar las condiciones de carrera y hacer las operaciones en los recursos compartidos atmicas. Esta propiedad es conocida como exclusin mutua. Hay muchas primitivas de sincronizacin que pueden ser usadas para forzar esta propiedad. La ms simple de esta es conocida como Mutex. Win32 provee la primitiva mutex, y una pariente cercana de esta, la Seccin Crtica (Critical Section). Algunas versiones de Delphi poseen una clase que encapsula las llamadas a secciones crticas Win32. Esta clase no ser discutida aqu, ya que su funcionalidad no es comn a todas las versiones de 32 bits de Delphi. Los usuarios de esa clase han de tener algunas dificultades usando los mtodos correspondientes en la clase para lograr los mismos efectos que los discutidos aqu.

Secciones Crticas.

La seccin crtica es una primitiva que nos permite forzar la exclusin mutua. El API Win32 soporta varias operaciones sobre esta: InitializeCriticalSection. DeleteCriticalSection. EnterCriticalSection. LeaveCriticalSection. TryEnterCriticalSection (Windows NT unicamente). Las operaciones InitializeCriticalSection y DeleteCriticalSection pueden considerarse como algo muy parecido a la creacin y destruccin de objetos en memoria. Por ende, es sensato dejar la creacin y destruccin de secciones crticas a un hilo en particular, normalmente el que exista ms tiempo en memoria. Obviamente, todos los hilos que quieran tener un acceso sincronizado usando esta primitiva debern tener un manejador o puntero a esta primitiva. Esto puede ser directo, a travs de una variable compartida, o indirecto, quiz porque la seccin crtica est embebida en un clase hilo segura, a la que ambos hilos puedan acceder.

Una vez que el objeto seccin crtica 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 estndar en el tema de las sincronizaciones, estas operaciones son tambin conocidas como WAIT y SIGNAL, o LOCK y UNLOCKrespectivamente. Estos trminos alternativos son tambin usados para otras primitivas de sincronizacin, y tienen significados equivalentes. Por defecto, cuando se crea la seccin crtica, , ninguno de los hilos de la aplicacin tiene posesin de ella. Para obtener posesin, un hilo debe llamar a EnterCriticalSection, y si la seccin crtica no pertenece a nadie, entonces el hilo obtiene su posesin. Es entonces cuando, tpicamente, el hilo realiza operaciones sobre recursos compartidos (la parte crtica del codigo, ilustrada por una doble lnea), y una vez que ha terminado, libera su posesin mediante un llamado a LeaveCriticalSection.

La propiedad que tienen las secciones crticas es que slo un hilo por vez puede ser propietario de alguna de ellas. Si un hilo intenta entrar a una seccin crtica cuando otro hilo est an en la seccin crtica, el que intenta entrar quedar suspendido, y solamente se reactivar cuando el otro hilo abandone la seccin crtica. Esto nos provee la exclusin mutua necesaria con los recursos compartidos. Ms de un hilo puede ser suspendido, esperando ser propietario en algn momento, de modo que las secciones crticas pueden ser tiles para sincronizaciones entre ms de dos hilos. A modo de ejemplo, aqu est lo que sucedera si cuatros hilos intentaran tener acceso a la misma seccin crtica en momentos muy cercanos.

Como deja en claro el grfico, slo un hilo esta ejecutando cdigo crtico por vez, de modo que no hay problemas de carreras ni de atomicidad.

Qu significa todo esto para el programador Delphi?


Esto significa que, ms all de que uno no est realizando operaciones con la VCL, sino slo 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, slo necesita estar fuera de la seccin crtica. Las secciones crticas no saben ni les preocupa saber si un hilo es el hilo principal de la VCL o una instancia de un objeto TThread, de modo que uno puede usar las secciones crticas entre cualquier par de hilos. El programador de hilos puede ahora (prcticamente) usar WaitFor en forma segura, evitando problemas de Deadlock.

El ltimo punto no es absoluto, ya que an 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 seccin crtica. Como veremos luego, suspender hilos por largos perodos de tiempo mientras est en una seccin crtica es normalmente una mala idea. Ahora que la teora fue explicada adecuadamente, presentar un nuevo ejemplo. Este es un poco ms elegante e interesante que el programa de nmeros primos. Cuando empieza, intenta buscar nmeros primos empezando por el 2, y sigue hacia arriba. Cada vez que encuentra un nmero 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 cdigo del formulario principal. Es bastante similar a los ejemplos anteriores con respecto a la creacin del hilo, pero hay algunos miembros extra en el formulario principal que deben ser inicializadas. StringSection es la seccin crtica que controla el acceso al recurso compartido entre hilos. FStringBuf es una lista de strings que acta como buffer entre el formulario principal y el hilo en proceso. El hilo en proceso enva los resultados al formulario principal agregndolos a esta lista de strings, que es el nico recurso compartido en este programa. Finalmente tenemos una variable boleana, FStringSectInit. Esta variable acta como un verificador, asegurndose que los objetos necesarios en la sincronizacin estn realmente creados antes de ser usados. Los recursos compartidos son creados cuando comenzamos un hilo de procesamiento y se destruyen poco tiempo despus de que estemos seguros que el hilo de procesamiento ha salido. Ntese que pese a que las listas de strings actan como buffer que son asignados dinmicamente,debemos usar WaitFor al momento de destruir el hilo, para asegurarnos que el hilo de procesamiento no usa ms 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 situacin donde dos hilos se estn esperando uno al otro. La lnea de razonamiento para probar esto es bien simple:

1. El hilo de procesamiento slo espera cuando intenta ganar acceso a la seccin crtica. 2. El hilo del programa principal slo espera cuando est esperando que el hilo de procesamiento termine. 3. El programa principal no espera cuando tiene posesin de la seccin crtica. 4. Si el hilo de procesamiento est esperando por la seccin crtica, el programa principal abandonar la seccin crtica antes de esperar por algn motivo al hilo de procesamiento. Aqu est el cdigo del hilo de procesamiento. El hilo de procesamiento busca a travs de sucesivos enteros positivos, tratando de encontrar alguno que sea primo. Cuando lo encuentra, toma posesin de la seccin crtica, modifica el buffer, abandona la seccin crtica y luego enva un mensaje al formulario principal indicando que hay datos en el buffer.

Puntos de inters.
Este ejemplo es ms 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 tambin algunas caractersticas del cdigo 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 esttico vs. dinmico.

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 envo 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 sincronizacin del hilo que enlace a un mensaje de windows con una actualizacin 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 despus de la actualizacin. Esto significa que el hilo principal del programa siempre recibe mensajes de actualizacin del buffer despus de una actualizacin del buffer. Por este motivo, es imposible que los datos permanezcan en el buffer por una indeterminada cantidad de tiempo. Si los datos estn actualmente en el buffer, el hilo de procesamiento y el hilo principal estn en algn punto en el proceso desde el envo a la recepcin de mensajes de actualizacin del buffer. Ntese 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 ms recientes, provocando que los resultados ms recientes queden atascados en el buffer por algn 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 comunicacin tambin cuentan. Si el hilo principal est ocupado actualizando por un largo perodo de tiempo, es posible que los mensajes se apilen en el la cola, de modo que recibimos los mensajes de actualizaciones mucho tiempo despus de que el hilo de procesamiento enviara esos mensajes. En la mayora 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 botn 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 sincronizacin y el buffer, y luego, subsecuentemente, recibir mensajes que se han apilado durante algn tiempo. En el ejemplo mostrado, verifiqu este problema, asegurndome que la seccin crtica y el objeto buffer existen antes de procesar los mensajes (La lnea de cdigo comentada Not necessarily the case!). Esta consideracin tiende a ser suficiente para la mayora de las aplicaciones.

Consideraciones de control de flujo y lista de ineficiencias.


Atrs, en el capitulo 2, dije que una vez que se crean hilos, no existe ninguna sincronizacin implcita 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 manifestacin del nivel del problema de sincronizacin 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 rpido 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 nmeros primos pequeos, 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 ejecucin de algunos hilos es balanceada de modo que la tasa de entradas en el buffer y la tasa de salidas estn medianamente balanceadas. El ejemplo de arriba es particularmente simple, pero ocurre en muchos otros casos. Casi cualquier E/S o mecanismo de transferencia de datos entre hilos o procesos incorpora algn tipo de control de flujo. En casos simples, esto simplemente puede involucrar alguna pieza excepcional de dato en trnsito, 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 ms complejos, el hilo puede ejecutarse en diferentes mquinas y el buffer puede estar compuesto de buffers internos en esas mquinas, 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 pgina web, el protocolo TCP arbitra entre las dos mquinas, asegurndose que ms all del microprocesador o la velocidad de disco, toda la transferencia de datos ocurre a una tasa que puedan manejar las dos mquinas [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 garanta de hierro. Otro aspecto relacionado con el control de flujo es que, en el caso del ejemplo de arriba, el tamao 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 nmero 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 lmite. Intenta quitar la sentencia que establece la prioridad del hilo. Notars que el hilo de procesamiento genera resultados mas rpido de lo que el hilo principal de VCL pueda procesar, lo que hace a la lista de strings muy larga. Esto, adems, lentifica ms 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 mquina comenzar a retorcerse y todo se detendr ruidosamente. Tan catico es, que cuando prob el ejemplo, no pude conseguir que Delphi respondiera a mis solicitudes para salir de la aplicacin, 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 nmero de potenciales gremlins. Soluciones ms robustas a este problema son discutidas en la segunda parte de esta gua.

Mutexes.
Un mutex funciona exactamente del mismo modo que las secciones crticas. La nica diferencia en las implementaciones Win32 es que la seccin crtica esta limitada para ser usada con solamente un proceso. Si tienes un programa que usa varios hilos, entonces la seccin crtica 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 crticas. Pese a que el API Win32 provee un rango ms variado de funciones para trabajar con mutexes y otros objetos de sincronizacin que sern explicados aqu, las siguientes funciones son anlogas a las descriptas para secciones crticas ms arriba: CreateMutex / OpenMutex CloseHandle WaitForSingleObject(Ex) ReleaseMutex Estas funciones estn bien documentadas en los archivos de ayuda del API Win32, y sern discutidas en ms detalle luego.

[1] El protocolo TCP tambin realiza muchas otras funciones raras y maravillosas, como copiar con datos perdidos y el optimizado del tamao de las ventanas de modo que el flujo de la informacin no slo se ajusta a las dos mquinas en los extremos de la conexin, sino tambin a la red que las une, mientras mantiene una mnima latencia y maximizando la conexin. Tambin posee algoritmos de back-off para asegurarse que varias conexiones TCP puedan compartir una conexin fsica, sin que ninguna de ellas monopolice el recurso fsico.

Captulo 7. Gua de programacin de mutex. Control de concurrencia.


En este capitulo:

Momento para introducir un poco de estilo. Deadlock en funcin 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 adquisicin de mutex. Fuera de la cacerola y en el fuego! Evitando el Deadlock al modo vago y dejando que Win32 lo haga por ti. Atomicidad en la composicin 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 mayora de los ejemplos presentados en este tutorial eran bastante puntuales y preparados. Cuando diseamos componentes reusables, o las bibliotecas para una gran aplicacin multihilo, una concepcin de vuelo de guila no es apropiada. El programador o diseador de componentes necesitan construir clases que tengan seguridad para la programacin multihilo en s mismos, es decir, clases que asuman que podran ser accedidas desde diferentes hilos y poseer los mecanismos internos necesarios para asegurarse de que los datos se mantengan consistentes. Para hacer esto, el diseador de componentes necesita estar al tanto de algunos problemas que surgen cuando se usan mutex en aplicaciones cada vez ms 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. Ntese que cada vez que se mencione mutex de aqu en ms, lo mismo vale para las secciones crticas; omitir mencionar las secciones crticas en cada caso para abreviar.

Deadlock en funcin del ordenamiento de mutex.


Si un programa posee ms de un mutex, entonces ser sorprendentemente sencillo provocar un Deadlock, con un cdigo de sincronismo descuidado. La situacin ms comn es cuando existen dependencias cclicas por el orden en que los mutex son adquiridos. Esto es generalmente conocido en la literatura acadmica como el problema de la cena de los filsofos. Como vimos antes, el criterio de un Deadlock es que todos los hilos estn esperando a otro para liberar el objeto de sincronizacin. El ejemplo ms 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 ms delicada con una cadena de dependencias, como la ilustrada ms abajo con cuatro hilos y cuatro mutexes, A a D.

Obviamente, situaciones como esta no son aceptables en la mayora de las aplicaciones. Hay muchas maneras de evitar este problema, y un montn de tcnicas para aliviar problemas de dependencia de este tipo, haciendo mucho ms sencillo evitar situaciones de Deadlock.

Evitando el Deadlock de un hilo, dejando que la espera de timeout.

Las funciones de Win32 para lidiar con mutex no requieren que un hilo espere por siempre para adquirir un objeto mutex. La funcin 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 cdigo de error indicando que a la espera se le acab el tiempo (time-out). Cuando usamos mutex para forzar el acceso sobre una regin crtica del cdigo, uno no espera tpicamente que el hilo tenga que esperar mucho tiempo, y un time-out establecido para suceder en pocos segundos debera ser apropiado. Si tu hilo usa este mtodo, entonces deber, por supuesto, poder manejar situaciones de error en forma adecuada, quizs volvindolo a intentar o abandonndolo. Desde luego que los usuarios de las secciones crticas no tienen este lujo, ya que las funciones de espera de las funciones crticas esperan por siempre.

Evitando el Deadlock de un hilo imponiendo un orden en la adquisicin de mutex.


Si bien es una buena idea ser capaz de manejar situaciones de error al adquirir un mutex, es una buena prctica asegurarse que las situaciones de Deadlock no sucedan en primer lugar. Como este tipo de Deadlock es provocado por dependencias cclicas, puede ser eliminado al imponer un orden en la adquisicin de mutexes. Este ordenamiento es muy sencillo. Digamos que tenemos un programa con mutexes M1, M2, M3, Mn, donde uno o ms de estos mutex pueden ser adquiridos por los hilos en el programa. El Deadlock no ocurrir ya que para algn mutex arbitrario Mx, los hilos slo intentarn adquirir el mutex Mx si no tienen posesin 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 terminologa parece apropiada cuando un mutex est asociado con un dato, y el acceso atmico a ese dato es necesario. Uno debera notar que esto efectivamente significa que cada hilo obtiene el mutex antes de acceder a un objeto, y abandona el mutex despus de haber accedido: la operacin es idntica a las discutidas

anteriormente, el nico cambio est en la terminologa, que para esta coyuntura, es ms apropiada para un modelo orientado a objetos. En esencia, Objeto.Lock puede ser considerado completamente equivalente a EnterCriticalSection(Objecto.CriticalSection) o quizs 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 esttica, no cambia, y puede ser leda libremente por los hilos sin ningn tipo de bloqueo. Los hilos que operan en esta estructura de datos quieren hacer alguna de estas cosas: Leer un tem, bloquendolo, leyendo los datos, y luego desbloquendolo. Escribir en un tem, bloquendolo, escribiendo los datos, y luego desbloquendolo. Comparar dos tems, bloquendolos primero en la lista, luego realizando la comparacin y desbloquendolo. Un simple pseudo-cdigo para estas funciones, ignorando los tipos, manejos de excepciones y otros aspectos que no son centrales, puede verse como algo as.

Imaginmonos 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 podra 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 solucin sencilla sera bloquear primero el tem cuyo nmero sea el menor, u ordenar los ndices de entrada, realizar los bloqueos y ajustar los

resultados de la comparacin apropiadamente. Sin embargo, una situacin ms interesante es cuando un objeto contiene detalles de otro objeto con el que es necesario hacer la comparacin. En esta situacin, 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 comparacin. Todo muy fcil. El problema ocurre cuando el segundo objeto tiene mayor ndice en la lista que el primero. No podemos bloquearlo inmediatamente, porque de hacerlo, estaramos 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 comparacin indirecta, representativo de esta discusin.

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 operacin compuesta: la operacin en s no es ms atmica. Solucione a este problema son discutidas ms abajo, en la pgina.

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 diseadores de Sistemas Operativos en Microsoft, nos han provisto de una manera de solucionar el problema mediante otra funcin de sincronizacin de Win32: WaitForMultipleObjects(Ex). Esta funcin le permite al programador esperar para adquirir muchos objetos de sincronizacin (incluyendo mutex) de una vez. En particular, esto le permite a un hilo esperar hasta que uno o todo un grupo de objetos estn libres (en el caso de mutex, el equivalente seria sin propietario), y luego adquirir la

propiedad de los objetos sealados. 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 ningn objeto es adquirido o todos son adquiridos atmicamente, de modo que es imposible un caso de deadlock de esta manera. Este enfoque tambin tiene algunas desventajas. La primera desventaja es que como todos los objetos de sincronizacin deben estar libres antes de que alguno de ellos sea adquirido, es posible que un hilo que espere por un gran nmero de objetos, no adquiera la propiedad por un largo perodo de tiempo si otros hilos estn adquiriendo los mismos objetos de sincronizacin de a uno. Por ejemplo, en el diagrama de abajo, el hilo ms a la izquierda espera por los mutexes A, B y C, mientras que otros tres hilos adquieren cada mutex en forma individual. En el peor de los casos, el hilo esperando por muchos objetos puede que nunca adquiera la propiedad. La segunda desventaja es que an 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 comn con mtodo de time-out para evitar el Deadlock, es que no es posible usar esta funcin si se estn usando secciones crticas, la funcin EnterCriticalSection no le permite especificar una cantidad de tiempo de espera, ni tampoco devuelve un cdigo de error.

Atomicidad en la composicin de operaciones optimismo versus pesimismo en el control de concurrencia.


Cuando pensamos en el ordenamiento de mutex anterior, nos encontramos en una situacin 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 comnmente un modo vlido de lidiar con el problema en situaciones complejas donde la sobrecarga de estructuras de datos por varios hilos no es demasiado elevada. En el caso presentado antes, podemos verificar trivialmente esto, guardando una copia local de los datos y verificando que an son vlidos cuando volvemos a bloquear ambos objetos en el orden requerido. Aqu esta la rutina modificada. Con estructuras de datos ms complicadas, uno puede recurrir algunas veces a IDs nicos globales o marcado de versiones en piezas de cdigo. Como nota personal, recuerdo haber trabajado con un grupo de otros estudiantes en un proyecto de fin de ao de la universidad, donde este enfoque funcion muy bien: un nmero secuencial era incrementado cuando una pieza de datos era modificada (en este caso los datos consistan en anotaciones en un diario multiusuario). Los datos eran bloqueados mientras se lea, luego se mostraban al usuario y si el usuario editaba los datos, el nmero era comparado con el obtenido por el usuario en la ltima lectura, y la actualizacin era abandonada si los nmeros no coincidan.

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 bsquedas, debern bloquear primero la lista. Esto provee una solucin 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 diseo alternativo del bloqueo. Un hilo puede querer leer y modificar los contenidos de un objeto de la lista, pero sin modificar el objeto existente ni su posicin en la lista. Esta operacin 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 fantstico ya que, an 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). Ntese 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 operacin 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 ms simple que el mencionado en la seccin anterior:

Bloquear la lista. Buscar el primer objeto. Bloquear el primer objeto. Buscar el segundo objeto. Bloquear el segundo objeto. Desbloquear la lista. Realizar la comparacin.

Desbloquear los objetos (en cualquier orden). Como vern, en la operacin de comparacin, no he hecho ninguna restriccin 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 ocurrirn nunca. Y no ocurrirn nunca porque cuando un hilo bloquea un objeto mutex, l ya tiene posesin del mutex de la lista, y con esta posesin, puede bloquear varios objetos si no libera el mutex de la lista. El bloqueo compuesto en varios objetos resulta atmico. Como resultado de esto, podemos modificar el criterio de Deadlock de arriba:

El Deadlock no ocurrir ya que para algn mutex arbitrario Mx, los hilos slo intentarn adquirir el mutex Mx si no tienen posesin de alguno de los mutex de mayor prioridad, esto es M(x+1) Mn. Adems, 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 adquisicin que no lleva un orden, si todas las operaciones de bloqueo en esos mutex son atmicas, normalmente mediante el bloqueo de las operaciones dentro de una seccin crtica (obtenida por el bloqueo de otro mutex).

Evitando agujeros en el esquema de bloqueo.


No es ninguna novedad a estas alturas, que el ejemplo de arriba es tpico de un cdigo de bloqueo que es muy sensible al ordenamiento. Ms 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 ests seguro que tu programa funcionar en Windows NT (o 2K, XP, 2003), entonces el API de Windows provee en efecto una solucin al problema de operaciones compuestas cuando se desbloquean y vuelven a bloquear objetos. La llamada del APISignalObjectAndWait te permite marcar atmicamente (o liberar) un objeto de sincronizacin, y esperar por otro. Conservando

estas dos operaciones atmicas, se puede transferir un estado de bloqueo de un objeto a otro, mientras que se asegura que ningn 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 bsico 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. Pequeas aplicaciones de escritorio pueden funcionar habitualmente con enfoques no tan complicados. Hay varias maneras de tirar la toalla. No se preocupe por la eficiencia, y bloquee todo. Meta todos los datos en la BDE. Bloquear todos los datos compartidos es habitualmente til, si uno est dispuesto a sacrificar eficiencia. La mayora 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 mltiples hilos, lo que significa que puedes acceder a tus datos sin ningn problema desde hilos separados. Si usas un motor de bases de datos, entonces debers estudiar algo sobre administracin de transacciones, por ejemplo, las semnticas reservation, y el uso de premature, commit y rollback, pero recuerda que esto es slo el enfoque basado en transacciones para solucionar problemas de concurrencia, y sencillamente la otra cara de la misma moneda; la mayor parte de la programacin difcil (incluido los dolores de cabeza) lo han hecho por ti. El uso de la BDE con hilos de ejecucin ser tratado luego.

Captulo 8. Clases Delphiseguras para entornos multihilo y prioridades.


En este capitulo:

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 administracin del flujo de los datos. Monitores. Clases Interlock. Soporte multihilo en la VCL. TThreadList TSynchroObject TCriticalSection TEvent y TSimpleEvent TMultiReadExclusiveWriteSincronizer Gua para programadores de clases seguras en entornos multihilo. Administracin 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 programacin multihilo, tienden a incluir su sincronizacin como parte de la lgica de la aplicacin. Como demostr el capitulo anterior, es increblemente fcil que se generen errores en la lgica de sincronizacin, y disear un esquema de sintonizacin separado para cada aplicacin es mucho trabajo. Un nmero relativamente pequeo de mecanismos de sincronizacin son usados una y otra vez: casi todos los hilos destinados a E/S, comunican los datos a travs de buffers compartidos, y el uso de listas y colas con sincronizacin incorporada en situaciones de E/S es muy comn. Estos factores indican que hay muchas ventajas si creamos libreras de objetos y estructuras de datos

que son seguras en entornos multihilos: los problemas involucrados en la comunicacin entre hilos son difciles, pero un pequeo nmero 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. Cdigos en DLLs que accede a variables nicas del sistema deben poseer sincronizacin de hilos, an si la DLL no posee ningn objeto hilo. Dado que los programadores Delphi usarn las facilidades del lenguaje (clases) para permitir un desarrollo modular y re-utilizacin de cdigo, estas DLLs tendrn clases, y estas clases deben ser seguras para entornos multihilo. Algunas pueden ser bastante simples, quizs 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 sincronizacin 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 tamaos diferentes, programadores con una razonable experiencia en Delphi estarn 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 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 clasificacin 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 ms simple de clases para entornos multihilo. Tpicamente, la clase que es ampliada, tiene una funcionalidad bastante limitada y est contenida en s misma. En el caso ms 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 automticamente. Cul 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 administracin del flujo de los datos.


Estas son una pequea extensin de las de arriba y tienden a ser una clase buffer: listas, pilas y colas. Adems, para mantener la atomicidad, estas clases pueden realizar un control automtico del flujo de datos en los hilos que operan en el buffer. Esto consiste frecuentemente en suspender los hilos que intentan leer de un buffer vaco o escribir en uno que est lleno. La implementacin de estas clases se trata con ms detalle en el capitulo 10. Un rango de operaciones puede ser soportado por esta clase: en un extremo del buffer se proveern operaciones que no realizarn ningn 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 asincrnicas, pero provistas de notificaciones por call-back o mensajera cuando una operacin 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 lgico en el camino hacia las clases administradoras del flujo de datos. Estos tpicamente permiten acceso concurrente a los datos, lo que requiere una sincronizacin y bloqueo ms 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 categora: tpicamente, un complicado bloqueo y administracin de transacciones es provisto para permitir un alto grado de concurrencia cuando se acceden a datos compartidos, con una mnima prdida 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 composicin, y tambin proveen garantas 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 mltiples hilos accedan a mltiples 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 administracin 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 operacin entrar en conflicto y, una vez que la operacin 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, an cuando haya fallas de energa o en el sistema operativo.

Clases Interlock.
Las clases Interlock son nicas en esta clasificacin, porque stas no contienen ningn dato. Algunos mecanismos de bloqueo son muy tiles en el sentido de que el cdigo que forma parte del sistema de bloqueo puede ser fcilmente separado del cdigo 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 atmicas en un recurso. El modo de operacin de esto ser examinado ms abajo, y el funcionamiento interno de la clase ser visto en captulos 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 hgalo 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 versin 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 descripcin 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 pequeo 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 comunicacin entre hilos. La clase TThreadList realiza sincronizaciones de las mas bsicas requeridas por hilos de ejecucin. En adicin a los mtodos presentes en TList, se agregaron dos mtodos extra: Lock y Unlock. El uso de estos debe ser bastante obvio para los lectores que han visto como se trabaja a travs de los captulos anteriores: La lista es bloqueada antes de ser manipulada, y desbloqueada luego. Si un hilo realiza mltiples operaciones en a lista que necesitan ser atmicas, entonces la lista permanece bloqueada. La lista no realiza ninguna sincronizacin implcita 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 puado de mtodos virtuales, Adquire y Release que son usados en todas las clases bsicas de sincronizacin en Delphi, dado que la realidad ltima de los objetos simples de sincronizacin

tienen el concepto de posesin como fue discutido previamente. Las secciones crticas y las clases evento son derivadas de esta clase.

TCriticalSection
Esta clase no necesita ninguna explicacin detallada. Sospecho su inclusin en Delphi como simplemente destinada a aquellos programadores Delphi con fobia al API Win32. No es nada valiosa, ya que provee cuatro mtodos: Adquire, Release, Enter y Leave. Los dos ltimos no hacen ms que llamar a los dos primeros, slo 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 sincronizacin. En lugar de forzar la exclusin mutua, se usan para hacer que un nmero variable de hilos esperen hasta que algo suceda, y 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 estn muy relacionados con los semforos, y son discutidos en captulos posteriores.

TMultiReadExclusiveWriteSincronizer
Este objeto de sincronizacin es muy til en situaciones conde un gran nmero 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 captulos 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 exclusin mutua se necesita siempre. Una exclusin mutua completa insiste en que slo un hilo puede realizar alguna operacin en algn momento. Podemos relajarnos con esto, si nos vemos que hay dos tipos principales de conflictos entre hilos:

Escribir despus de que se haya hecho una lectura. Escribir despus de que se haya hecho otra escritura. El conflicto de escribir despus de que se haya hecho una lectura ocurre cuando un hilo escribe en una parte de un recurso despus de que otro hilo ha ledo ese valor, y asume que es vlido. Este es el tipo de conflictos ilustrado en el captulo tres. El otro tipo de conflicto ocurre cuando dos hilos escriben en un recurso compartido, uno despus del otro, sin que el segundo hilo haya percibido la escritura anterior. Esto resulta en que la primera escritura es eliminada. Por supuesto, algunas operaciones son perfectamente legales, como leer despus de leer o leer despus 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 mnimos son:

Varios hilos pueden leer al mismo tiempo. Slo un hilo puede escribir por vez. Si un hilo est escribiendo, entonces ningn hilo puede estar leyendo. El sincronizador TMultiReadExclusiveWriteSincronizer fuerza este criterio al proveer cuatro funciones: BeginRead, BeginWrite, EndRead, EndWrite. Al llamar estas funciones antes y despus de escribir, se consigue la sincronizacin apropiada. En lo que se refiere al programador de aplicaciones, puede verlo ms bien como una seccin crtica, con la excepcin de que los hilos la adquieren para leer o para escribir.

Gua para programadores de clases seguras en entornos multihilo.


Si bien los captulos 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 disean clases seguras para entornos multihilo, me parece valioso incluir una serie de simples consejos que le ayudarn mucho.

Quin hace el bloqueo?

S econmico cuando bloquees recursos. 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 slo una funcionalidad simple, es normalmente lo mejor entregar esta responsabilidad al usuario de la clase. Seguramente usarn varias instancias de esta clase, y al darle la responsabilidad del bloqueo, un se asegura que los Deadlocks inesperados no ocurrirn, y uno tambin le da la posibilidad de elegir cunto bloquea, de modo de maximizar la simplicidad o la eficiencia. Para clases ms 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 cdigo, pueden en principio limitar sensiblemente los beneficios de usar hilos de ejecucin. Por supuesto, no hay nada de malo con empezar hacindolo 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 cdigos de error, entones este es un enfoque posible. Alternativamente, podrs desear escribir una clase de abstraccin que encapsule los objetos de sincronizacin Win32 que puedan llegar a emitir un mensaje de error cuando esto ocurra. En cualquier caso, siempre ten en cuenta usar el bloque try finally para asegurarte que en el caso de una falla, los objetos de sincronizacin son dejados en un estado conocido.

Administracin de prioridades.
Todos los hilos son creados igual, pero algunos son ms iguales que otros. El administrador de tareas debe dividir el tiempo del

microprocesador entre todos los hilos en funcionamiento en la mquina en todo momento. Para hacer esto, necesita tener alguna idea de cunto tiempo del microprocesador deseara usar cada hilo, y cun importante es que un hilo en particular sea ejecutado cuando est disponible para correr. La mayora de los hilos se comportan de dos maneras posibles: su tiempo de ejecucin est atado al microprocesador o a la E/S. Los hilos atados al microprocesador tienden a realizar un gran nmero de operaciones en segundo plano. Absorbern todos los recursos del microprocesador disponibles para ellos, y raramente se suspendern para esperar por comunicaciones de E/S con otros hilos. Con bastante frecuencia, su tiempo de ejecucin no es crtico. Por ejemplo, un hilo en un programa de grficos por computadoras puede realizar una operacin de manipulacin 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 operacin toma doce o treinta segundos para ejecutarse, y ningn otro hilo en el sistema est esperando urgentemente un resultado de este hilo. 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 pequeas cantidades de procesamiento. Con mucha frecuencia estn suspendidos (bloqueados) en E/S, y cuando reciben una entrada, tpicamente corren por un corto perodo de tiempo, para procesar esa entrada en particular, y en forma prcticamente inmediata se vuelven a suspender cuando no hay ms entradas disponibles. Un ejemplo de esto es el hilo que procesa las operaciones de movimiento del ratn y actualiza la posicin del cursor. Cada vez que el ratn es movido, el hilo se toma una pequea fraccin de segundos en actualizar el cursor y vuelve a ser suspendido. Hilos de este tipo tienden a ser ms crticos con respecto al tiempo: no corren por largos perodos de tiempo, pero cuando corren, es bastante crtico que respondan de inmediato. En la mayora de los sistemas GUI, es inaceptable que el cursor permanezca son responder, an por cortos

perodos de tiempo, y de hecho el hilo de actualizacin del cursor del ratn es crtico con respecto tiempo. Los usuarios de WinNT notarn que an cuando la computadora est trabajando muy duro en operaciones intensas en el microprocesador, el cursor del ratn sigue respondiendo inmediatamente. Todos los sistemas operativos multihilo que utilizan un mecanismo de preferencia, Win32 incluido, proveen soporte para estos conceptos, permitindole al programador asignar prioridades a los hilos. Tpicamente, los hilos con mayor prioridad tienen a ser los atados a E/S y los hilos con menor prioridad, los que estn atados al microprocesador. La implementacin de las prioridades de los hilos de ejecucin en Win32 es ligeramente diferente de las implementaciones de (por ejemplo) UNIX, de modo que los detalles discutidos aqu son especficos para Win32.

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


La mayora de los sistemas operativos asignan una prioridad a los hilos de ejecucin, para saber cunta atencin del microprocesador debe recibir cada hilo. En Win32, la prioridad de cada hilo de ejecucin es calculada en el momento, a partir de un nmero 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 estmulo) 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 excepcin 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 ms 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, 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 tendrn 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 estmulo. La prioridad de estmulo es un mecanismo que el administrador de tareas usa para probar y tomar cuenta del comportamiento de los hilos en tiempo de ejecucin. Pocos hilos sern 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. Adems, a los hilos que poseen manejadores de ventanas que estn como ventanas en segundo plano tambin se les da un ligero fomento para probar y mejorar la respuesta al usuario.

De qu prioridad debo hacer mi hilo?


Con una bsica comprensin de las prioridades, podemos intentar asignar prioridades realmente tiles a los hilos en nuestra aplicacin. Ten en cuenta que, por defecto, el hilo de la VCL se ejecuta en un nivel de prioridad normal. Generalmente, la mayora de las aplicaciones Delphi estn 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 ejecucin. La mayora 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 operacin 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 mquina resultar para el usuario con mucha ms capacidad de respuesta que si el hilo fuera dejado a un nivel de prioridad normal. Tpicamente, un usuario es mucho ms tolerante a sensibles demoras para que se completen las operaciones en hilos de ejecucin de baja prioridad: podr hacer otras cosas mientras se completan estas tareas, y la mquina lo mismo que la aplicacin se mantendrn con una capacidad de respuesta normal.

Captulo 9. Semforos. Administracin del flujo de datos. La relacin productor - consumidor.


En este capitulo:

Semforos. Qu hay de los conteos por encima de uno? Secciones no tan crticas. Un nuevo uso para los semforos: administracin del flujo de datos y control de flujo. El buffer limitado. Una implementacin Delphi del buffer limitado. Creacin: Inicializando los semforos correctamente. Operacin: valores correctos de espera. Destruccin: Liberando todo. Destruccin: Las sutilezas continan. Los accesos a los manejadores de sincronizacin deben ser sincronizados!

Administracin de manejadores Win32. Una solucin. Usando el buffer limitado: un ejemplo. Un par de puntos finales

Semforos.
Un semforo es otro tipo de primitiva de sincronizacin, que es ligeramente ms general que el mutex. Usado en el modo ms 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 sincronizacin ms avanzado. Primero que nada, reconsideremos el comportamiento de los mutexes. Un mutex puede estar marcado o sin marcar. Si esta marcado, un operacin de espera en el mutex no provoca bloqueo. Si no esta marcado, una operacin de espera en el mutex provoca bloqueo. Si el mutex no est marcado, entonces es propiedad de un hilo en particular, y adems, slo un hilo por vez puede poseer el mutex. Los semforos pueden ser creados para actuar precisamente de la misma manera. En lugar de tener el concepto de propiedad, un semforo tiene un conteo. Cuando ese conteo es mayor que 0, el semforo es marcado, y las operaciones de espera en l no producen bloqueos. Cuando la cuenta es 0, el semforo no est marcado, y las operaciones de espera en l sern bloqueadas. Un mutex seria esencialmente un caso especial de semforo cuya cuenta es slo 0 o 1. De igual modo, los semforos pueden ser pensados como fantsticos mutexes que pueden tener ms de un propietario por vez. Las funciones en el API Win32 para lidiar con semforos son muy similares a las que se usan para lidiar con mutexes.

CreateSemaphore. Esta funcin es similar a CreateMutex. En lugar de una marca indicando que el hilo que est creando el mutex quiere ser su propietario inicialmente, esta funcin toma un argumento indicando el conteo inicial. Crear un mutex con la propiedadinicial es similar a crear un semforo 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 semforo con un conteo de 1: en ambos casos, uno y slo un hilo no ser bloqueado cuando espera para tomar posesin del objeto de sincronizacin. Funciones de espera. Las funciones de espera son idnticas en ambos casos. Con mutex, una espera exitosa da la propiedad del mutex al hilo. Con semforos, una espera exitosa decrementa el conteo del semforo, 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 semforo, o activar el nmero apropiado de hilos bloqueados en el semforo o ambos. La siguiente tabla muestra como el cdigo usando mutexes puede ser convertido en cdigo usando semforos, y las equivalencias entre ambos. Mutexes MiMutex := CreateMutex(nil, FALSE, <name>); MiMutex := CreateMutex(nil, TRUE, <name>); WaitForSingleObject(M iMutex, INFINITE); ReleaseMutex(MiMutex) ; Semforos. MiSemaforo := CreateSemaphore(nil, 1, 1, <name>); MiSemaforo := CreateSemaphore(nil, 0, 1, <name>); WaitForSingleObject(MiS emaforo, INFINITE); ReleaseSemaphore(MiSema foro, 1); CloseHandle(MiSemaforo) CloseHandle(MiMutex); ; Como un ejemplo sencillo, aqu estn las modificaciones necesarias para el cdigo presentado en el captulo 6, de modo que el programa use semforos en lugar de secciones crticas.

Qu hay de los conteos por encima de uno? Secciones no tan crticas.


Permitir que un semforo tenga conteos mayores que uno es algo anlogo a permitir que los mutexes tengan ms de un propietario. Los semforos permiten que sean creadas secciones crticas, lo que permite que un cierto nmero de hilos est dentro de una regin particular de cdigo, o acceda a un objeto en particular. Esto es principalmente til en situaciones donde un recurso compartido consiste de un nmero de buffers o un nmero 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 regin particular del cdigo. Un semforo es creado con un valor de conteo inicial y mximo de 3, asumiendo que ningn hilo est presente en la regin crtica. La ejecucin de cinco hilos intentando acceder a un recurso compartido puede verse como algo as:

Esta aplicacin particular de los semforos probablemente no sea muy til para los programadores Delphi, principalmente porque hay muy pocas estructuras estticamente dimensionadas en nivel de aplicacin. Sin embargo, resulta considerablemente ms til dentro del SO, donde los manejadores, o recursos como buffers de un sistema de archivos suelen ser asignados estticamente cuando arranca de la computadora.

Un nuevo uso para los semforos: administracin del flujo de datos y control de flujo.
En el captulo 6, se perfilaba la necesidad de un control de flujo cuando se pasaban datos entre los hilos. Nuevamente, en elcaptulo 8, se habl de este tema cuando discutimos los monitores. Este captulo

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 cclico, es decir, contendr un nmero fijo de entradas y tendr un puado de punteros get y put para indicar donde los datos deben ser insertados y removidos en el buffer. Hay tpicamente cuatro operaciones permitidas en el buffer: Create Buffer: El buffer y cualquier mecanismo asociado de sincronizacin 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 ms 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 vaco, el intento del hilo por tomar un tem ser bloqueado (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 sern necesarios cuando se manipulan datos compartidos. Sin embargo, podemos usar semforos para realizar las operaciones de bloqueo necesarias cuando el buffer est lleno o vaco, eliminando la necesidad de chequear rangos o an conservar una cuenta de cuntos tems hay en el buffer. Para hacer esto, necesitamos un pequeo cambio de mentalidad. En lugar de esperar por un semforo y luego liberarlo cuando se realizan operan relacionadas con

el buffer, usaremos el contador en el par de semforos para tomar cuenta de cuntas entradas en el buffer estn vacas o llenas. Llamemos a estos semforos EntriesFree y EntriesUsed. Normalmente, dos hilos interactan 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) debera intervenir de modo de crear y destruir el buffer.

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 semforo 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 semforo 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, permitindole al hilo productor agregar otro tem. Bloqueando el hilo apropiado, ya fuera que el buffer se torne vaco o lleno, detiene a uno u otro hilo de pasarse de vueltas. Dado un tamao de buffer de N, el hilo productor puede estar slo a N tems de distancia del hilo consumidor antes de que ste sea suspendido, y de igual modo, el hilo consumidor no puede estar ms de N tems atrs. Esto nos trae algunos beneficios: Un hilo no puede sobre-producir, de modo que se evita el problema visto en el captulo 6, donde tenamos la salida de un hilo colocndose en cola en una lista de un tamao cada vez mayor. El buffer es de tamao 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 pequeos bucles que no hacen nada ms que esperar por ms 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 mximo de 4 entradas en l, y es inicializado de modo que todas las entradas estn libres. Se pueden dar muchos caminos de ejecucin, 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.

Accin del hilo lector Comienza el hilo. Espera(EntriesUsed) bloquea. Suspendido.

Accin del hilo escritor Hilo inactivo (no activado)

Contador de entradas libres 4 4

Contador de entradas en uso 0 0

Espera(EntriesFree) pasa. Agrega Item. Marca(EntriesUsed) Espera(EntriesFree) pasa Agrega Item. Marca(EntriesUsed) Espera(EntriesFree) pasa Agrega Item. Marca(EntriesUsed) Espera(EntriesFree) pasa Agrega Item. Marca(EntriesUsed) Espera(EntriesFree) bloquea. Suspendido. Espera(EntriesUsed) completa. Quita Item. Marca(EntriesFree) Espera(EntriesUsed) pasa. Quita Item. Marca(EntriesFree) Espera(EntriesUsed) pasa. Quita Item. Marca(EntriesFree) Espera(EntriesUsed)

3 3 2 2 1 1 0 0 0 0 1 1 2 2 3 3

0 1 1 2 2 3 3 4 4 3 3 2 2 1 1 0

pasa. Quita Item. Marca(EntriesFree) Espera(EntriesUsed) bloquea. Suspendido.

4 4

0 0

Una implementacin Delphi del buffer limitado.


Aqu est la primer implementacin Delphi del buffer limitado. Como es acostumbrado, en la implementacin aparecen un par de puntos que llevan una mencin, y esto tiene algunos problemas que resolveremos luego.

Qu valores deben ser entregados a la llamada de creacin del semforo? Qu tan larga debe ser la espera en el mutex o la seccin crtica? Qu tan larga debe ser la espera en el semforo? Cul es la mejor manera de destruir limpiamente al buffer?

Creacin: Inicializando los semforos correctamente.


Con esta implementacin del buffer limitado, los datos son almacenados como una serie de punteros con ndices de lectura y escritura en l. Para facilitar la depuracin, he acordado que si el buffer tiene N entradas, ser declarado lleno cuando sean llenadas N1 entradas. Esto se hace con bastante frecuencia en los buffers cclicos, donde los ndices de lectura y escritura son evaluados para determinar si el buffer est absolutamente lleno, de modo que es comn en el cdigo de los buffers cclicos tener siempre una entrada libre de modo que estas dos condiciones puedan ser distinguidas. En nuestro caso, como estamos usando semforos, esto no es estrictamente necesario. Sin embargo, me adher a esta convencin de modo de facilitar la depuracin. Teniendo esto presente, podemos inicializar el semforo 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 mximo N-1 tems al buffer, inicializamos EntriesFree a N-1. Tambin debemos considerar el conteo mximo permitido en los semforos. El procedimiento que destruye el buffer siempre realiza una operacin de MARCA en ambos semforos. Entonces, como el buffer fue destruido, poda tener cualquier nmero de tems en l, incluyendo estados completamente llenos o completamente vacos. Establecemos el conteo mximo a N, permitiendo una operacin de marcado en el semforo dados todos los estados posibles del buffer.

Operacin: valores correctos de espera.


Us mutexes en lugar de secciones crticas en esta pieza de software porque stas le permiten al desarrollador un control fino sobre situaciones de error. Adems, soportan situaciones de time-out. Los tiempos antes de que se disparen los time-out en las operaciones de espera deberan ser realmente infinitos; es posible que el buffer permanezca lleno o vaco por un largo perodo de tiempo, y necesitamos que el hilo est bloqueado mientras el buffer est vaco. Los programadores paranoicos o inseguros preferirn 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 cdigo 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 seccin crtica son rpidas, hasta las N escrituras en memoria, y dado que N es bastante pequeo (es decir, menos de un milln), estas operaciones no deberan tomar ms de 5 segundos. Como beneficio adicional, parte del cdigo 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 desbloquearn y devolvern un error.

Destruccin: Liberando todo.

Hasta ahora, la mayora de los lectores han deducido que las operaciones de limpieza son habitualmente la parte ms difcil de la programacin multihilo. El buffer limitado no es la excepcin. El procedimiento ResetState realiza esta limpieza. La primera cosa que hace es verificar el valor de FBufInit. He asumido que no es necesario ningn acceso sincronizado, ya que el hilo que crea el buffer tambin debe destruirlo. Y ya que FBufInit slo es escrita por un solo hilo, y todas las operaciones de escritura ocurren en una seccin crtica (al menos despus de la creacin), 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 operacin 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 semforos. Las operaciones se realizan en este orden, porque cuando los semforos son liberados, el estado del buffer no es ms consistente: el conteo de los semforos 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 devolvern un error, en lugar de la operacin en los datos basura (por la inconsistencia del buffer). Luego, desbloqueamos los dos hilos al liberar ambos semforos, y entonces cerramos todos los manejadores de sincronizacin. Luego destruimos el mutex sin liberarlo. Esto est bien, porque como todas las operaciones de espera en el mutex devolvieron time-out, podemos estar seguros de que ambos hilos lector y escritor sern desbloqueados eventualmente. Adems, como slo hay un hilo lector y uno escritor, podemos garantizar que ningn otro hilo pudo haber intentado una espera en el semforo durante este proceso. Esto significa que una operacin de marca en ambos semforos ser suficiente para activar ambos hilos, y como destruimos los manejadores de los semforos mientras tuvimos propiedad del mutex, cualquier operacin futura de

lectura o escritura en el buffer fallar cuando intente esperar en alguno de los semforos.

Destruccin: Las sutilezas continan.


Se garantiza el funcionamiento de este cdigo solamente con un hilo lector, un hilo escritor y un hilo de control. Por qu? Si existiera ms de un hilo escritor o lector, ms de un hilo podra estar esperando en alguno de los semforos en algn momento. Si esto fuera as, podramos no llegar a activar todos los hilos lectores o escritores que estn esperando en algn semforo, cuando reiniciemos el estado del buffer. La primera reaccin de un programador a esto sera modificar la rutina de limpieza para continuar marcando uno u otro semforo hasta que todos los hilos hayan sido desbloqueados, haciendo algo comoesto. Por desgracia, esto es an insuficiente, porque uno de los bucles de repeticin en la limpieza podra terminar justo antes de que otro hilo entre en una operacin de lectura o escritura esperando en un semforo. Obviamente queremos alguna clase de atomicidad, pero no podemos colocar las operaciones en el semforo dentro de secciones crticas, porque los bloqueos de los hilos en los semforos se apoyarn en las secciones crticas, y todos los hilos caern en Deadlocks.

Los accesos a los manejadores de sincronizacin deben ser sincronizados!


La siguiente posibilidad podra ser hacer cero manejador del semforo 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 semforo! Si tambin tienen que sincronizar sus objetos de sincronizacin! Que podra 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 cul 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 rpidamente realice una operacin de espera en el mutex que pensamos que haba sido liberado recin! Es muy poco probable que estos sucesos se den as, pero de todos modos, es una solucin inaceptable.

Administracin de manejadores Win32.


Este problema es lo suficientemente espinoso que vale la pena ver qu pasa exactamente cuando hacemos una llamada a Win32 para cerrar un mutex o un semforo. En particular, es realmente til saber: Al cerrar un manejador, se desbloquean los hilos esperando en ese mutex o semforo 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 aplicacin de prueba de mutex y una aplicacin de prueba de semforos. A partir de estas aplicaciones se puede determinar que cuando se cierra el manejador de un objeto de sincronizacin, Win32 no desbloquea ningn 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 sincronizacin pueden conservar el conteo de referencia interna desde que llega a cero y al cerrar el manejador del objeto de sincronizacin de la aplicacin, todo lo que hacemos es perder todo lo parecido a tener un control sobre ese objeto de sincronizacin. En nuestra situacin, esto es un verdadero fastidio. Idealmente, cuando se liberan los recursos, uno esperara que un intento por esperar en un manejador cerrado desbloqueara los hilos que esperan en ese objeto de sincronizacin a travs de ese manejador en particular. Esto permitira al programador de la aplicacin entrar en la seccin crtica, liberar los datos en esa seccin crtica, y luego cerrar el manejador, provocando el desbloqueo de los hilos esperando en l, con un valor de error apropiado (quizs WAIT_ABANDONED?).

Una solucin.

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 semforos, solamente si sabemos cuntos hilos estn 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 arreglrselas con un nmero arbitrario de hilos. En l, las funciones de espera en los semforos han sido modificadas, y a las rutinas de limpieza se les ha hecho pequeos cambios. En vez de realizar una espera infinita en el mutex apropiado, el hilo lector y escritor llaman ahora a una funcin Controlled Wait (Espera controlada). En esta funcin, cada uno de los hilos espera en los semforos slo por una finita cantidad de tiempo. Esta espera por el semforo puede devolver tres valores posibles, como es documentado en el archivo de ayuda de Win32. WAIT_OBJECT_0 (xito) WAIT_ABANDONED WAIT_TIMEOUT Primero que nada, si el semforo es liberado, la funcin devuelve WAIT_OBJECT_0, y no se requiere ninguna otra accin. En segundo lugar, en el caso donde la funcin WaitFor de Win32 devuelva WAIT_ABANDONED, la funcin devuelve error; este valor de error en particular indica que un hilo ha salido sin liberar apropiadamente un objeto de sincronizacin. El caso en el que estamos ms interesados es donde la espera devuelve time-out. Esto puede ser por dos razones posibles:

El hilo podra estar bloqueado por un largo perodo de tiempo. El buffer interno fue destruido sin que se haya reactivado ese hilo en particular. Para verificar esto, intentamos entrar a la seccin crtica y verificar que la variable que indica si el buffer est inicializado contina siendo verdadera. Si alguna de estas operaciones falla, entonces sabremos que el buffer interno fue reiniciado y la funcin 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, slo fue una demora inesperada en un hilo). La rutina de limpieza tambin fue ligeramente modificada. Ahora marca los dos semforos y libera el mutex de la seccin crtica. Al hacer esto, se asegura de que el primer hilo lector y escritor sern desbloqueados inmediatamente mas all de que el estado del buffer sea reiniciado. Por supuesto, los hilos adicionales tendran 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 aplicacin usando dos hilos. Esta aplicacin busca nmeros primos palndromos (palndromos son los nmeros que se leen igual de derecha a izquierda y de izquierda a derecha). Un par de primos palndromos existir cuando dos nmeros, X e Y sean los dos primos, y adems Y sea el palndromo de X. Ni X ni Y necesitan ser nmeros palndromos en s mismos, sin embargo si uno de ellos lo es, entonces X = Y, lo que es un caso especial. Ejemplos de primos palndromos 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 cdigo) realizan tareas bastante diferentes. El primer hilo (el hilo adelantado) busca nmeros 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 dgitos, verifica si el nmero invertido es primo, y si es el caso, enva una cadena de texto con los dos nmeros al formulario principal (aqu el cdigo). Si bien hay bastante cdigo en este ejemplo, hay muy pocas cosas nuevas para discutir. Se le recomienda al lector echarle un vistazo a los mtodos execute de cada hilo, ya que estos proveen una visin 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 captulos anteriores. Y el ltimo punto para preocuparnos es lo has adivinado! Liberacin de recursos y limpieza.

Un par de puntos finales


Y pensabas que no se poda decir nada ms acerca de la destruccin? Hay un tema final para mencionar. El cdigo del buffer limitado asume que los hilos intentarn acceder a los campos del objeto despus 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 slo entonces liberar el buffer, liberando la memoria que ocupa el objeto. Fallar al hacer esto, pueden resultar en violaciones de accesos. La funcin StopThreads hace esto correctamente, asegurando una salida limpia. Tampoco hay algo trascendente en el hecho de que otra sincronizacin 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. Podra establecer el tamao del buffer cuando est en uso. Esto es generalmente una mala idea, ya que significa que, si ms de dos hilos estn usando el buffer, uno escritor y uno lector, podran detectar incorrectamente la destruccin del buffer. Si el buffer debe ser redimensionado, entonces todos los hilos que usan el buffer deberan 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 podran desear escribir una versin del buffer que maneje las operaciones de redimensionado en forma transparente.

Captulo 10. E/S y flujo de datos: del bloqueo a lo asincrnico, ida y vuelta.

En este captulo:

Diferencias en los hilos VCL y diseo de interfaces de E/S. Mapa de ruta. Implementando una conversin de bloqueo a asincrnico. Agregando operaciones de observacin en el buffer limitado. Creando un buffer limitado bi-direccional. El buffer de bloqueo a asincrnico en detalle. Construccin del BBA (Buffer de Bloqueo a Asincrnico) Destruccin 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. Miscelnea de limitaciones. La otra cara de la moneda: buffer de flujos de datos.

Diferencias en los hilos VCL y diseo 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 ms sencillo. Desde el punto de vista de un hilo usando un recurso de E/S a travs de llamadas con bloqueo, el xito o fracaso es inmediatamente evidente despus de hacer la llamada de E/S, y la lgica del programa nunca debe preocuparse acerca del perodo de tiempo entre las operaciones de E/S que estn siendo invocadas ni cuando sern completadas. Las operaciones que involucran al hilo de la VCL, no se les suele permitir bloqueos por largos perodos de tiempo: el hilo debe estar siempre disponible para procesar nuevos mensajes con una demora mnima. 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 asincrnicas, especialmente las operaciones que involucran comunicacin entre hilos, procesos o maquinas, ya que el tiempo de demora involucrado en

la operacin no puede ser conocido de antemano. El beneficio de las operaciones asincrnicas, 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 cdigo que se ejecuta en el hilo de la VCL tiene que desconocer el estado de evolucin 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 mquina 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 solucin bajo demanda ser suficiente. Cuando diseamos un grupo de funciones de transferencia de datos, esta diferencia debe ser tenida en cuenta. Tomando las comunicaciones como un ejemplo, el ms frecuente grupo de operaciones soportas en un canal de comunicacin son: Open,Close, Read y Write. Las interfaces de bloqueo de E/S ofrecen estas facilidades como funciones simples. Las interfaces asincrnicas ofrecen cuatro funciones bsicas, y adems, proveen hasta cuatro notificaciones, ya sea por call-back o porevento. Estas notificaciones indican que una operacin previa que estaba pendiente se ha completado, o que es posible repetir la operacin o un mezcla de ambas. Un ejemplo de interfaz podra ser:

Una funcin Open y su evento asociado OnOpen, que indica que la apertura se ha completado, y reporta el xito o fracaso de la operacin. Una funcin Read y su evento asociado CanRead (o OnRead). El evento tpicamente indica que una llamada a Read leer algunos datos nuevos, y/o que algn otro dato ha llegado desde la ltima lectura. Una funcin Write, y su evento asociado CanWrite (o OnWrite). El evento tpicamente 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 ms operaciones

Write. Dependiendo de la semntica, este evento puede o puede no ser disparado despus de una llamada a Open que haya tenido xito. Una funcin Close, y su evento asociado OnClose. El evento tpicamente indica que el canal de comunicacin fue cerrado finalmente, y ningn 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 comunicacin despus de haber llamado a Close, y suele funcionar bien para establecer y romper comunicaciones con mecanismos que usan autentificacin de tres vas (por ejemplo, TCP).

Mapa de ruta.
Antes de seguir adelante en este captulo, parece apropiado revisar los mecanismos existentes para transferencia de datos entre hilos y hacer un bosquejo de los mtodos por los que se extendern. Sin ms, se podra persuadir a algunos lectores a completar este captulo sin dejar de leer, mas all del hecho de que hay un montn de cdigo para estudiar. El punto ms importante en esta coyuntura es que muchos de los detalles de implementacin, al tiempo que son tiles para aquellos que quieran escribir programas funcionales que incluyan estas tcnicas, 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 captulo se mostrarn varias extensiones a este buffer. El primer puado de modificaciones ser bastante simple: colocar dos buffer vuelta y vuelta, y agregar una operacin sin bloqueo en ambos lados del buffer bi-direccional resultante.

Hasta ahora vamos bien. Esto no debera ser ninguna sorpresa para cualquier lector en este punto, y todos los que han seguido este tutorial hasta aqu no deberan tener problemas en implementar este tipo de construccin. La siguiente modificacin es un poco ms ambiciosa: en lugar de hacer todas las lecturas y escrituras en el buffer mediante bloqueos de buffer, haremos una serie de operaciones asincrnicas.

Especficamente, crearemos un componente que convierta operaciones de bloqueo en asincrnicas y viceversa. En su personificacin 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 semnticas de bloqueo y asincnicas.

La pregunta aqu es: Porqu? La respuesta debera ser obvia: Si podemos hacer un buffer que provea comunicacin bi-direccional entre dos hilos, donde un hilo usa operaciones de bloqueo, y el otro usa operaciones asincrnicas, entonces:

Podemos usarlo para comunicaciones entre el hilo de la VCL y el hilo de procesamiento en nuestra aplicacin sin bloquear el hilo de la VCL. Todas las complejidades quedarn ocultas dentro del cdigo del buffer: ningn nmero mgico, ni uso de sincronize, ni secciones crticas visibles pblicamente. Realizar control de flujo entre el hilo de la VCL y el hilo de procesamiento; una tarea que an no es posible hacer. Podra ser usado como una solucin 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 sincronizacin.

Implementando una conversin de bloqueo a asincrnico.


El Componente que crearemos asume que un solo hilo de la VCL est ejecutndose, y por consiguiente, una interfaz asincrnica ser provista para solamente un hilo. Las operaciones de bloqueo provistas por este buffer funcionarn con exactamente las mismas limitaciones que las presentadas para el ejemplo del buffer limitado en el captulo anterior, y por ende, cualquier nmero de hilos con bloqueo podrn 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), involucrndose solamente un elemento, el bloqueo a un buffer asincrnico (tambin llamado el BAB) tambin permitir simples operaciones que involucran un solo elemento. La semntica de la interfaz ser:

Creacin: En el momento de la creacin, el componente BAB crear las estructuras de datos 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 asincrnica). 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 operacin de lectura

asncrona se pueda ejecutar bien, vale decir, cuando los datos estn aguardando para que el hilo de la VCL los lea. Escritura: El BAB proveer dos funciones; BlockingWrite (escritura por bloqueo) y AsyncWrite (escritura asincrnica). 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 operacin de escritura, es decir, hay suficiente espacio libre en el buffer en el que podra ser escrito un item. Nuevamente, una relacin 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 notificacin. Operaciones de observacin: Cualquier hilo podr echar una mirada al buffer para saber cuantas entradas estn vacas o usadas en el buffer en una cierta direccin. Esta operacin podra ser muy til para el hilo de procesamiento para determinar si una operacin 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 observacin en el buffer limitado.


Aqu hay una mejora al buffer limitado para permitir operaciones de observacin. Ntese que si bien es posible leer la cuenta que lleva los semforos durante ciertas operaciones, prefer mantener estos conteos manualmente usando un par de variables extra FEntryCountFree y FEntryCountUsed. Un par de mtodos extra fueron provistos para leer estas variables. Muchos programadores Delphi pensarn inmediatamente en exponer estos atributos del buffer limitado como propiedades. Desgraciadamente, necesitamos tener en mente que las operaciones de sincronizacin necesarias para acceder esas variables podran fallar. En lugar de devolver conteos de -1 en una propiedad Integer, parece ms apropiado dejar las operaciones de observacin

como funciones, informando al programador que requiere algn trabajo acceder a los datos requeridos, y que la funcin podra fallar. Algunos podran argumentar que, siguiendo este razonamiento, tambin se debera programar la lectura del atributo Size (tamao) del buffer como una funcin explcita de lectura. Esto es ms que nada un tema de estilo, ya que el tamao del buffer puede ser ledo directamente sin que se necesite algn tipo de sincronizacin.

Creando un buffer limitado bi-direccional.


Esta operacin es casi trivial y no requiere explicaciones complejas. Lo he implementado como una simple encapsulacin de dos objetos buffer limitados. Todas las operaciones soportadas por el buffer limitado tambin son soportadas por el buffer limitado bi-direccional, con la pequea modificacin que el hilo usando este objeto debe especificar con que lado del buffer desea operar. Tpicamente, un hilo opera con el lado A, y el otro con el lado B. Aqu est el cdigo fuente. Esta clase implementa la funcionalidad descrita pictricamente en el diagrama de abajo representando el buffer limitado bi-direccional.

El buffer de bloqueo a asincrnico en detalle.


Habiendo hecho todo el trabajo previo de preparacin, ahora puede ser explicado el BAB con mayor detalle. El BAB posee un buffer bidireccional 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 ejecucin de todos estos hilos puede ser representada pictricamente, con slo un mnimo abuso de las convenciones existentes:

Este diagrama se ve un poco intimidatorio; quiz resulte ms fcil 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. 3. El hilo copia los datos ledos en un buffer intermedio y local para la clase hilo, y dispara un evento de flujo de datos, manejado por el BAB. 4. El cdigo de manejo del flujo de datos del BAB, ejecutndose en el contexto del hilo de lectura, enva un mensaje a su propio manejador

de ventanas indicando que los datos fueron ledos por el hilo de lectura. 5. El hilo de lectura espera entonces en un semforo que indicar que los datos fueron ledos por el hilo principal de la VCL. 6. En algn 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 notificacin 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 lgica del resto de la aplicacin (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 mtodo 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 semforo en el que est bloqueado el hilo de lectura, permitindole intentar y realizar otra operacin de lectura en el buffer bi-direccional. El BAB funciona exactamente de la misma manera cuando escribe. La escritura es realizada asincrnicamente 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 ms operaciones de escritura. En esencia, la interfaz entre operaciones de bloqueo y asincrnicas a travs del envo de mensajes es idntico 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 ms formal y de una manera mejor definida. Aqu est el cdigo 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 comn, lo que es implementado en la case base TBlockAsyncThread. Esta clase contiene:

El buffer intermedio, que guarda slo un nico puntero. Una seccin crtica para permitir un acceso atmico 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 semforo inactivo. Este semforo es usado para implementar las operaciones Wait for VCL write (espera para la escritura de la VCL) y Wait for VCL read (esperar a la lectura de la VCL) de una manera genrica. La case base del hilo tambin implementa las imprescindibles funcionalidades comunes: creacin del hilo, destruccin, y el disparador del evento OnDataFlow. La clase base tiene dos hijos: TBAWriterThread y TBAReaderThread. Estas implementan los mtodos actuales de ejecucin de los hilos y tambin proveen mtodos de lectura y escritura que sern 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. Adems, almacena el manejador de ventana FHWND, que es usado para el procesamiento especializado de mensajes.

Construccin del BAB


Echmosle ahora un vistazo a la implementacin. Desde la creacin, el componente BAB asigna un manejador de ventanas usando AllocateHWnd. Esta es una funcin 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 hacindolo 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 tambin con

una gran cantidad de cosas extra que no necesita. Tambin hay una pequea mejora de eficiencia, ya que el procedimiento de manejado de mensajes en el componente realiza una mnima cantidad de procesamiento requerido, lidiando con un mensaje en particular e ignorando el resto. Durante la creacin, el componente BAB tambin 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 notificacin publicando estas interfaces entre los hilos lectores y escritores y el hilo principal de la VCL. Como resultado de la creacin del componente, los hilos se inicializan. Todo el trabajo aqu es comn a ambos hilos lectores y escritores y, de igual modo, en el constructor del TBlockAsyncThread. Esto simplemente inicializa una seccin crtica necesaria para mantener un acceso atmico al buffer intermedio en cada hilo, y ste tambin crea el semforo inactivo para cada hilo, que asegura que el hilo de procesamiento esperar al hilo de la VCL antes de leer o escribir algn dato.

Destruccin del BAB.


La destruccin del componente es ligeramente ms complicada, pero usa principios que ya discutimos en captulos anteriores. El buffer bidireccional contenido en el BAB es similar al buffer limitado discutido en captulos anteriores, en el sentido de que la destruccin 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 lo menos estn en un estado en el que no puedan realizar ninguna otra operacin en el buffer. Una vez que esta condicin se haya alcanzado, la tercera etapa comienza, que es la destruccin de las estructuras fsicas de datos. El modo de destruccin del BAB en lneas 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 operacin en el buffer que est en progreso. El destructor para ambos hilos es llamado. Esto desbloquea en cada hilo sus semforos inactivos, y luego espera a que el hilo se complete antes de destruir la seccin crtica y el semforo 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 mltiples 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 sincronizacin internos del componente sin que el usuario del componente ni siquiera tenga que preocuparse por los problemas potenciales de ordenamiento inherentes a la operacin de limpieza. Una simple llama al Free del BAB ser suficiente. Esto es obviamente deseable.

Mas all de esto, el componente todava expone su mtodo ResetState. La razn 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 aplicacin principal debe terminar los hilos de procesamiento, reiniciar el estado del BAB y esperar a que el hilo de procesamiento termine antes de destruir fsicamente el BAB.

Un programa de ejemplo usando el BAB.


Aqu hay una nueva variante del tema de los nmeros primos. El formulario principal le pide al usuario dos nmeros el nmero de comienzo y fin de un rango. Estos nmeros son dados, colocados en

una estructura de pedido, y un puntero hacia esta estructura es escrito asincrnicamente en el BAB. En algn 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 cules nmeros 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 con el control de flujo del buffer. Una vez que un pedido es generado, el botn de pedido es deshabilitado. Solamente es rehabilitado cuando recibe un evento OnWrite del BAB indicando que se pueden escribir ms datos en forma segura. La implementacin actual establece el tamao del buffer bi-direccional a 4. Esto es suficientemente pequeo como para que el usuario pueda verificar que luego de enviar cuatro pedidos que tomen mucho tiempo en procesar, el botn 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 rpido 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 mtodo ResetState del BAB como fue descripto anteriormente para asegurarse que se limpia el hilo y la liberacin del buffer se produce de manera ordenada. Una falla en esto podra resultar en una violacin de acceso. El cdigo del hilo de procesamiento es bastante simple. No es muy interesante, ya que usa operaciones de lectura y escritura por bloqueo, slo usa la CPU cuando est procesando un pedido: si no puede recibir un pedido o enviar una respuesta, debido a una congestin en el buffer, entonces est bloqueado.

Hemos alcanzado nuestro objetivo!

Un pequeo resumen de las cosas que hemos conseguido con este componente:

Una transferencia de datos armoniosa entre el hilo de la VCL y los hilos de procesamiento. Todos los detalles de sincronizacin quedaron ocultos dentro del BAB (con la excepcin de los detalles de ResetState). Un completo control de flujo entre el hilo de la VCL y los hilos de procesamiento. Ningn ciclo ocupado o sometido: la CPU es usada con eficiencia. Ningn uso de synchronize. Los hilos no son bloqueados en forma innecesaria. El lector podra haber olvidado que estos problemas existan

Has notado el agujero en la memoria?


Durante los dos captulos anteriores y este captulo, un aspecto importante fue dejado de lado; los tems en los varios buffer que hemos diseado no se destruyen de forma apropiada cuando el buffer es destruido. Cuando diseamos inicialmente estas estructuras de buffer, adoptamos una implementacin similar a TList: la lista o buffer simplemente provee almacenamiento y sincronizacin. La correcta asignacin y liberacin del objeto es responsabilidad del hilo usando el buffer. Esta implementacin simplista tiene dificultades mayores. En el ms comn de los casos, es excesivamente difcil asegurarse que el buffer est vaco en ambas direcciones antes de que sea destruido. En el ejemplo de arriba, que es el uso ms simple posible del buffer, hay cuatro hilos, cuatro mutex o secciones crticas, y seis semforos en el sistema completo. Determinar el estado de todos los hilos y orquestar una salida con una limpieza perfecta, en estas situaciones es obviamente imposible. En el programa de ejemplo, esto fue resuelto conservando el conteo de cuntos pedidos hay sin responder en cualquier momento. Si hemos recibido tantas respuestas como pedidos hemos hecho, podemos estar seguros de que los varios buffer estn vacos.

Evitando agujeros en la memoria.


Un enfoque sera permitir que los buffer implementen call-backs que destruyan los objetos que contienen al momento de la limpieza. Esto funcionara en el caso general, pero abre una puerta al abuso, y el uso de implementaciones de este tipo suelen terminar siendo difcil en la prctica. Otra posibilidad es tener un esquema de administracin general del buffer que lleva cuenta de los tipos especficos de objetos, tomando cuenta cuando entran y salen de los varios buffer en la aplicacin. Una vez ms, una implementacin de este tipo se volvera difcil y requerira un mecanismo de seguimiento potencialmente complicado para hacer un trabajo que realmente debera ser simple. La mejor solucin es hacer la estructura de los buffers anlogas a TObjectList; es decir, todos los itemas colocados en el buffer son clases. Esto permitir a los hilos realizar la operacin de limpieza llamando al destructor apropiado en todos los tems del buffer. An mejor, usando tipos de referencia de clases, podemos realizar verificaciones automticas en tiempo de ejecucin en los objetos pasados a travs del buffer, y producir un paquete de buffers de tipos seguros. La implementacin de un esquema semejante se deja como ejercicio para el lector. Ningn cambio es necesario para el mecanismo bsico de sincronizacin, pero la armona en los procedimientos de lectura y escritura necesitarn modificaciones, como tambin lo necesitarn 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, todava era posible proveer un mecanismo razonablemente constante para echar un vistazo a los buffer y ver cuntos tems hay en ellos. Es posible que cuando le echemos un vistazo al buffer bi-direccional, la liberacin y el conteo de uso no se agregue a la misma figura, ya que ambas operaciones no pueden realizarse atmicamente. Sin embargo, se asegura que con slo un hilo lector y uno escritor en cada direccin, las

ojeadas pueden ser utilizadas como indicacin razonable de que una operacin tendra xito sin el bloqueo. Con el buffer asincrnico, el problema es peor en el sentido de que no es posible asegurarse una buena mirada en el estado del buffer con la implementacin actual. Esto es as porque hay esencialmente dos buffer en cada direccin, el buffer limitado y el interno, que almacena un solo tem. Ningn mecanismo es provisto para bloquear globalmente ambos buffer y, en una operacin atmica, determinar el estado de los dos. 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 engaar al programador hacindolo pensar que los resultados podran ser exactos! Es posible hacerlo de otra manera?

Haciendo a un lado el buffer intermedio.


La mejor manera de superar la situacin es quitando completamente el buffer intermedio. Si nos detenemos a pensar un momento, esto es posible, pero requiere la reescritura de todo el cdigo para el buffer. Necesitaremos implementar un nuevo buffer limitado con una semntica 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 asncrona (un simple xito/fracaso sin bloqueo), y adems implementar un par de mtodos Block Until (bloquear hasta que se d una condicin). Estos mtodos bloquearn un hilo hasta que a una operacin de lectura o escritura pueda asegurrsele el xito. De esta manera, los hilos lector y escritor pueden ser usados para enviar notificaciones por bloqueo hasta que una operacin sea posible, y el hilo de la VCL pueda realizar la operacin de lectura y escritura sobre el buffer limitado, sin bloqueo.

Con esta semntica, slo tenemos un grupo de buffer que deben ser administrados, y es comparativamente ms fcil proveer una operacin

para ver el estado del buffer que provea resultados exactos. Un vez ms, esto se deja como ejercicio para el lector

Miscelnea de limitaciones.
Todas las estructuras del buffer introducidas en los ltimos captulos han asumo que el programador enva punteros a direcciones de memoria vlidas, y no NIL. Algunos lectores puede que hayan notado que parte del cdigo en los hilos lector y escritor asumen implcitamente que NIL es un valor null vlido que no ser enviado a travs del buffer. Esto podra naturalmente ser solucionado con algunas marcas de validacin en el buffer, pero a costa de que el cdigo quede un poco desprolijo. Una limitacin ms terica es que el usuario final de este componente podra crear una gran cantidad de buffer. La gua de programacin de Win32 para la programacin con hilos establece que generalmente es una buena idea limitar el nmero de hilos de procesamiento a alrededor de diecisis por aplicacin, lo que podra permitir ocho componentes BAB. Ya que no hay limitacin en el nmero de hilos de procesamiento que pueden realizar operaciones de bloqueo en el BAB, parece apropiado tener slo un BAB por aplicacin y usarlo para comunicarse entre un hilo VCL y todos los hilos de procesamiento. Esto, por supuesto, asume que todos los hilos de procesamiento estn realizando el mismo trabajo. En suma, esto debe ser aceptable, porque la mayora de las aplicaciones Delphi deberan compartir su tiempo de ejecucin con un puado de hilos para consumir el tiempo de las operaciones en segundo plano.

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


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 mayora 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 caractersticas, 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 semforos para llevar cuenta de un nmero concreto de tems en el buffer. En lugar de eso, los semforos se usan en un estilo binario, esto es, con conteos de slo 1 o 0. Cuando leemos o escribimos con buffer de flujo, un clculo debe hacerse para saber si el buffer ser llenado o vaciado por la operacin. 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 ejecucin, el estado no se mantiene en ningn lado, grabando el estado de bloqueo o ejecucin 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 operacin de lectura o escritura debe ser desbloqueado. Esto complica un poco la deteccin de bloqueo y desbloqueo, pero el principio general es el mismo. Esquemas de notificacin para buffer de flujo son modificados de manera similar. El actual esquema de notificacin enva una notificacin para todas las lecturas o escrituras. Los componentes BAB que operan con flujos envan notificaciones basndose en si el buffer intermedio (o su equivalente) sigue estando lleno o no. Como las notificaciones pueden ser consideradas como el equivalente asincrnico a las operaciones Signal o ReleaseSemaphore, esta modificacin es anloga a los puntos de arriba. Hay mucho ms que debera ser mencionado al respecto. Si el lector quiere ver un ejemplo funcional de buffer de flujo, puede consultar el cdigo en el captulo final.

Captulo 11. Sicronizadores y Eventos.


En este captulo.

Mas mecanismos de sicronizacin. Cuando la eficiencia ptima es imprescindible. Un MREWS Simple. Puntos sobre la implementacin a resaltar. Un uso de ejemplo de MREWS simple. Una introduccin a los Eventos. Simulacin de eventos usando semforos. El MREWS simple usando eventos. El MREWS de Delphi.

Mas mecanismos de sicronizacin.


El material introducido en captulos anteriores ha cubierto todos los mecanismos bsicos de sincronizacin. En el conjunto, los semforos y los mutexes permiten que el programador cree el resto de los mecanismos de sincronizacin, no obstante con un cierto esfuerzo. A pesar de esto, hay algunas situaciones que son muy comunes adentro la programacin multihilo, pero no fciles de ocuparse de usar los mecanismos demostrados hasta ahora. Dos nuevas primitivas sern 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 crtico se debe acceder con frecuencia por una gran cantidad de hilos, esto puede resultar ser ineficiente. El bloqueo exclusivo es, de hecho, ms cuidadoso que absolutamente necesario. Recordar el captulo 6, observa que la sincronizacin mnima 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 mnimo 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 de un nmero relativamente grande de hilos, operaciones de escritura son relativamente infrecuentes, y solamente un nmero pequeo de hilos las realizan.

Estas condiciones permanecen en numerosas situaciones del mundo real. Por ejemplo, la base de datos de stock para una compaa puede contener una gran cantidad de artculos, y numerosas lecturas pueden ocurrir para calcular la disponibilidad de ciertas mercancas. Sin embargo, la base de datos es solamente actualizada cuando los artculos se piden o se envan 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 computacin: 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 mayora 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 ms operaciones de lectura, que se garantizan sern atmicas 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 EndRead, los datos obtenidos en esos pares son siempre consistentes: ninguna

operacin 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 ms operaciones de escritura, y puede estar seguro que todas las operaciones de escritura son atomicas. Despus de las operaciones de escritura, el hilo llama a EndWrite. Las operaciones de escritura no sern sobreescritas por otras operaciones, y ninguna lectura obtendr resultados inconsistentes debido a estas operaciones cuando estn en progreso.

Un MREWS Simple.
Hay varias maneras de implementar un MREWS. La VCL contiene una implementacin bastante sofisticada. Para familiarizar al usuario con los principios basicos, aqu hay una implementacin ms simple pero levemente menos funcional usando los semforos. El MREWS simple contiene los puntos siguientes:

Una seccin crtica para asegurar el acceso a datos compartidos (DataLock). Un contador del nmero de los lectores activos (ActRead). Un contador del nmero de los lectores que estn leyendo (ReadRead). Un contador del nmero de los escritores activos (ActWrite). Un contador del nmero de los escritores que estn escribiendo (WriteWrite). Un par de semforos, conocido como los semforos del lector y del escritor (ReaderSem y WriterSem). Una seccin crtica para forzar la exclusin 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 intencin de leer o de escribir. Una vez que haya ocurrido esto, el hilo se puede bloquear, dependiendo de si hay otra operacin de lectura o escritura en progreso. Cuando se desbloquea, ingresa a la segunda etapa, realiza las operaciones de lectura o escritura, y despus 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 operacin que el hilo realizaba (ledo o escriba). El diagrama siguiente ilustra esto ms detalladamente.

En este punto, una implementacin de esta clase particular de sincronizacin debe ser obvia. Aqu est. Si en este punto el lector todava est confundido, entonces no se asuste! Este objeto de sincronizacin no se entiende fcilmente 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 implementacin a resaltar.


Hay una asimetra en el esquema de la sincronizacin: los hilos que potencialmente quieren leer se bloquearn 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 clculos, si un hilo debe ser bloqueado o no ocurre en la seccin crtica, es perfectamente permisible hacer el sincronizador simtrico. Lo malo de esto es que, si ocurren muchas operaciones de lectura concurrentes, pueden impedir que todas las escrituras ocurran. Por supuesto, la situacin opuesta, con muchas escrituras deteniendo operaciones de lecturas tambin se puede dar. Tambin es digno de observar el uso de semforos cuando se adquieren recursos de lectura o escritura: Operaciones de espera en semforos se deben realizar siempre fuera de la seccin crtica que guarda los datos compartidos. As la sealizacin condicional de un semforo dentro de la seccin crtica est puramente para asegurarse de que la operacin 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. Imagnese que es necesario que una gran cantidad de hilos no pierdan de vista el estado de un nmero 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. Desafortunadamente, los archivos pueden ser modificados por un nmero 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 ejecucin que itera a travs de todos los archivos en un directorio, calculando una suma de comprobacin (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 comprobacin en unos o ms archivos. Primero, vamos mirar la fuente para la lista de la suma de comprobacin. Aqu est. Las operaciones bsicas son:

Establecer la suma de comprobacin para un archivo particular. Esto agrega una entrada para el archivo en la lista si no existe. Obtener la suma de comprobacin 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 comprobacin. Todas estas operaciones publicamente accesibles tienen llamadas de sincronizacin apropiadas al comienzo y al final de la operacin.

Observe que hay un par de mtodos los cuales comienzan con el nombre "NoLock". Estos mtodos son los mtodos que necesitan ser invocados desde ms de un mtodo visible publicamente. La clase se ha escrito de esta manera debido a una limitacin 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 ms detalladamente ms adelante. Aparte de esto, la mayora del cdigo para la lista de la suma de comprobacin es bastante mundano, consistiendo sobre todo en el manejo de la lista, y no debe presentar ninguna sorpresa para la mayora de los programadores de Delphi. Ahora demos una mirada al cdigo del hilo en ejecusin. Este hilo parece levemente diferente de la mayora de los hilos de ejemplo que he presentado hasta ahora porque se pone en ejecucin como una mquina de estado. El mtodo Execute simplemente ejecuta una funcin para cada estado, y dependiendo del valor de retorno de la funcin, busca el siguiente estado requerido en una tabla de transicin. Una funcin lee la lista de archivos desde el objeto lista de sumas de comprobacin, el segundo quita sumas de comprobacin innecesarias de la lista, y el tercero calcula la suma de comprobacin para un archivo particular, y la actualiza en caso de ser necesario. La belleza de usar una mquina de estado es que hace mucho ms limpia la terminacin del hilo. El mtodo Execute llama a las funciones, busca el

siguiente estado y comprueba en un ciclo while si el hilo debe terminar. Puesto que a cada funcin le toma normalmente un par de segundos terminar, la terminacin del hilo es normalmente bastante rpida. Adems, una sola verificacin de terminacin del hilo es necesaria, haciendo al cdigo ms limpio. Tambin me gusta el hecho de que la lgica entera de la mquina de estado est implementada en una lnea de cdigo. Hay cierta pulcritud en esto. Finalmente, hecharemos una ojeada el cdigo del form principal. Esto es relativamente simple: el hilo y la lista de sumas de comprobacin se crean al iniciar, y se destruyen cuando el programa se cierra. La lista archivos y sus sumas de comprobacin se muestra regularmente como resultado un contador de tiempo (timer). El directorio que est siendo observado es fijo en el cdigo; 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 atmica. Hay varios lugares en el hilo de la actualizacin 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 funcin "check file" del hilo. Una vez que se haya calculado la suma de comprobacin del archivo, el hilo lee la suma de comprobacin almacenada para ese archivo, y lo actualiza si no coincide con la actual suma de comprobacin calculada. Estas dos operaciones no son atmicas, puesto que las llamadas mltiples al objeto lista de sumas de comprobacin no son atmicas. Esto proviene principalmente del hecho que llamadas anidadas al sincrinizador no trabaja con nuestro sincronizador simple. Una solucin posible es dar al objeto lista de sumas de comprobacin, dos nuevos mtodos: "bloquearse para la lectura" y "bloquearse para la escritura". Un bloqueo se podra adquirir en los datos compartidos, para la lectura o la escritura, y operaciones de lecturas y escrituras realizadas. Sin embargo, esto todava no soluciona todos los posibles problemas de sincronizacin. Soluciones ms avanzadas sern discutidas ms adelante en este captulo.

Puesto que el funcionamiento internos del sincronizador ocurre a nivel de Delphi, es posible obtener una estimacin de cmo ocurren 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 nmero de archivos y de sumas de comprobacin llega a ser grande, los conflictos son cada vez ms comunes, puesto que mas tiempo se pierde accediendo y copiando datos compartidos.

Una introduccin a los Eventos.


Los eventos son quizs una de las primitivas de sincronizacin ms simples de entender, pero una explicacin de ellos se ha dejado para este punto, simplemente porque se utilizan mejor conjuntamente con otras primitivas de sincronizacin. Hay dos tipos de eventos: eventos manuales y eventos automticos. Por el momento, consideraremos eventos manuales. Un evento trabaja exactamente como un semforo (o luz de parada para los lectores de ESTADOS UNIDOS)[1]. Tiene dos estados posibles: sealado (anlogo a un semforo en verde) o no-sealado (anlogo a un semforo en rojo). Cuando el evento est sealado, los hilos que estn en espera en el evento no se bloquean y continan en ejecucin. Cuando el evento no est sealado, los hilos que estan en espera en el evento se bloquean hasta que se seala 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 sincronizacin. As como permitir que el evento sea creado en un estado sealado o nosealado, una bandera boleana indica si el evento es un evento manual o automtico.

SetEvent: Esto fija el estado del evento a sealado, as reanudando todos los hilos que estn esperando en el evento, y permitiendo que ltimos hilos pasen sin bloquearse. ResetEvent: Esto fija el estado del evento a no-sealado, 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 todava quedan bloqueados. Los eventos automticos son un caso especial de los eventos manuales. En un evento automtico, el estado de un evento sealado se fija de nuevo a no-sealado 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 idntica a los semforos, y si un programador est utilizando eventos automticos, deben considerar usar semforos en su lugar, para hacer el comportamiento del mecanismo de sincronizacin ms obvio.

Simulacin de eventos usando semforos.


Una primitiva de evento de hecho puede ser creada usando semaforos: Es posible utilizar un semforo para bloquear condicionalmente todos los hilos que esperan en la primitiva de evento y para desbloquear hilos cuando se seala la primitiva. Para hacer esto, se utiliza un acercamiento muy similar al algoritmo de sincronizacin. El evento guarda dos piezas de estado: un boleano indicando si el evento est sealado o no, y una cuenta del nmero de hilos bloqueados actualmente en el semforo en el evento. Aqu est cmo se ponen en ejecucin las operaciones:

CreateEvent: Se crea el objeto del evento, la cuenta de hilos bloqueados se fija a cero, y el estado de la seal se fija segn lo especificado en el constructor. SetEvent: El estado de la seal se fija para no bloquear los hilos entrantes. Adems, la cuenta de hilos bloqueados en el semforo se

examina, y si esta arriba de cero, entonces el semforo se seala repetidamente hasta que se desbloquean todos los hilos bloqueados. ResetEvent: El estado de la seal se fija para bloquear los hilos entrantes. PulseEvent: Todos los hilos bloqueados actualmente en el semforo se desbloquean, pero no se realiza ningn cambio al estado de la seal. WaitForEvent: El estado de la seal del evento se examina. Si indica que el evento est sealado, entonces se seala el semforo interno, y la cuenta de hilos bloqueados en el semforo se decrementa. La cuenta de hilos bloqueados se incrementa, y una espera se realiza en el semforo interno. Aqu est el cdigo para un evento simulado usando semforos. Si el lector ha entendido el sincronizador simple, entonces este cdigo debe ser bastante auto explicativo. La implementacin podra ser simplificada levemente substituyendo los ciclos while que desbloquean los hilos con una sola sentencia que incremente la cuenta en el semforo por la cantidad requerida, no obstante el acercamiento implementado aqu es ms consistente con la implementacin del sincronizador presentado anteriormente.

El MREWS simple usando eventos.


Las estructuras del control requeridas para simular un evento que usa semforos 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 semforos. Esto no es particularmente difcil: aqu est. Como es normal, la conversin atrae la atencin sobre algunos puntos de la implementacin dignos de mirar. Primero que nada, el sincronizador simple calculaba si los hilos se deben bloquear en la seccin crtica de los procedimientos StartRead y StartWrite, y despus realizar las acciones de bloqueo requeridas fuera de la seccin crtica. 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 seccin crtica de DataLock, para garantizar resultados consistentes, y las acciones de bloqueo se realizan fuera de la seccin crtica para evitar Deadlocks. En segundo lugar, este sincronizador particular es simtrico, y permite operaciones de escritura o lectura con igual prioridad. Desafortunadamente, puesto que hay solamente un sistema de contadores en este sincronizador, es algo ms difcil hacerlo asimtrico.

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 condicin de que ningn hilo llame a StartWrite en el medio de una secuencia de llamadas anidadas a StartRead. Una vez ms, si esto ocurre, un Deadlock ser una consecuencia inevitable. Lo ideal sera que pudieramos anidar operaciones de lectura y de escritura. Si un hilo es un lector activo, entonces las llamadas repetidas a StartRead no deberan tener ningn efecto, con tal que sean emparejadas por un nmero igual de llamadas a EndRead. Semejantemente, llamadas anidadas a StartWrite deben ser posibles tambin, y todas pero el par externo de las llamadas a StartWrite y EndWrite no deberan tener ningn efecto. El segundo problema es que los sincronizadores ilustrados hasta ahora no permiten operaciones atmicas de leer-modificar-escribir. Lo ideal sera que un simple hilo pudiese llamar a StartRead, StartWrite, EndWrite, EndRead; as permitiendo que un valor sea ledo, 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 operacin 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 sincronizacin se encajan en un objeto compartido de los datos (como en el ejemplo) puede ser muy difcil proporcionar un interfaz

conveniente a ese objeto que permita operaciones de lecturamodificacin-y-escritura sin tambin proveer llamadas separadas de sincronizacin para bloquear el objeto en la lectura o escritura. Para hacer esto, se requiere una implementacion en conjunto ms sofisticada, por el que cada operacin de comienzo y fin se fije en cul hilo esta realizando la operacin 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 cdigo de fuente de VCL aqu y discutir exactamente que lo hace. Sin embargo, sea suficiente decir que el Delphi MREWS:

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.

Permite operaciones de lectura anidadas. No permite operaciones de escritura anidadas. Permite que las operaciones de lectura sean promovidas a operaciones de escritura, permitiendo operaciones de leermodificar-y-escribir se hagan con bloqueos mnimos en cada etapa de los procedimientos. Est escrito tendiendo mucho a la eficiencia: Se utilizan las secciones crticas solamente donde son absolutamente necesarias, y se prefieren operaciones de interbloqueo. Esto obscurece el cdigo un poco, pero el aumento en eficiencia es ms que valioso. Puede ser intercambiado con las clases de sincronizador presentadas arriba sin cambio en la semntica.

Captulo 12. Ms dispositivos Win32 para la sincronizacin.


En este captulo:

Mayor eficiencia va operaciones de interbloqueo. Atomicidad desde la nada. Eventcounts y secuenciadores. Otros dispositivos Win32 para la sincronizacin.

Mayor eficacia va operaciones de interbloqueo.


Las primitivas convencionales para la sincronizacin 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. Las operaciones de interbloqueo fueron concebidas originalmente como mecanismo para la sincronizacin de bajo nivel en los sistemas con multiples procesadores simtricos 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 ms 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 despus escribirla atmicamente, mientras que se asegura de que ningn otro procesador tenga acceso a la misma memoria, y el procesador que realiza la operacin 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 debera usar operaciones de interbloqueo despus de todo? Un buen ejemplo es el de una cerradura de vueltas. De vez en cuando uno desea crear algo similar a una seccin crtica. Sin embargo, puede haber cdigo muy pequeo en la seccin crtica, y el cdigo en la seccin crtica puede ser accedido muy a menudo. En casos tales como este, un objeto totalmente basado en la sincronizacin 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, despus 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 perodos largos en la cerradura mientras que un hilo de ms baja prioridad tiene la cerradura. En planificadores simples, si las prioridades del hilo son iguales, despus la llamada a dormir no ser necesaria. La operacin de interbloqueo es necesaria, porque si un hilo realiz una lectura de memoria, incremento, comparacin y posterior escritura, entonces dos hilos podran adquirir la cerradura simultneamente. 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 algn tiempo apreciable, entonces la CPU de desperdicia, as que son solamente tiles para poner secciones crticas pequeas. Las cerraduras de vuelta son tiles al hacer cumplir las secciones crticas que son ellos mismos parte de las estructuras de la sincronizacin. Los datos compartidos dentro de primitivas o de planificadores de sincronizacin son protegidos a menudo por las cerraduras de esta clase: las cerraduras son a veces necesarias porque las primitivas de sincronizacin a nivel del OS no pueden ser usadasr para implementar primitivas de sincronizacin a nivel del OS. Las cerraduras de vuelta tienen todos los mismos problemas de concurrencia que los mutexes, con salvedad de que la adquisicin cclica da lugar ya no a deadlocks, si no a livelocks. Esta es una situacin levemente peor que un deadlock porque aunque hilos "bloqueados" no estn ejecutando ningn cdigo til, estn funcionando como un bucle infinito, estn utilizando la CPU y estn degradando el funcionamiento del sistema entero. Las cerraduras de vuelta no deben ser utilizadas como semforos para "suspender" un hilo.

Atomicidad desde la nada.


Con cuidado, Es de hecho posible crear una cerradura de vueltas que sea atmica sin asumir ningn interbloqueo en absoluto, a condicin 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 algn otro tiene la cerradura, y vamos otra vez, si no, tenemos la cerradura. Lo importante sobre este sistema de operaciones es que, dado ciertas clasulas, un cambio de hilo puede ocurrir en cualquier momento, sto todava 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 atmico. Entonces leemos el valor de la cerradura en un vairable local. Esto no es atmico. El valor ledo dentro de la variable local puede ser diferente del resultado del incremento. Sin embargo, la cosa realmente astuta sobre esto es que porque el incremento se realiza antes de la operacin de lectura, los conflictos de hilo que ocurren significarn siempre que el valor ledo es demasiado alto en vez de demasiado bajo: los conflictos de hilo resultan en una estimacin 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 estn dejando en memoria, y no se estn depositando en registros. Mientras que resulta, en Delphi 4 al lo menos, pasando la cerradura como parmetro var, e incluyendo la variable local, el compilador Delphi genera el cdigo correcto que trabajar en mquinas de processor nico. En las mquinas con multiples procesadores, los incrementos y los decrementos indirectos no son atmicos. Esto ha sido solucionada en la versin 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 bs de memoria exclusivamente mientras dura la instruccin, haciendo atmicas estas operaciones as. Las malas noticias son que aunque en teora sto es correcto, la mquina 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

cdigo con privilegios de Ring 0. Otro problema es que desde esta versin de la cerradura de vueltas no llama a dormir, es posible que los hilos monopolicen el procesador mientras esperan la cerradura, algo que est garantizado para traer la mquina a un cuelgue total.

Eventcounts y secuenciadores.
Una propuesta alternativa a los semforos es usar dos nuevos tipos de primitivas: eventcounts y secuenciadores. Ambas contienen contadores, pero a diferencia de los semforos, los contadores aumentan indefinidamente a partir del tiempo de su creacin. Alguna gente es ms 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 disposicin 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 despus 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 operacin:

Sequencer.Ticket(): Vuelve el contador interno actual en el secuenciador, y lo incrementa. Una definicin de las clases implicadas se debera ver a algo como esto. Es entonces relativamente fcil utilizar eventcounts y secuenciadores para realizar todas las operaciones que se pueden realizar usando semforos:

Hacer cumplir una exclusin mutua. Buffer limitado con un productor y un consumidor. Buffer limitado con un nmero arbitrario de productores y de consumidores. Una ventaja particular de este tipo de primitiva de sincronizacin es que las operaciones de avanzar y pedir turnos se pueden implementar de forma muy sensilla, usando la instruccin de comparacin de

bloqueos mutuos. Esto se deja como ejercicio levemente ms difcil para el lector.

Otros dispositivos Win32 para la sincronizacin.


Waitable Timers (Temporizadores con Tiempo de espera). Windows NT y Win2K proporcionan objetos Waitable Timers. stos permiten a un hilo o a un nmero 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 nmero de hilos sobre una base sincronizada; una manera de controlar el flujo de los hilos. Adems, el retardo que los temporizadores con tiempo de espera (Waittables Timers) proporcionan se pueden fijar a valores muy exactos: el valor ms pequeo disponible es alrededor 100 nanosegundos, haciendo a los temporizadores ms deseables que usar Sleep() si un hilo tiene que ser suspendido por cierta cantidad de tiempo. MessageWaits (Espera de mensajes). Cuando las aplicaciones Delphi estn esperando que los hilos terminen, el hilo principal de VCL se bloquea permanentemente. Esto es una situacin potencialmente problemtica, porque el hilo de la VCL no puede procesar mensajes. Win32 proporciona la funcin MsgWaitForMultipleObjects para solucionar esto. Un hilo espera un mensaje se bloquea tambin hasta que los objetos de sincronizacin se sealan, 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 ejecusin mientras que tambin permite que responda a los mensajes de las ventanas. Un buen artculo sobre el tema se puede encontrar en: http://www.midnightbeach.com/jon/pubs/MsgWaits/MsgWaits.h tml (en ingls).

Captulo 13. Usar hilos conjuntamente con el BDE, las excepciones y las DLLs.

En este captulo:

Programacin 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 implementacin de la DLL. Trampa 1: La encapsulacin de Delphi de la funcin de punto de entrada. Escribir un DLL con multiproceso. Objetos globales con nombre. La DLL en detalle. Inicializacin de la DLL. Una aplicacin usando la DLL. Trampa 2: Contexto del hilo en las funciones de punto de entrada. Control de Excepciones. El BDE.

Programacin de DLL y Multiprocesos.


Las bibliotecas de enlace dinmicas, o los DLL permiten que un programador comparta cdigo ejecutable entre varios procesos. Se utilizan comnmente para proporcionar el cdigo de la librera compartida para varios programas. El cdigo de la escritura para los DLL est en la mayora es similar al cdigo de escritura para los executables. A pesar de esto, la naturaleza compartida de las DLL significa que los programadores familiarizados con programacin multihilos los utilizan a menudo para proporcionar servicios a nivel del sistema: se es el cdigo que afecta varios procesos que tengan el DLL cargado. En este captulo, miraremos cmo escribir cdigo para una DLL que funciona a travs de ms 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 cdigo de los usuarios: tambin incluye

todas las variables globales en las bibliotecas runtime de Borland, y cualquier unidad usada por cdigo en la DLL. Esto tiene la ventaja que los programadores principiantes de DLLs pueden tratar la programacin de DLLs de la misma manera que la programacin de ejecutables: si una DLL contiene una variable global, entonces cada proceso tiene su propia copia. Adems, esto tambin significa que si una DLL es invocada por los procesos que contienen solamente un hilo, entonces no se requieren ninguna tcnica en especial: la DLL no necesita ser segura frente a hilos, puesto que todos los procesos tienen instancias totalmente aisladas de la DLL. Podemos demostrar esto con una DLL simple que no haga nada mas que almacenar un nmero entero. Exporta un par de funciones que permiten a una aplicacion leer y escribir el valor de ese nmero entero. Podemos entonces escribir; una simple aplicacin de prueba que utilice esta DLL. Si varias copias de la aplicacin se ejecutan, uno observa que cada aplicacin utiliza su propio nmero entero, y ninguna interferencia existe entre ellas.

Escribir una DLL multihilo.


Escribir una DLL multihilo es sobre todo igual que la escritura cdigo multihilo en una aplicacin. El comportamiento de hilos mltiples dentro de la DLL es igual que el comportamiento de hilos mltiples en una aplicacin 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 omisin, 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 sincronizacin 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 automticamente si se crea una clase TThread para un mdulo dado.

El problema es que un ejecutable y la DLL consisten de dos mdulos separados, cada uno con su propia copia del administrador de memoria de Delphi. As, si 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 mltiples. Esto puede ser solucionado estableciendo la variable IsMultiThread a true. Es mejor establecer esto usando la funcin Entry Point de la DLL, discutido ms 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 ms a menudo con los strings largos, pero puede ocurrir al usar asignacin de memoria con New o GetMem, y liberarla usando Dispose o FreeMem. La solucin en este caso es incluir ShareMem, una unidad que mantiene dos administradores de memoria en conjunto usando las tcnicas discutidas ms adelante.

Puesta a punto e implementacin de la DLL.


Atento al hecho de que los programadores de DLL necesitan a menudo estar enterados de cuntos hilos y procesos estn activos en una DLL en cualquier momento, los arquitectos del sistema Win32 proporcionan un mtodo para los programadores de DLL para no perder de la cuenta de los hilos y procesos en una DLL. Este mtodo se conoce como la funcin de punto de entrada (Entry Point) de la DLL. En un ejecutable, el punto de entrada (segn lo especificado en el encabezado del mdulo) indica donde la ejecucin del programa debe comenzar. En una DLL, seala a una funcin 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 funcin toma un solo parmmetro 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 caractersticas que pueden conducir a malentendidos y problemas al escribir cdigos de punto de entrada. La primera caracterstica ocurre como resultado de la encapsulacin de Delphi de la funcin Entry Point, y es relativamente simple de solucionar. La segunda ocurre como resultado de contexto del hilo, y ser discutido ms adelante.

Trampa 1: La encapsulacin de Delphi de la funcin de punto de entrada.


Delphi utiliza la funcin del punto de entrada de la DLL para manejar inicializacin y finalizacin de unidades dentro de una DLL as como la ejecucin del cuerpo principal del cdigo de la DLL. El escritor de la DLL puede poner un gancho en el manegador de Delphi asignando una funcin apropiada a la variable DLLProc. El manejador por omisin de Delphi funciona as: Se carga la DLL, la funcin del punto de entrada se llama con DLL_PROCESS_ATTACH. Delphi utiliza esto para llamar la inicializacin de todas las unidades en la DLL, seguido por el cuerpo principal del cdigo de la DLL. La DLL se descarga, dando por resultado dos llamadas a la funcin del punto de entrada, con los argumentos DLL_PROCESS_DETACH. Ahora, el escritor de la aplicacin solamente consigue cdigo para ejecutarse en respuesta a la funcin del punto de entrada cuando la

variable DLLProc apunta a una funcin. 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 funcin del punto de entrada. Resumiendo, lo que esto significa es que al usar la funcin del punto de entrada en la DLL, el programador de Delphi nunca ver la primera unin 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 ms procesos se unen ms adelante, el mismo argumento se aplica, puesto que cada instancia de la DLL tiene variables globales separadas. En caso de que todava confundan al lector, presentar un ejemplo. Aqu est una DLL modificada que contiene una unidadcon una funcin que muestra un mensaje. Como usted puede ver, el cuerpo principal, la inicializacin de la unidad y la funcin 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 aplicacin de prueba. Consiste de una ventana con un botn encendido. Cuando se hace click en el botn, se crea un hilo, el cual llama al procedimiento en la DLL, y despus se destruye. As pues, qu sucede cuando ejecutamos el programa?

La DLL avisa de la inicializacin de las unidades. La DLL avisa de la ejecucin del cuerpo principal de la DLL. Cada vez que se hace click en el botn la DLL informa: o Punto de entrada: unin de un hilo. o Procedimiento de la Unidad. o Punto de entrada: separacin del Hilo Note que si disparamos ms de un hilo desde la aplicacin, 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 ms all de una. Cuando el programa se cerra, la DLL informa el punto de entrada: separacin del proceso, seguido por la finalizacin de la unidad.

Escribiendo una DLL multiproceseso.


Armado con el conocimiento de cmo utilizar la funcin de punto de entrada, ahora escribiremos una DLL multiprocesos. Esta DLL almacenar cierta informacin a nivel de sistema usando memoria compartida entre los procesos. Vale recordar que cuando el cdigo tiene acceso a los datos compartidos entre los procesos, el programador debe proporcionar la sincronizacin apropiada. Pues los hilos mltiples en un solo proceso intrnsecamente no se sincronizan, as que los hilos principales en diversos procesos tampoco se sincronizan. Tambin miraremos algunas delicadezas que ocurren al intentar utilizar la funcin de punto de entrada para poder seguirles la pista a los hilos globales. Esta DLL compartir un solo nmero entero entre los procesos, as como mantener un contador del nmero 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 ms de cerca al cdigo, vale repasar cmo 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 annimos o con cierto nombre. Los objetos creados annimos son, en el todo, limitado para utilizar por un solo proceso, la excepcin es que pueden ser heredados por procesos hijos. Los objetos creados con un nombre se pueden compartir entre los procesos. Tpicamente, un proceso crear el objeto, especificando un nombre para ese objeto, y otros procesos abrirn 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

aplicacin se cae: muchas veces Windows hace un buen trabajo de limpieza de los manejadores despus 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 paginacin "a pedido" de imgenes 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 porcin de la memoria que se compartir entre varios procesos. Esto es asombrosamente eficiente: una vez que se instale el archivo mapeado, no se hace ningn 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 porcin de la memoria.

Inicializacin de la DLL.
La inicializacin consiste en cuatro etapas principales: Creacin de los objetos de sincronizacin (globales y otros). Creacin de datos compartidos. Incremento inicial de los contadores de hilo y de proceso. Enganchar la funcin de punto de entrada de la DLL. En la primera etapa, se crean dos objetos de sincronizacin, un mutex global, y una seccin crtica. Poco necesita ser dicho acerca de la seccin crtica. El mutex global se crea va la llamada a la API CreateMutex. Esta llamada tiene la caracterstica 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 atmicamente. Si esto no es el caso, entonces podran 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 atmicas, el programador de aplicaciones Win32 estara 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 direccin del proceso que llama. Tambin comprobamos si es el proceso que cre originalmente el archivo mapeado, si ste es el caso, entonces ponemos a cero los datos en nuestra vista mapeada. Esta es la razn por la cual el procedimiento se envuelve en un mutex: CreateFileMapping tiene las mismas caractersticas de atomicidad que CreateMutex, asegurndose de que nunca ocurrirn 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 tena un archivo fsico, 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 manipulacin en los datos global compartidos, incrementando los contadores de proceso s y de hilos, puesto que la ejecucin del cuerpo principal de la DLL es consistente con la adicin 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 seccin crtica del proceso local. Esto asegura que los hilos mltiples del mismo proceso vean una vista completamente consistentes de ambas cuentas. En la etapa final, se engancha el DLLProc, as se asegura que la creacin y la destruccin de otros hilos en el proceso es monitoreada, y la salida final del proceso tambin es registrada.

Una aplicacin usando la DLL.

Una aplicacin 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 nmero entero compartido, y crean unos o ms hilos dentro de la aplicacin, solo para verificar que los contadores locales del hilo funcionan. Segn lo esperado, los contadores de hilo se incrementan siempre que una nueva copia de la aplicacin 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 aplicacin simple, intentemos uno que haga algo avanzado. En esta situacin, el DLL se carga manualmente por el programador de la aplicacin, en vez de ser cargado automticamente. Esto es posible substituyendo la unidad con la ventana anterior por sta. Se agrega un botn adicional que carga la DLL, e instala el procedimiento manualmente. Intente ejecutar el programa, crear varios hilos de rosca y despus cargar la DLL. Debe apreciar que la DLL ya no le sigue la pista correctamente al nmero de hilos en los variados procesos que lo utilizan. Por qu es esto?. El archivo de la ayuda Win32 indica eso al usar la funcin punto de entrada con los parmetros DLL_THREAD_ATTACH y DLL_THREAD_DETACH: DLL_THREAD_ATTACH indica que el proceso actual est creando un hilo nuevo. Cuando ocurre esto, el sistema llama a la funcin 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 funcin entry-point de la DLL con el valor DLL_PROCESS_ATTACH no llama a la funcin entry-point de la DLL con el valor DLL_THREAD_ATTACH.

Observe que la funcin entry-point de una DLL es llamada con este valor solamente por los hilos creados despus de que la DLL se una al proceso. Cuando una DLL es cargada con LoadLibrary, los hilos existentes no llaman a la funcin 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 funcin 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 funcin entry-point es llamada por un hilo que termina incluso si el DLL nunca se ha unido al hilo en cuestin. El hilo era el hilo inicial en el proceso, as que el sistema llam a la funcin entry-point con el valor DLL_PROCESS_ATTACH. El hilo ya funcionaba cuando fue hecha una llamada a la funcin, as que el sistema nunca llam a la funcin entry-point para ella" Este comportamiento tiene dos efectos secundarios potencialmente desagradables.

No es posible, por lo general no perder de vista cuntos hilos estn en la DLL sobre una base global, a menos que uno pueda garantizar que una aplicacin carga la DLL antes de crear cualquier hilo hijo. Uno podra asumir equivocadamente que una aplicacin que carga una DLL tendra 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 estn 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 estn 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 funcin del punto de entrada y la inicializacin de la DLL. Si un

hilo se crea casi al mismo tiempo que la DLL es cargada por una aplicacin, 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 todava se est ejecutando. Esta es la razn por la cual es siempre una buena idea instalar la funcin del punto de entrada como la ltima accin en la inicializacin del DLL. Los lectores se beneficiaran al observar que ambos efectos secundarios tienen repercusiones al decidir cuando fijar la variable IsMultiThread.

Control de Excepciones.
Al escribir aplicaciones robustas, el programador debe prepararse siempre para las cosas que van a ir mal. Lo mismo es cierto para la programacin multihilo. La mayora 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 excepcin en un hilo no cae dentro de los mecanismos de control de excepcin estndares de la VCL. En vez de levantar una caja de dilogo, una excepcin no controlada abortar la aplicacin. Como resultado de esto, el mtodo 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 excepcin se haya capturado en un hilo, trabajar con ella es tambin un poco diferente al manejo ordinario que hace la VCL. Puede no ser apropiado demostrar una caja de dilogo siempre. Muy frecuentemente, una tctica vlida 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 comunicacin usuales, y despus 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 operacin en particular.

A pesar de esto, hay algunas situaciones con los hilos donde el tratar casos de error puede ser particularmente difcil. La mayora de estas situaciones ocurren cuando se usan hilos para realizar operaciones de fondo continuas. Recordando el captulo 10, el BAB tiene un par de hilos con 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 relacin no muy clara con ninguna operacin dentro del hilo de la VCL, y puede ser difcil comunicar la falla inmediatamente de regreso al hilo de la VCL. No solamente esto, una excepcin cualquiera en stos hilos probablemente romperan con el bucle de lectura y escritura en el que estn, planteando la difcil 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 fallarn, forzando al hilo principal que destruya y para volva a iniciar el buffer. La mejor solucin es incluir la posibilidad de tales problemas en el diseo original de la aplicacin, y determinar las mejores tentativas de recuperacin que se puedan hacer.

La BDE.
En el captulo 7, indiqu que una solucin 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 conexin 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 conexin a la base de datos. Cada aplicacin tiene un componente TSessionList llamado Sessions para permitir que esto se haga fcilmente. La explicacin detallada de sesiones mltiples est ms all del alcance de este documento.

Captulo 14. Un problema del mundo real, y su solucin.

En este captulo:

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

El problema.
En los ltimos aos he estado escribiendo un raytracer distribuido. Este utiliza TCP/IP para enviar descripciones de las escenas que se renderizarn a travs de una red desde un servidor central a un grupo de clientes. Los clientes renderizan la imagen, y despus 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 mquina. Decid que sera til escribir cierto cdigo que emulara los seckets TCP, permitiendo la comunicacin entre dos aplicaciones (cliente y servidor) en la mquina local. Varias soluciones potenciales fueron investigadas. La ms prometedora al principio pareca ser usar caeras con nombre (named pipes). Desafortunadamente surgi un problema: Los protocolos que estaba usando encima de TCP/IP asumian que la semntica de la conexin se poda realizar en base a conexiones punto-a-punto (peerto-peer): cualquier programa podra iniciar una conexin con la otra, y cualquier programa podra desconectarse en cualquier momento. La conexin y desconexin eran perfectamente simtricas: Los protocolos usados encima de TCP realizaban tres formas de inicializacin encima de aquella realizada en la capa TCP para negociar si una conexin podra ser cerrada, y si eso ocurra, cualquier extremo podra cerrar la conexin. Desafortunadamente, las caerias con nombre, no proporcionaban la semntica correcta para la desconexin, y no se las arreglaban bin frente a varias situaciones de error.

La solucin.

No tengo la intencin de explicar la solucin detalladamente, pero lectores ms avanzados pueden encontrar interesante la lectura del cdigo. Al final, decid utilizar memoria compartida para la transferencia de datos, y poner toda la sincronizacin desde el principio. La solucin fue implementada en 3 etapas.

Una DLL fue escrita la cual proporcion una caera bloqueante bidireccional entre las dos aplicaciones. Se escribieron dos hilos, uno lector y otro escritor, para permitir el acceso asincrnico a las caeras bloqueantes. Una envoltura alrededor de los hilos fue escrita para proporcionar un interfaz asincrnico similar a los sockets nonblocking.

Los archivos de la DLL y de interfaz.


MCHPipe.dpr MCHPipeInterface2.pas MCHPipeTypes.pas Este DLL es similar al ejemplo de buffer limitado que se encuentra en el captulo 9. Volviendo a ver este cdigo, puedo presumir solamente que lo deb escribr despus de un par de semanas de hackear freneticamente con el C en el trabajo, porque est mucho ms revuelto de lo que necesita ser. Un punto de inters es que los semforos usados para bloquear operaciones no asumen que los buffers limitados son de cualquier tamao en particular; en lugar de eso el estado se mantiene si los hilos lector o escritor estn bloqueados o no.

Los hilos lectores y escritores.


MCHPipeThreads.pas MCHMemoryStream.pas Los hilos de la caera son exactamente anlogos a los hilos lector y escritor en el BAB del captulo 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 semntica de protocolos de capa ms altos.

Una interfaz basada en sockets.


MCHPipeSocket.pas MCHTransactions.pas MCHPipeTransactions.pas Esta es una interfaz de sockets no muy pura, y debe ser razonablemente obvio a esos familiarizados con la programacin de sockets de TCP. Puesto que esta implementacin fue diseada para trabajar especficamente con otros protocolos que escrib, es valioso incluyendo la capa de transaccin de los protocolos sobrepuestos as que usted puede ver cmo encaja el socket dentro del en el esquema de cosas.

También podría gustarte