Controlando la ejecución de threads

Continuando con un tema que ya he tratado en algún artículo anterior (éste y éste) voy a hablar algo más sobre el manejo de hilos en .NET. En esta ocasión voy a explicar cómo controlar la ejecución de los hilos, pausándolos y reanudándolos conforme lo necesitemos.

La clase Thread dispone de métodos que podríamos considerar indicados para estas labores, como Suspend y Resume, pero tienen un gran inconveniente que ha hecho que se declaren como deprecated (obsoletos) en .NET 2.0. El problema radica en que cuando se suspende un proceso no sabemos en qué punto exacto de ejecución se realiza esta parada, pudiendo probocar fallos de funcionamiento que además suelen ser complejos de depurar. La solución que se ha adoptado, y que por cierto coincide con la de Java, es que se utilicen otras clases para sincronizar los hilos, de forma que su ejecución se pueda detener y reanudar de forma controlada.

En el siguiente código muestro una posible alternativa utilizando la clase Monitor. El programa es sencillo, se crean dos hilos que se ponen en ejecución, y se aceptan una serie de comandos como «pause 1» para detener el hilo 1, o «play 1» para reanudarlo. Cada hilo se limita a escribir en consola un número cada dos segundos.

using System;
using System.Threading;
 
namespace Hilos
{
    class Ejemplo
    {
        [STAThread]
        static void Main(string[] args)
        {
            Hilo h1 = new Hilo("Hilo 1");
            Hilo h2 = new Hilo("Hilo 2");
 
            Thread th1 = new Thread(new ThreadStart(h1.Work));
            Thread th2 = new Thread(new ThreadStart(h2.Work));
            th1.Start();
            th2.Start();
 
            Console.WriteLine("Comandos: pause 1, pause 2, play 1, play 2, end 1, end 2, quit");
            string c = "";
            while(c!="quit")
            {
                c = Console.ReadLine();
                switch(c)
                {
                    case "pause 1": h1.Pause();break;
                    case "pause 2": h2.Pause();break;
                    case "play 1" : h1.Play();break;
                    case "play 2" : h2.Play();break;
                    case "end 1"  : h1.End();break;
                    case "end 2"  : h2.End();break;
                    case "quit": h1.End();h2.End();break;
                    default: break;
                }
            }
 
            th1.Join();
            th2.Join();
 
        }
    }
 
    public class Hilo
    {
        private bool paused = false;
        private bool end    = false;
        private string name = "";
 
        public Hilo(string name)
        {
            this.name = name;
            this.end = false;
            this.paused = false;
        }
 
        public void Work()
        {
            int cont = 0;
            while(!end)
            {
                cont++;
                Console.WriteLine(name+": "+cont);
                Thread.Sleep(2000);
 
                lock(this)
                {
                    if(this.paused) Monitor.Wait(this);
                }
 
            }
        }
 
        public void Pause()
        {
            lock(this)
            {
                this.paused = true;
                Console.WriteLine("Parando hilo "+name);
            }
        }
 
        public void Play()
        {
            lock(this)
            {
                this.paused = false;
                Console.WriteLine("Continuando hilo "+name);
                Monitor.PulseAll(this);
            }
        }
 
        public void End()
        {
            lock(this)
            {
                this.end = true;
                Console.WriteLine("Finalizando hilo "+name);
                Monitor.PulseAll(this);
            }
        }
    }
 
}

Cuando el usuario solicita detener un determinado hilo se llama al método Pause. En el bucle del método Work del hilo lo que hacemos tras cada iteración, es comprobar si se ha solicitado que el trabajo se detenga. Puede apreciarse que el comportamiento es diferente a la utilización de Suspend, en vez de detener el hilo directamente, le hemos solicitado que se detenga, es responsabilidad del hilo hacer caso a la señal que le hemos mandado y detenerse en un punto donde pueda hacerlo.

En nuestro caso el punto que hemos decidido es tras cada iteración, por lo que en el caso de que se haya solicitado detener el hilo hacemos un Monitor.Wait, produciéndose que el hilo se detenga. El hilo permanecerá así hasta que solicitemos que se reanude mediante una señal Pulse o PulseAll, por lo que en el método Play marcamos el hilo como no detenido y ejecutamos PulseAll, produciéndose que el hilo reanude su ejecución en la siguiente línea a donde se había detenido, Monitor.Wait en Work.

Merece la pena mencionar que tanto la llamada a Wait como a PulseAll y los lock se realizan pasándoles un objeto como parámetro. Este es el objeto que utilizamos como elemento de sincronización, podría ser un objeto concreto o uno creado explícitamente para ello, pero muchas veces es suficiente con utilizar this. Lo importante es que el objeto indicado en el bloque lock coincida con usado en el Monitor y que los métodos trabajen con el mismo objeto. Si no lo hiciéramos de esta forma podríamos quedarnos atrapados en un lock, si no coincide su objeto con el del monitor interno, o podríamos quedando detenidos eternamente si hacemos Wait sobre un objeto y PulseAll sobre otro.

Referencias

Twitter Digg Delicious Stumbleupon Technorati Facebook Email

2 Respuestas para “Controlando la ejecución de threads”

  1. Muy sencillo y muy explicativo tu ejemplo, me ha sido de gran ayuda, y ahora entiendo por que esta deprecado el metodo suspend 😉 gracias man!

  2. Muchas gracias hermano, me pongo de pie, me sirvio de mucho!