Tarde o temprano a cualquier programador le toca lidiar con la programación multihilo para hacer un uso más intensivo de los recursos disponibles y/o mejorar la respuesta de aplicación ante una determinada acción o tarea. En una de las cosas que más lo podemos notar es en la respuesta de la interfaz de usuario en una aplicación de escritorio donde, si la aplicación está llevando a cabo una tarea de cierta carga, el usuario tendrá la sensación de que la aplicación se ha quedado colgada y no hace nada, cuando en realidad, sigue a lo suyo.  Por lo tanto, la programación multihilo seguro que es el pan de cada día de más de uno… además, con los procesadores que llevan un tiempo en el mercado con más de un núcleo parece que es la moda lo de usar “hilos”.

Pues bien, lanzar hilos a diestro y siniestro sin control es relativamente sencillo, pero algo habrá que hacer con ellos, es decir, en algún momento nos puede interesar “manejar” un hilo. Principalmente lo que vamos a poder hacer es acabar con la ejecución de un hilo de una forma más o menos controlada. Existen varias formas de enfocar el como terminar la ejecución de un hilo.

La primera de ellas, es disponer de una variable que determine cuando termina un bucle donde se realiza el trabajo que realiza el subproceso. Para este esquema, tenemos que pensar en como dividir el trabajo del subproceso en diferentes subtrabajos que se corresponderán con las diferentes iteraciones del bucle. Esto nos proporciona una ventaja y es que en caso de querer reanudar esta tarea en un nuevo subproceso, dicha tarea se encontrará en un estado “seguro” puesto que los datos con los que trabajemos serán coherentes y podremos continuar sin problemas. Como principal desventaja tenemos que la ejecución del subproceso no termina de forma inmediata.

private Thread t;

private volatile bool terminado;

 

private void LanzarHilo()

{

    t = new Thread(new ThreadStart(DoWork));

    terminado = false;

 

    t.Start();

}

 

private void DetenerHilo()

{

    terminado = true;

}

 

private void DoWork()

{

    while (!terminado)

    {

        //realizar subtrabajo...

        //si antes de detener el hilo, termina

        //con su tarea salimos

        if (concluido)

            terminado = true;

    }

}

El código no creo que tenga mayor complicación para entenderlo, puntualizar que la variable que determina cuando finaliza el while está declarada como volatile, por lo que se eliminan las optimizaciones que pueda establecer el compilador para el uso por un solo subproceso y se garantiza que siempre se acceda al valor más actualizado de la variable por cualquier subproceso.

El otro esquema que podemos utilizar para terminar con la ejecución de un hilo es mediante el uso de la llama a Abort(), que termina de forma abrupta con la ejecución del hilo. Esta llamada lanza una excepción especial del tipo ThreadAbortException que se produce en el subproceso, por lo tanto el código definido en el método o función que ejecuta el subproceso debe de estar dentro de un bloque try-catch y capturar, al menos, este tipo de excepción para saber cuando se produce la cancelación y realizar las operaciones que creamos convenientes. Tendríamos algo así:

private void DoWork()

{

    try

    {

        //subprocesamiento...

    }

    catch (ThreadAbortException e)

    {

        //tratamiento de la excepcion

    }

    finally

    {

 

    }

}

Esto tiene dos implicaciones, la primera es que si pensábamos que al llamar a Abort() la cancelación del hilo se produce de forma inmediata no es algo del todo correcto, puesto que podríamos seguir ejecutando código de forma ilimitada dentro de los bloques catch-finally. La otra implicación viene dada por ese carácter especial que le hemos dado antes a la excepción ThreadAbortException, y es que la particularidad de esta excepción viene dada porque una vez que finaliza el bloque try-catch donde se captura la excepción producida por la llamada al método Abort(), se vuelve a producir de forma la misma excepción de forma automática. De este modo si existiera más código después del bloque finally, éste no se ejecutaría.

Tenemos la posibilidad de que esta excepción no se vuelva a producir mediante la llamada a Thread.ResetAbort(), este método anula la cancelación del subproceso.

private void DoWork()

{

    try

    {

        //subprocesamiento...

    }

    catch (ThreadAbortException e)

    {

        Thread.ResetAbort();

    }

    finally

    {

        //…

    }

 

    //mas codigo…

}

En este caso, el código que sigue al bloque finally si que se ejecutaría puesto que hemos llamado a ResetAbort(). Además, conviene incluir la llamada en el bloque catch, donde tenemos las certeza que se ha llamado a Abort(), ya que si la llamada a ResetAbort() se realiza cuando aun no se ha lanzado la excepción ThreadAbortException, esta condición podría darse en el bloque finally, se produce una excepción del tipo ThreadStateException.

Una vez que se ha ejecutado el bloque finally, podríamos seguir ejecutando más código, pero ¿qué ocurre si se llama de nuevo a Abort()? No se producirá ninguna nueva excepción, independientemente de si hemos llamado a ResetAbort() o no. Una vez que llamamos a Abort(), el thread pasará a un estado llamado AbortRequested, indicando que se está esperando a que se produzca la excepción ThreadAbortException para que se produzca la finalización del hilo. Luego sucesivas llamadas a Abort() no tienen efecto ninguno y ya no disponemos de la posibilidad de abortar el hilo nuevamente.

Para evitar el perder la posibilidad de volver a cancelar el thread, podemos “relanzarlo” en el momento de capturar la excepción producida por la llamada a Abort() mediante un delegado que apunte al mismo método que ejecuta el hilo. 

private delegate void MyDelegate();

private MyDelegate myDlg;

 

public void LanzarHilo()

{

    t = new Thread(new ThreadStart(DoWork));

 

    myDlg += new MyDelegate(DoWork);

}

 

private void DetenerHilo()

{

    t.Abort(/*data...*/);

}

 

private void DoWork()

{

    try

    {

        //asignamos el hilo a la ref global para manejarla posteriormente

        t = Thread.CurrentThread;

 

        //subprocesamiento...

    }

    catch (ThreadAbortException e)

    {

        Thread.ResetAbort();

 

        //podemos utilizar el objeto que pasamos en la

        //llamada a Abort() para determinar que hacer

 

        //if (e.ExceptionState...)

        //dejar datos en un estado coherente

        myDlg.BeginInvoke(null, null);

    }

}

La llamada Abort() puede tomar como argumento un objeto que luego podemos recuperar mediante la propiedad ExceptionState de la excepción para determinar las condiciones que han producido que se cancele el hilo y así determinar si queremos relanzarlo o dejarlo que muera. Cuando se produce la llamada a BeginInvoke del delegado en caso de que queramos relanzar el hilo, se va a crear un nuevo thread que volverá a ejecutar el método DoWork(). Si analizamos el thread que se ejecuta al llamar a BeginInvoke podemos ver que se ejecuta en background y además, que es un hilo perteneciente al ThreadPool de .Net, por lo que la capacidad de realizar este esquema o patrón repetidas veces de forma simultanea estará limitada al número de hilos que se puedan ejecutar del forma simultanea mediante el ThreadPool.

¿En qué casos nos puede resultar este modelo útil? Podemos disponer de una subtarea potencialmente bloqueante y que, si transcurrido un determinado tiempo (determinado, por ejemplo, mediante timer) no se ha completado, la cancelemos  y la relanzemos el número de veces que creamos convenientes.