Main logo La Página de DriverOp

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

En este capitulo:

  • Semáforos.
  • ¿Qué hay de los conteos por encima de uno? Secciones “no tan críticas”.
  • Un nuevo uso para los semáforos: administración del flujo de datos y control de flujo.
  • El buffer limitado.
  • Una implementación Delphi del buffer limitado.
  • Creación: Inicializando los semáforos correctamente.
  • Operación: valores correctos de espera.
  • Destrucción: Liberando todo.
  • Destrucción: Las sutilezas continúan.
  • ¡Los accesos a los manejadores de sincronización deben ser sincronizados!
  • Administración de manejadores Win32.
  • Una solución.
  • Usando el buffer limitado: un ejemplo.
  • Un par de puntos finales…

Semáforos.

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

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

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

  • CreateSemaphore. Esta función es similar a CreateMutex. En lugar de una marca indicando que el hilo que está creando el mutex quiere ser su propietario inicialmente, esta función toma un argumento indicando el conteo inicial. Crear un mutex con la propiedadinicial es similar a crear un semáforo con un conteo de 0: en ambos casos, cualquier hilo que espera por el objeto será bloqueado. Del mismo modo, crear un mutex sin la propiedad inicial es similar a crear un semáforo con un conteo de 1: en ambos casos, uno y sólo un hilo no será bloqueado cuando espera para tomar posesión del objeto de sincronización.
  • Funciones de espera. Las funciones de espera son idénticas en ambos casos. Con mutex, una espera exitosa da la propiedad del mutex al hilo. Con semáforos, una espera exitosa decrementa el conteo del semáforo, o si el conteo es 0, bloquea el hilo en espera.
  • ReleaseSemaphore. Esto es similar a ReleaseMutex, pero en lugar de liberar la propiedad del objeto, ReleaseSemaphore toma un valor entero extra, como argumento para especificar en cuando debe ser incrementado el conteo. ReleaseSemaphore puede incrementar el conteo en el semáforo, o activar el número apropiado de hilos bloqueados en el semáforo o ambos.

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

Mutexes Semáforos.
MiMutex := CreateMutex(nil, FALSE, ); MiSemaforo := CreateSemaphore(nil, 1, 1, );
MiMutex := CreateMutex(nil, TRUE, ); MiSemaforo := CreateSemaphore(nil, 0, 1, );
WaitForSingleObject(MiMutex, INFINITE); WaitForSingleObject(MiSemaforo, INFINITE);
ReleaseMutex(MiMutex); ReleaseSemaphore(MiSemaforo, 1);
CloseHandle(MiMutex); CloseHandle(MiSemaforo);

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

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

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

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

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

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

El buffer limitado.

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

  • Create Buffer: El buffer y cualquier mecanismo asociado de sincronización son creados e inicializados.
  • Put Item: Este intenta colocar un ítem en el buffer de un modo seguro entre hilos. Si no es posible, porque el buffer está lleno, entonces el intento del hilo para colocar un ítem en el buffer es bloqueado (suspendido) hasta que el buffer esté en un estado que permita que sean agregados más datos.
  • Get Item: Este intenta tomar un ítem fuera del buffer en un modo seguro entre hilos. Si esto no fuera posible, porque el buffer está vacío, el intento del hilo por tomar un ítem será bloqueado (suspendido) hasta que el buffer esté en un estado que permita que sean quitados datos.
  • Destroy Buffer: Esto desbloquea todos los hilos esperando en el buffer y destruye el buffer.

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

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

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

Bloqueando el hilo apropiado, ya fuera que el buffer se torne vacío o lleno, detiene a uno u otro hilo de “pasarse de vueltas”. Dado un tamaño de buffer de N, el hilo productor puede estar sólo a N ítems de distancia del hilo consumidor antes de que éste sea suspendido, y de igual modo, el hilo consumidor no puede estar más de N ítems atrás. Esto nos trae algunos beneficios:

  • Un hilo no puede sobre-producir, de modo que se evita el problema visto en el capítulo 6, donde teníamos la salida de un hilo colocándose en cola en una lista de un tamaño cada vez mayor.
  • El buffer es de tamaño finito, a diferencia de la lista del enfoque visto anteriormente, de modo que podemos limitar mejor el uso de memoria.
  • No hay “esperas ocupadas”. Cuando un hilo no tiene nada que hacer, está suspendido. Esto evita las situaciones donde los programadores escriben pequeños bucles que no hacen nada más que esperar por más datos sin ser bloqueados. Esto debe ser evitado, ya que desperdicia tiempo del microprocesador.

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

