viernes, 27 de junio de 2014

Receta Multithreading en C# No. 1-4: Abortar un Thread en Ejecución

Tabla de Contenido

0. Introducción
1. Problema
2. Solución
3. Discusión de la Solución
3.1 Versión Abort
3.1.1 Detalles de implementación en .NET Framework
3.1.2 Ejemplos
3.1.2.1 Ejemplo básico
3.1.2.2 Detener un thread en un ciclo infinito
3.1.2.3 Verificación de estados de un thread
3.1.2.4 Inmortalidad de un thread
3.2 Veresión Abort(Object)
4. Práctica: Código C#
5. Conclusiones
6. Glosario
7. Literatura & Enlaces

0. Introducción

Aprovechemos esta nueva oportunidad para hablar acerca de otros de los métodos con los que cuenta la clase System.Threading.Thread: se trata del método que nos sirve para aborta un thread que se encuentre en ejecución, Abort. Exploraremos las versiones sobrecargadas de este método, junto con usos, ejemplos. También introduciré, grosso modo, el funcionamiento de Abort en .NET, y la interacción de este con el sistema operativo. Además, de algunas consideraciones sobre el uso (y peligros) al intentar abortar un método. ¡Empecemos!

1. Problema

Proponer una solución para cancelar un thread que encuentre en ejecución.

2. Solución

La clase Thread (System.Threading) cuenta con el método sobrecargado Abort para abortar la ejecución de un thread. La Figura 1 muestra la lista sobrecargada de este miembro.
Lista sobrecargada del método Thread.Abort
Figura 1. Lista sobrecargada del método Thread.Abort [2].

3. Discusión de la Solución

Discutamos las dos versiones sobrecargadas del método Abort en las siguientes subsecciones.

3.1 Versión Abort()

Interna e implícitamente este método lanza una excepción ThreadAbortException [7] sobre el thread sobre el que se invoco el método Abort para iniciar el proceso de terminación de su ejecución. Esta es su firma [10]:

public void Abort()

Descripción puntual:
  • Parámetros: «Ninguno»
  • Valor de retorno: void

3.1.1 Detalles de implementación

Gracias al poder del Framework .NET, como programadores podemos manipular la operación de abortar un thread en ejecución desde código de aplicación para atrapar la excepción ThreadAbortException y mantener la aplicación en ejecución sin fallas fatales o inconsistencias en tiempo de ejecución.

Por otro lado, vale incluir cómo el Framework .NET lleva a cabo tareas complejas detrás de cámaras para implementar el método Abort: (Extracción literal desde [4].)
  1. Suspend the underlying OS thread to abort.
  2. Set the .NET thread to abort state to include the AbortRequested bit.
  3. Wait for the thread to abort to be interruptible, that is, sleeping, waiting, or joining.
  4. Add an asynchronous procedure call (APC) to thread's APC queue (using the Win32 function QueueUserAPC) and resume the thread.
  5. When the thread to abort moves into an alertable wait state, the scheduler will call the APC, which sets the thread to abort state to AbortInitiated. A thread enters an alertable wait state only when it passes TRUE as bAlertable to a function like SleepEx. If that doesn't happen, the APC queue is never serviced, which means that Thread.Abort is not guaranteed to cause an exception in the thread to abort.
  6. When the Common Language Runtime (CLR) gets back control of the thread to abort, it will return to execution via a "trip" function, which checks all kinds of states for special activity, including if the thread should be aborted.
  7. If the thread's state is set to AbortInitiated, throw the ThreadAbortException, which the thread being aborted can handle via catch and/or finally (or not, as it chooses).
Como es evidente, se trata de un proceso que involucra la interacción con las API (e.g., Win32) del sistema operativo, funciones de bajo nivel, entre otros detalles que son gestionados por el Framework .NET.

3.1.2 Ejemplos

Comprendamos el funcionamiento de este método con estos ejemplos.

3.1.2.1 Uso básico de Abort

El siguiente código de ejemplo demuestra cómo iniciar y detener inmediatamente un thread.

