Main logo La Página de DriverOp

Capítulo 5. Más sobre destrucciones de hilos. Deadlock.

En este capitulo:

  • El método WaitFor.
  • Terminación controlada de hilos – Enfoque 2.
  • Una rápida introducción al pasaje de mensajes y notificaciones.
  • WaitFor puede resultar en largas demoras.
  • ¿Haz notado el bug?
  • Evitando esta particular manifestación de Deadlock.

El método WaitFor.

El evento OnTerminate, discutido en el capítulo anterior, es muy útil si estás usando hilos que inicializas y luego los olvidas, con destrucción automática. ¿Que pasa si, en cierto punto de la ejecución del hilo principal de la VCL, quieres asegurarte de que todos los demás hilos hayan terminado? La solución a esto es el método WaitFor. Este método es útil si:

  • El hilo principal de VCL necesita acceder al objeto hilo en funcionamiento antes de que su ejecución haya terminado, y ya no se pueda leer o modificar datos en el hilo.
  • Forzar la terminación de un hilo cuando se termina el programa no es una opción viable.

Bastante sencillo. Cuando el hilo A llama al método WaitFor del hilo B, el hilo A queda suspendido hasta que el hilo B termina su ejecución. Cuando el hilo A se vuelve a activar, puede estar seguro que los resultados del hilo B se pueden leer, y que el objeto hilo representado por B puede ser destruido. Típicamente esto ocurre cuando el programa termina, donde el hilo principal de VCL llamará el método Terminate en todos los hilos no-VCL y luego al método WaitFor en todos los hilos no-VCL antes de salir.

Terminación controlada de hilos – Enfoque 2.

En este ejemplo, modificaremos el código del programa de números primos de modo que sólo un hilo se ejecute por vez, y el programa espere hasta que el hilo complete su ejecución antes de salir. A pesar de que en este programa no es estrictamente necesario esperar a que los hilos terminen, es un ejercicio útil y demuestra algunas propiedades de WaitFor que no son siempre deseables. Tambien ilustra algunos claros bugs con los que se pueden topar programadores principiantes. Primero que nada, el código del formulario principal. Como puede ver, hay varias diferencias con el ejemplo anterior:

  • Tenemos un “número mágico” declarado al inicio del unit. Este es un número arbitrario de mensaje, y su valor no es importante; es el único mensaje en la aplicación con este número.
  • En vez de tener un conteo de hilos, mantenemos una referencia explícita a un hilo y sólo un hilo, apuntado por la variable FThread del formulario principal.
  • Sólo queremos que un hilo se ejecute por vez, ya que sólo tenemos una única variable apuntando al hilo que realizará el trabajo. Por este motivo, el código de creación del hilo verifica si hay hilos ejecutándose, antes de crear otros.
  • El código de creación del hilo no establece la propiedad FreeOnTerminate a verdadero. En cambio, el hilo principal de VCL liberará el hilo en funcionamiento más tarde.
  • El hilo principal tiene un manejador de mensajes definido que espera que el hilo en ejecución se complete y entonces lo libera.
  • De igual modo, el código ejecutado cuando el usuario desea liberar el formulario espera que el hilo en ejecución se complete y lo libera.

Habiendo notado estos puntos, aquí esta el hilo que hará el trabajo. Nuevamente, hay algunas diferencias con el código presentado en el capitulo 3.

  • La función IsPrime verifica ahora si se solicitó que el hilo termine, resultando en una rápida salida si la propiedad terminated es establecida.
  • La función Execute verifica si se produjo una terminación anormal.
  • Si la terminación fue normal, entonces usa synchronize para mostrar los resultados, y envía un mensaje al formulario principal solicitando que el formulario principal lo libere.

Una rápida introducción al pasaje de mensajes y notificaciones.

Bajo circunstancias normales, el hilo es ejecutado, corre por su curso, usa synchronize para mostrar los resultados y luego envía un mensaje al formulario principal. Este envío de mensaje es asincrónico: el formulario principal toma el mensaje en algún punto en el futuro. PostMessage no suspende el trabajo del hilo en ejecución, lo hace correr hasta que se complete. Esta es una propiedad muy útil: no podemos usar synchronize para decirle al formulario principal que libere al hilo, porque volveremos de la llamada a Synchronize a un hilo que no existe más. En cambio, esto simplemente actúa como una notificación, un gentil recordatorio para el formulario principal de que debe liberar el hilo tan rápido como le sea posible.