Acción del hilo lector Acción del hilo escritor Contador de entradas libres Contador de entradas en uso
Comienza el hilo. Hilo inactivo (no activado) 4 0
Espera(EntriesUsed) bloquea. Suspendido.   4 0
  Espera(EntriesFree) pasa. 3 0
  Agrega Item. Marca(EntriesUsed) 3 1
  Espera(EntriesFree) pasa 2 1
  Agrega Item. Marca(EntriesUsed) 2 2
  Espera(EntriesFree) pasa 1 2
  Agrega Item. Marca(EntriesUsed) 1 3
  Espera(EntriesFree) pasa 0 3
  Agrega Item. Marca(EntriesUsed) 0 4
  Espera(EntriesFree) bloquea. Suspendido. 0 4
Espera(EntriesUsed) completa.   0 3
Quita Item. Marca(EntriesFree)   1 3
Espera(EntriesUsed) pasa.   1 2
Quita Item. Marca(EntriesFree)   2 2
Espera(EntriesUsed) pasa.   2 1
Quita Item. Marca(EntriesFree)   3 1
Espera(EntriesUsed) pasa.   3 0
Quita Item. Marca(EntriesFree)   4 0
Espera(EntriesUsed) bloquea. Suspendido.   4 0

Una implementación Delphi del buffer limitado.

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

  • ¿Qué valores deben ser entregados a la llamada de creación del semáforo?
  • ¿Qué tan larga debe ser la espera en el mutex o la sección crítica?
  • ¿Qué tan larga debe ser la espera en el semáforo?
  • ¿Cuál es la mejor manera de destruir limpiamente al buffer?

Creación: Inicializando los semáforos correctamente.

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

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

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

Operación: valores correctos de espera.

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

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

Destrucción: Liberando todo.

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

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

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

Destrucción: Las sutilezas continúan.

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

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

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

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

Administración de manejadores Win32.

Este problema es lo suficientemente espinoso que vale la pena ver qué pasa exactamente cuando hacemos una llamada a Win32 para cerrar un mutex o un semáforo. En particular, es realmente útil saber:

  • Al cerrar un manejador, ¿se desbloquean los hilos esperando en ese mutex o semáforo en particular?
  • En el caso de los mutexes, ¿hace alguna diferencia si uno es propietario del manejador cuando libera el mutex?

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

Una solución.

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

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

  • WAIT_OBJECT_0 (Éxito)
  • WAIT_ABANDONED
  • WAIT_TIMEOUT

Primero que nada, si el semáforo es liberado, la función devuelve WAIT_OBJECT_0, y no se requiere ninguna otra acción. En segundo lugar, en el caso donde la función WaitFor de Win32 devuelva WAIT_ABANDONED, la función devuelve error; este valor de error en particular indica que un hilo ha salido sin liberar apropiadamente un objeto de sincronización. El caso en el que estamos más interesados es donde la espera devuelve time-out. Esto puede ser por dos razones posibles:

  • El hilo podría estar bloqueado por un largo período de tiempo.
  • El buffer interno fue destruido sin que se haya reactivado ese hilo en particular.

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

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

Usando el buffer limitado: un ejemplo.

Para facilitarle una estructura a este ejemplo, se concibió una simple aplicación usando dos hilos. Esta aplicación busca números primos palíndromos (palíndromos son los números que se leen igual de derecha a izquierda y de izquierda a derecha). Un par de primos palíndromos existirá cuando dos números, X e Y sean los dos primos, y además Y sea el palíndromo de X. Ni X ni Y necesitan ser números palíndromos en sí mismos, sin embargo si uno de ellos lo es, entonces X = Y, lo que es un caso especial. Ejemplos de primos palíndromos incluyen: (101, 101), (131, 131), que son ambos casos especiales y (16127, 72161), (15737, 73751) y (15683, 38651), que no son casos especiales.

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

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

Un par de puntos finales…

¿Y pensabas que no se podía decir nada más acerca de la destrucción? Hay un tema final para mencionar. El código del buffer limitado asume que los hilos intentarán acceder a los campos del objeto después de que el buffer haya sido destruido. Esto está bien, pero significa que cuando destruimos los dos hilos y el buffer entre ellos, debemos reiniciar el estado del buffer, luego esperar a que todos los hilos terminen, y sólo entonces liberar el buffer, liberando la memoria que ocupa el objeto. Fallar al hacer esto, pueden resultar en violaciones de accesos. La función StopThreads hace esto correctamente, asegurando una salida limpia.

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

Martin Harvey -