En la línea 10 creamos un nuevo thread y especificamos como método a encapsular en el delegado ThreadStart MostrarMensaje (líneas 30-36). Con la sentencia if (línea 12) validamos que el estado del thread threadNuevo no sea finalizado. Sobre la línea 15 iniciamos la ejecución del thread, e inmediatamente en la línea 18 abortamos su ejecución. Este sentencia evita que no se ejecute ninguna sentencia del cuerpo de implementación del método MostrarMensaje. Solo se ejecutará las sentencias restantes del thread Main.

Compilación:


  1. csc /target:exe UsoBasicoThreadAbort.cs

Ejecución assembly:


  1. .\UsoBasicoThreadAbort.exe

> Prueba de ejecución (Ideone):

> Prueba de ejecución (local):
Prueba ejecución UsoBasicoThreadAbort.exe
Figura 2. Prueba ejecución UsoBasicoThreadAbort.exe.

3.1.2.2 Detener un thread en ciclo infinito


Este segundo ejemplo demuestra cómo el método Thread.Abort() es capaz de detener la ejecución de un ciclo infinito contenido en un thread.


Archivo C# UsoBasicoThreadAbort2.cs [enlace alternativo]:

El thread creado en la línea 10 declara un delegado compatible con el delegado ThreadStart y se encapsula un método anónimo para declara un ciclo infinito while. El thread nuevoThread se inicia en la línea 21. El thread Main se detiene (pausa) durante 2 segundos (línea 25). El thread nuevoThread se aborta (línea 28).

Compilación:


  1. csc /target:exe UsoBasicoThreadAbort2.cs

Ejecución assembly:


  1. .\UsoBasicoThreadAbort2.exe

Prueba de ejecución (Ideone):

> Prueba de ejecución (local):
Prueba ejecución assembly UsoBasicoThreadAbort2.exe
Figura 2. Prueba ejecución assembly UsoBasicoThreadAbort2.exe.

3.1.2.3 Verificación estados de un thread


Este ejemplo demuestra los diferentes estados que pueden presentarse antes de ejecutar o abortar un thread.


Archivo C# VerificacionEstadoThread.cs [enlace alternativo]:



La propiedad Thread.ThreadState [8] contiene el estado actual del thread. Esta propiedad la utilizamos para verificar el estado del thread nuevoThread en las líneas 22, 29, 35, y 41. Con este ejemplo, repito, verificamos el estado de un thread antes de iniciarlo (línea 25) y abortarlo (línea 32). Vale mencionar que en la línea 39 se intenta poner en espera al thread Main para dar finalizar la ejecución del thread nuevoThread, pero esté ya se encuentra abortado (independiente de su estado, no se generara ningún problema o error en el flujo de ejecución).


Compilación:


  1. csc /target:exe VerificacionEstadoThread.cs

Ejecución assembly:


  1. .\VerificacionEstadoThread.exe

Prueba de ejecución (Ideone):

> Prueba de ejecución (local):
Prueba ejecución assembly VerificacionEstadoThread.exe
Figura 3. Prueba ejecución assembly VerificacionEstadoThread.exe.

3.1.2.4 Inmortalidad thread


Este ejemplo demuestra cómo un thread puede recuperarse después de múltiples intentos de abortar su ejecución.


Archivo C# InmortalidadThread.cs [enlace alternativo]:



Creamos un thread nuevo en la línea 10nuevoThread. Este thread tiene encapsulado el método EjecutarTarea. Este método cuenta con un ciclo infinito while (líneas 29-43) integrado por una sentencia try-catch (líneas 31-40) para tratar el lanzamiento de la excepción ThreadAbortException. Sin embargo, en la línea 39 se invoca el método Thread.ResetAbort [9] para cancelar la solicitud del método Abort. Lo anterior provoca que todos los intentos de abortar la ejecución del thread nuevoThread fracasen.


Compilación:


  1. csc /target:exe InmortalidadThread.cs

Ejecución assembly:


  1. .\InmortalidadThread.exe

Prueba de ejecución (Ideone):

> Prueba de ejecución (local):
Prueba ejecución assembly InmortalidadThread.exe
Figura 4. Prueba ejecución assembly InmortalidadThread.exe.

[Nota: En la Figura 5 muestra que el thread nuevoThread sigue en ejecución después de los intentos de realizar la acción aborto.]