En un momento posterior, el hilo del programa principal recibe el mensaje y ejecuta al manejador. Este manejador verifica si el hilo aún existe y, si existe, espera a que se complete su ejecución. Este paso es necesario porque si bien es sabido que el hilo en ejecución está terminando (no hay muchas sentencias más luego del PostMessage), esto no es una garantía. Una vez que la espera haya terminado, el hilo principal puede liberar el hilo que hizo el trabajo.

El diagrama de abajo ilustra este primer caso. Para mantenerlo simple, fueron omitidos los detalles de la operación de Synchronize del diagrama. Además, la llamada a PostMessage se muestra como que ocurre en algún momento antes de que el hilo completa su funcionamiento de modo de ilustrar el funcionamiento de la operación WaitFor.

En capítulos posteriores se va a cubrir la ventaja de enviar mensajes con mayor detalle. Es suficiente decir hasta este punto que esta técnica es muy útil cuando se trata de comunicarse con el hilo VCL.

En un caso anormal de funcionamiento, el usuario intentará salir de la aplicación, y confirmará que desea salir inmediatamente. El hilo principal establecerá la propiedad terminated del hilo en proceso, lo que se espera que provoque una terminación en un tiempo razonablemente corto, y luego aguardará para que este se complete. Una vez que se ha completado el procesamiento del hilo, el proceso de liberación es como el caso anterior. El diagrama de abajo ilustra el nuevo caso.

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

WaitFor puede resultar en largas demoras.

El beneficio de WaitFor es también su mayor desventaja: suspende el hilo principal en un estado en el que no puede recibir mensajes. Esto significa que la aplicación no puede realizar ninguna de las operaciones normalmente asociadas con el procesamiento de mensajes: la aplicación no re-dibujará, no se re-dimensionará ni responderá a ningún estímulo externo cuando está esperando. Tan pronto como el usuario lo note, pensará que la aplicación se colgó. Esto no es un problema en el caso de un hilo que termina normalmente; llamando a PostMessage, la última operación en el hilo en funcionamiento, nos aseguramos de que el hilo principal no tendrá que esperar mucho. Sin embargo, en el caso de una terminación anormal del hilo, la cantidad de tiempo que el hilo principal pierde en este estado depende de que tan frecuentemente verifique el hilo de ejecución la propiedad terminate. El código fuente para PrimeThread tiene una línea marcada “Line A”. Si se le quita el fragmento “and not terminated”, podrá experimentar que sucede al finalizar la aplicación durante la ejecución de un cálculo que dure mucho tiempo.

Hay algunos métodos avanzados para suprimir este problema que involucra a las funciones Win32 de espera de mensajes, una explicación de este método se puede encontrar visitando http://www.midnightbeach.com/jon/pubs/MsgWaits/MsgWaits.html. En suma, es simple escribir hilos que verifican la propiedad Terminated con cierta regularidad. Si esto no es posible, entonces es preferible mostrarle algunas advertencias al usuario acerca de la potencial irresponsabilidad de la aplicación (a la Microsoft Exchange).

¿Haz notado el bug? WaitFor y Synchronize: una introducción a Deadlock.

La demora de WaitFor es realmente un problema menor, cuando se lo compara con otros vicios que tiene. En aplicaciones que usan Synchronize y WaitFor, es completamente posible hacer que la aplicación caiga en un Deadlock. Deadlock es un fenómeno donde no hay problemas de algoritmos en la aplicación, pero toda la aplicación se detiene, muerta en el agua. El caso general es que Deadlock ocurra cuando un hilo espera por el otro en forma cíclica. El hilo A esta esperando por el hilo B para completar algunas operaciones, mientras que el hilo C espera por el hilo D, etc. etc. Al final de la línea, el hilo D estará esperando por el hilo A para completar algunas operaciones. Desgraciadamente el hilo A no puede completar la operación porque está suspendido. Esto es el equivalente en computación del problema: “A: Tu vas primero… B: No, tu… A: No, ¡insisto!” que acosa a los motoristas cuando el derecho de paso no está claro. Este tipo de funcionamiento está documentado en los archivos de ayuda de la VCL.

