Documentos de Académico
Documentos de Profesional
Documentos de Cultura
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.
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).
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
Consideraciones de completado, terminacin y destruccin de hilos. Terminado prematuro de hilos. El evento OnTerminate.
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
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.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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!
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.
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.
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.
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.
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.
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).
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.
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?
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
4 4
0 0
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?
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.
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.
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.
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.
discuti en los captulos anteriores. Y el ltimo punto para preocuparnos es lo has adivinado! Liberacin de recursos y limpieza.
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.
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.
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.
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.
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.
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.
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.
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.
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
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?
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.
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.
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.
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!
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.
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.
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.
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.
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.
Mayor eficiencia va operaciones de interbloqueo. Atomicidad desde la nada. Eventcounts y secuenciadores. Otros dispositivos Win32 para la sincronizacin.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.