3.2 Versión Abort(Object)

La segunda versión de Abort posee un parámetro que sirve como contenedor de información de estado que puede ser usada para el thread sobre el que se intenta abortar su ejecución. Esta es su firma [11]:

public void Abort(Object stateInfo)

Descripción puntual:
  • Parámetros:
    • Object stateInfo: información de estado para el thread a ser abortado.
Ejemplo de uso:

En la línea 12 creamos un nuevo thread. Lo iniciamos en la línea 13. Esperamos un segundo (línea 15) para permitir que el método EjecutarTarea genere un mensaje en la salida estándar. En la línea 19 abortamos la ejecución del thread threadNuevo, y le pasamos como información la cadena de caracteres Datos desde el thread Main. para indiciar desde donde se invocó el método Abort(Object). Con la línea 22 ponemos en espera al thread Main para que el thread threadNuevo termine su ejecución y pueda mostrar el mensaje que se genera al atrapar la excepción ThreadAbortException en la línea 40.

Compilación:


  1. csc /target:exe UsoAbortObject.cs

Ejecución assembly:


  1. .\UsoAbortObject.exe

Prueba de ejecución (Ideone):

> Prueba de ejecución (local):
Prueba ejecución assembly UsoAbortObject.exe
Figura 5. Prueba ejecución assembly UsoAbortObject.exe.

4. Práctica: Código C#

Refresquemos los conocimientos conceptuales y empíricos que adquirimos en la sección anterior.

Compilación:


  1. csc /target:exe ImpresorNumeros.cs

Ejecución assembly:


  1. .\ImpresorNumeros.cs

Prueba de ejecución (Ideone):

> Prueba de ejecución (local):
Prueba ejecución assembly ImpresorNumeros.exe
Figura 6. Prueba ejecución assembly ImpresorNumeros.exe.

5. Conclusiones

El uso de las versiones sobrecargadas del método Abort facilitan abortar un thread que se encuentre en ejecución; sin embargo, es importante tener en cuenta que este proceso no se detiene sin generar efectos secundarios, debido a la generación de la excepción ThreadAbortException. A este caso debemos prestarle bastante atención, en particular, cuando intentemos capturar y tratar la excepción generada.En futuras entregas hablaré con más detalle acerca de esta excepción.

6. Glosario

  • Abort
  • Abortar
  • Multithreading
  • OS
  • Sistema Operativo
  • Thread

Literatura & Enlaces