En este caso en particular, el Deadlock puede ocurrir entre dos hilos de ejecución si el hilo de cálculo llama a Synchronize poco tiempo antes de que el hilo principal llame a WaitFor. Si esto sucediera, entonces el hilo de cálculo estará esperando que el hilo principal se libere para regresar al bucle de mensajes, mientras que el hilo principal está esperando que el hilo de cálculo se complete. Deadlock ocurrirá. También es posible que el hilo principal de VCL llame a WaitFor poco tiempo antes de que el hilo de cálculo llame a Synchronize. Dando una implementación simplista, esto también resultaría en un Deadlock. Por suerte, los que hicieron la VCL trataron de sortear este caso de error, lo que resulta en el surgimiento de una excepción en el hilo de cálculo, rompiendo el Deadlock y finalizando el hilo.

La programación del ejemplo, como está, se vuelve bastante indeseable. El hilo de cálculo llama a Synchronize si verifica que Terminated está es falso poco antes de terminar su ejecución. El hilo principal de la aplicación establece terminated poco antes de llamar a WaitFor. De modo que, para que ocurra un Deadlock, el hilo de cálculo deberá encontrar Terminated en falso, ejecutar Synchronize, y luego el control debe ser transferido al hilo principal exactamente en el punto donde el usuario ha confirmado forzar la salida.

Más allá del hecho de que estos casos de Deadlock son indeseables, eventos de este tipo son claras condiciones de carrera. Todo depende del momento exacto de los eventos, lo que variará de funcionamiento en funcionamiento en la máquina. El 99.9% de las veces, un cierre forzado funcionará, y una en mil veces, todo se bloqueará: exactamente el tipo de problema que necesitamos evitar a toda costa. El lector recordará que anteriormente le mencioné que ninguna sincronización de gran escala ocurrirá cuando se está leyendo o escribiendo la propiedad terminated. Esto quiere decir que no es posible usar la propiedad terminated para evitar este problema, como el diagrama anterior lo deja en claro.

Algún lector interesado en duplicar el problema del Deadlock, puede hacer relativamente fácil, modificando los siguientes fragmentos del código fuente:

  • Quite el texto “and not terminated” a la altura de “Line A”
  • Remplace el texto “not terminated” a la altura de “Line B” por “true”
  • Quite el comentario en “Line C”

El deadlock puede ser entonces provocado corriendo un hilo cuya ejecución demore cerca de 20 segundos, y forzar la salida de la aplicación poco tiempo después de que el hilo fue creado. El lector puede desear también ajustar el tiempo que el hilo principal de la aplicación se suspende, de modo de saber el “correcto” ordenamiento de los eventos:

  • El usuario comienza cualquier hilo de cálculo.
  • El usuario intenta salir y dice: “Sí, quiero salir más allá de que haya un hilo en funcionamiento”.
  • El hilo principal de la aplicación se suspende (Line C)
  • El hilo de cálculo eventualmente llega al final de la ejecución y llama a Synchronize. (asistido por las modificaciones en las líneas A y B).
  • El hilo principal de la aplicación se reactiva y llama a WaitFor.

Evitando esta particular manifestación de Deadlock.

El mejor modo de evitar esta forma de Deadlock, es no usar WaitFor y Synchronize en la misma aplicación. WaitFor puede ser evitado usando el evento OnTerminate, como fue expuesto previamente. Por suerte, en este ejemplo, el resultado del hilo es suficientemente simple como para evitar usar Synchronize a favor de un modo más trivial. Usando WaitFor, el hilo principal puede ahora acceder legalmente a las propiedades del hilo en funcionamiento luego de que éste termina, y todo lo que se necesita es una variable “resultado” para contener el texto producido por el hilo de cálculo. Las modificaciones necesarias para esto son:

  • Quitar el método “DisplayResults” del hilo.
  • Agregar una propiedad al hilo de cálculo.
  • Modificar el manejador  de mensajes en el formulario principal.

Aquí hay cambios relevantes. Con esto termina la discusión de los mecanismos de sincronización comunes a todas las versiones Win32 de Delphi. Aún no he discutido dos métodos: TThread.Suspend y TThread.Resume. Estos son discutidos en el capitulo 10. Los siguientes capítulos exploran las facilidades del API Win32, y posteriores versiones de Delphi. Sugiero que, una vez que el usuario haya asimilado los aspectos básicos de la programación multihilo en Delphi, se tome el tiempo de estudiar estos mecanismos más avanzados, ya que son una buena manera, más flexible, que trabajar con los mecanismos nativos de Delphi, y permiten al programador coordinar hilos de ejecución en un modo más elegante y eficiente, así como reducir las posibilidades de escribir código que pueda caer en Deadlocks.

Martin Harvey -