[1]: Multithreading in C# 5.0 Cookbook by Eugene Agafonov. Copyright 2013 Eugene Agafonov, 978-1-84969-764-4.
[2]: Thread.Abort Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.thread.abort(v=vs.110).aspx
[3]: How To Stop a Thread in .NET (and Why Thread.Abort is Evil) - You've Been Haacked - http://haacked.com/archive/2004/11/12/how-to-stop-a-thread.aspx/
[4]: WindowsDevCenter.com - http://www.windowsdevcenter.com/pub/a/dotnet/2003/02/18/threadabort.html
[5]: Abort a Thread in C# - http://www.dotnetheaven.com/article/abort-a-thread-in-csharp
[6]: Threading in C# - Part 4 - Advanced Threading - http://www.albahari.com/threading/part4.aspx
[7]: ThreadAbortException Class (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.threadabortexception(v=vs.110).aspx
[8]: Thread.ThreadState Property (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.thread.threadstate(v=vs.110).aspx
[9]: Thread.ResetAbort Method (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.thread.resetabort(v=vs.110).aspx
[10]: Thread.Abort Method (System.Threading) - http://msdn.microsoft.com/en-us/library/ty8d3wta(v=vs.110).aspx
[11]: Thread.Abort Method (Object) (System.Threading) - http://msdn.microsoft.com/en-us/library/5b50fdsz(v=vs.110).aspx


J

jueves, 26 de junio de 2014

Receta Multithreading No. 1-3 en C#: Poner en Espera un Thread

Tabla de Contenido

0. Introducción
1. Problema
2. Solución
3. Discusión de la Solución
3.1 Firma Join
3.2 Firma Join(Int32)
3.3 Firma Join(TimeSpan)
4. Práctica: Código C#
5. Conclusiones
6. Glosario
7. Literatura & Enlaces

0. Introducción

En la receta multithreading previa (Pausar un Thread) conocimos el método Sleep para pausar un thread durante una cantidad de tiempo (en milisegundos) determinada; sin embargo para los casos que se desconoce el tiempo de espera (debido a la disponibilidad de recursos de máquina, por ejemplo) se requiere el uso de una alternativa más apropiada para los tiempos de espera variables. Veremos que la clase Thread provee el método Join para satisfacer esta necesidad, de distintas maneras.

1. Problema

Se requiere crear un programa capaz de realizar cálculos de manera sincrónica, es decir, hasta no finalizar una tarea, el resto de tareas pendientes por ejecutar quedarán en espera.

2. Solución

La clase Thread (System.Threading) está compuesta por el método Join. Este método permite poner en espera el thread que realiza la invocación durante el tiempo que tarde en ejecutarse la instancia (tipo Thread) sobre la que se invoca. Este método provee la siguiente lista de métodos sobrecargados:
  • Join
  • Join(Int32), y 
  • Join(TimeSpan)
Exploraremos cada una de estas firmas en la sección 3.

3. Discusión de la Solución


A continuación describiré las tres firmas sobrecargadas del método Join pertenecientes a la clase Thread.
Versiones sobrecargadas del método Thread.Join
Figura 1. Versiones sobrecargadas del método Thread.Join.

3.1 Firma Join

La versión sobrecargada más sencilla de Join posee la siguiente firma [3]:

public void Join()

La tarea de Join es bloquear el método desde donde se invoca hasta que la instancia de Thread haya terminado de ejecutar el método encapsulado por el delegado ThreadStart [4]. Esta forma de funcionamiento permite implementar un mecanismo de sincronización para el acceso a recursos compartidos (e.g., archivo, área de memoria).

Ejemplo de uso:

En la línea 9 definimos tres variables de tipo Thread. En la línea 15 creamos la instancia Thread thread1 y encapsulamos implícitamente el método ProcesoThread en una instancia del delegado ThreadStart. De forma análoga procedemos para la variable thread2. En el método ProcesoThread, sobre la línea 31 mostramos el nombre del thread que entra en ejecución. Con la sentencia if (líneas 33 y 34) validamos que el nombre del thread actual corresponda con el nombre Thread1 y además que la instancia thread2 esté en ejecución (thread2.ThreadState != ThreadState.Unstarted).

Compilación:


  1. csc /target:exe UsoThreadJoin.cs

Ejecución assembly:


  1. .\EnsambladorRegex.exe

Prueba de ejecución (ideone).

Prueba de ejecución (local):
Uso del método Join.
Figura 2. Uso del método Join.

3.2 Firma Join(Int32)

A diferencia de la firma Join(), con Join(Int32) [5] además de bloquear el thread que invoca a este método, podemos especificar un límite de tiempo en el que puede estar en espera el método que le invoca. Aquí está su firma:

public bool Join(int millisecondsTimeout)

Descripción puntual:
  • Parámetros:
    • int millisecondsTimeout: número de milisegundos de espera.
  • Valor de retorno:
    • bool: true si el thread terminó de ejecutar el método encapsulado por el delegado ThreadStart. false si el thread no ha terminado de ejecutarse en el tiempo especificado por el parámetro millisecondsTimeout.
Esta forma de funcionamiento permite implementar un mecanismo de sincronización para el acceso a recursos compartidos (e.g., archivo, área de memoria).

Ejemplo de uso:

En la línea 9 definimos tres variables de tipo Thread. En la línea 15 creamos la instancia Thread thread1 y encapsulamos implícitamente el método ProcesoThread en una instancia del delegado ThreadStart. De forma análoga procedemos para la variable thread2. En el método ProcesoThread, sobre la línea 31 mostramos el nombre del thread que entra en ejecución. Con la sentencia if (líneas 33 y 34) validamos que el nombre del thread actual corresponda con el nombre Thread1 y además que la instancia thread2 esté en ejecución (thread2.ThreadState != ThreadState.Unstarted).


Continuando, con la sentencia if (línea 37) invocamos a Join especificando 2 segundos de espera mientras que el thread thread2 termina su ejecución. En caso de que termine antes de los 2 segundos, se mostrará el mensaje de la línea 39, de lo contrario se mostrara el mensaje de la línea 43.


Compilación:


  1. csc /target:exe UsoThreadJoinInt32.cs

Ejecución assembly:


  1. .\UsoThreadJoinInt32.exe

Prueba de ejecución (ideone).

Prueba de ejecución (local):
Uso del método Join(Int32).
Figura 3. Uso del método Join(Int32).

3.3 Firma Join(TimeSpan)

Con Join(TimeSpan) podemos especificar el tiempo de espera de ejecución del thread sobre el que se invoca este método como una instancia de la estructura TimeSpan [6]. Firma:

public bool Join(TimeSpan timeout)

Descripción puntual:
  • Parámetros:
    • TimeSpan timeout: cantidad de tiempo de espera para el thread sobre el que se invoca el método Join termina.
  • Valor de retorno:
    • true si el thread terminó de ejecutar el método encapsulado por el delegado ThreadStartfalse si el thread no ha terminado de ejecutarse en el tiempo especificado por el parámetro timeout.
Ejemplo de uso:

Archivo C# UsoThreadJoinTimeSpan.cs {enlace alternativo}:

En la línea 10 creamos una instancia de la estructura TimeSpan para representar un segundo. Creamos un instancia de Thread en la línea 16, e invocamos el método Start (línea 17) para iniciar inmediatamente el thread. Con la sentencia if (línea 20) validamos que la invocación de Join sobre nuevoThread tarde máximo 2 segundos (tiempoEspera + tiempoEspera).

Compilación:


  1. csc /target:exe UsoThreadJoinTimeSpan.cs

Ejecución assembly:


  1. .\UsoThreadJoinTimeSpan.exe

Prueba de ejecución (ideone).

Prueba de ejecución (local):
Uso del método Join(TimeSpan).
Figura 4. Uso del método Join(TimeSpan).

4. Práctica: Código C#

Afiancemos el conocimiento que adquirimos en la sección anterior con un ejemplo en el que esperamos la impresión de una serie de números (simula un calculo largo) mientras el thread Main espera.

Compilación:


  1. csc /target:exe ImpresorNumeros.cs

Ejecución assembly:


  1. .\ImpresorNumeros.exe

Prueba de ejecución (ideone).

Prueba de ejecución (local):
Prueba ejecución assembly ImpresorNumeros.exe
Figura 5. Prueba ejecución assembly ImpresorNumeros.exe.

4. Conclusiones

Demostré el proceso de colocación en espera de un thread a través del método Join, Join(Int32), o Join(TimeSpan) de la clase System.Threading.Thread. Este enfoque nos permite aplicar el mecanismo de sincronización de acceso a recursos compartidos (e.g., área de memoria, escritura sobra un archivo en disco). Para la próxima receta multithreading conoceremos el proceso necesario para abortar o cancelar un thread.

Glosario

  • Intervalo de espera
  • Multithreading
  • Recurso compartido
  • Sincronización
  • Thread

Literatura & Enlaces

[1]: Multithreading in C# 5.0 Cookbook by Eugene Agafonov. Copyright 2013 Eugene Agafonov, 978-1-84969-764-4.
[2]: Receta Multithreading No. 1-2 en C#: Pausar un Thread | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/06/receta-multithreading-no-1-2-en-csharp-pausar-un-thread.html
[3]: Thread.Join Method (System.Threading) - http://msdn.microsoft.com/en-us/library/95hbf2ta(v=vs.110).aspx
[4]: ThreadStart Delegate (System.Threading) - http://msdn.microsoft.com/en-us/library/system.threading.threadstart(v=vs.110).aspx
[5]: Thread.Join Method (Int32) (System.Threading) - http://msdn.microsoft.com/en-us/library/6b1kkss0(v=vs.110).aspx
[6]: TimeSpan Structure (System) - http://msdn.microsoft.com/en-us/library/System.TimeSpan(v=vs.110).aspx
[7]: Thread.Join Method (TimeSpan) (System.Threading) - http://msdn.microsoft.com/en-us/library/23f7b1ct(v=vs.110).aspx


S