Implementación de un Marco de Trabajo para Concurrencia en C#

La programación multihilo en C# ofrece herramientas poderosas para mejorar la reactividad y el rendimiento de las aplicaciones. Sin embargo, su implementación directa puede introducir complejidades relacionadas con la sincronización, la gestión del ciclo de vida de los hilos y el manejo de excepciones, lo que a menudo lleva a errores difíciles de depurar. Para mitigar estas dificultades, es beneficioso encapsular las operaciones de bajo nivel en un marco de trabajo que ofrezca una interfaz más simple y controlada.

El siguiente diseño propone un enfoque modular, donde una clase base maneja los detalles operativos del hilo, permitiendo a las clases derivadas concentrarse exclusivamente en la lógica de negocio a ejecutar. Esto simplifica el desarrollo y mejora la robustez del sistema, especialmente en escenarios donde la cancelación cooperativa es clave.

Clase Base para Tareas Asíncronas

La clase TareaAsincronaBase es el núcleo de este marco. Define la estructura para cualquier operación que deba ejecutarse en un hilo separado. Incorpora mecanismos para iniciar, detener y monitorear el estado de la tarea, utilizando el patrón de cancelación cooperativa de .NET a través de CancellationTokenSource y CancellationToken, lo que es una práctica recomendada frente a la deprecated Thread.Abort().


using System;
using System.Threading;
using System.Diagnostics; // Para fines de depuración, similar a Trace.WriteLine

namespace SistemaHilos
{
   public abstract class TareaAsincronaBase : IDisposable
   {
       public enum EstadoTarea
       {
           CREADA,
           EN_EJECUCION,
           FINALIZADA,
           CANCELADA,
           FALLIDA
       }

       private int _idHiloGestionado;
       private Thread _hiloInterno;
       private EstadoTarea _estadoActual;
       private CancellationTokenSource _fuenteCancelacion;

       protected TareaAsincronaBase()
       {
           _estadoActual = EstadoTarea.CREADA;
           _fuenteCancelacion = new CancellationTokenSource();
           _hiloInterno = new Thread(EjecutarPuntoEntrada);
           _hiloInterno.IsBackground = true; // Permite que la aplicación termine si solo quedan hilos de fondo
       }

       /// <summary>
       /// Método de punto de entrada para el hilo de trabajo.
       /// Maneja el ciclo de vida, excepciones y estados del hilo.
       /// </summary>
       private void EjecutarPuntoEntrada()
       {
           try
           {
               Console.WriteLine($"[TareaAsincronaBase] Iniciando tarea en hilo {Thread.CurrentThread.ManagedThreadId}");
               _estadoActual = EstadoTarea.EN_EJECUCION;
               Ejecutar(_fuenteCancelacion.Token); // Llamada al método abstracto de la subclase
               _estadoActual = EstadoTarea.FINALIZADA;
               Console.WriteLine($"[TareaAsincronaBase] Tarea en hilo {Thread.CurrentThread.ManagedThreadId} finalizada con éxito.");
           }
           catch (OperationCanceledException)
           {
               _estadoActual = EstadoTarea.CANCELADA;
               Console.WriteLine($"[TareaAsincronaBase] Tarea en hilo {Thread.CurrentThread.ManagedThreadId} ha sido cancelada.");
           }
           catch (Exception ex)
           {
               _estadoActual = EstadoTarea.FALLIDA;
               Console.Error.WriteLine($"[TareaAsincronaBase] Error inesperado en hilo {Thread.CurrentThread.ManagedThreadId}: {ex.Message}");
               Debug.WriteLine($"[TareaAsincronaBase] Excepción completa: {ex}");
           }
           finally
           {
               // Asegurarse de que el estado refleje el fin de la ejecución
               if (_estadoActual == EstadoTarea.EN_EJECUCION)
               {
                   _estadoActual = EstadoTarea.FINALIZADA;
               }
           }
       }

       /// <summary>
       /// Inicia la ejecución de la tarea en un hilo nuevo.
       /// </summary>
       /// <returns>0 si se inicia con éxito, -1 en caso de error.</returns>
       public int Iniciar()
       {
           if (_hiloInterno == null || _hiloInterno.IsAlive)
           {
               Console.WriteLine("[TareaAsincronaBase] El hilo ya está corriendo o no se ha inicializado correctamente.");
               return -1;
           }

           try
           {
               _hiloInterno.Start();
               _idHiloGestionado = _hiloInterno.ManagedThreadId;
               _estadoActual = EstadoTarea.EN_EJECUCION;
               Console.WriteLine($"[TareaAsincronaBase] Hilo {_idHiloGestionado} iniciado.");
               return 0;
           }
           catch (ThreadStateException ex)
           {
               Console.Error.WriteLine($"[TareaAsincronaBase] Error al iniciar el hilo: {ex.Message}");
               _estadoActual = EstadoTarea.FALLIDA;
               return -1;
           }
       }

       /// <summary>
       /// Solicita la detención cooperativa de la tarea y espera su finalización.
       /// </summary>
       /// <param name="tiempoEsperaMs">Tiempo en milisegundos para esperar la finalización del hilo.</param>
       /// <returns>0 si se detiene con éxito, -1 si falla o excede el tiempo de espera.</returns>
       public int Detener(int tiempoEsperaMs = 3000)
       {
           if (_hiloInterno == null || !_hiloInterno.IsAlive)
           {
               Console.WriteLine("[TareaAsincronaBase] El hilo no está activo para detener.");
               _estadoActual = EstadoTarea.FINALIZADA; // O CANCELADA si se estaba esperando
               return 0;
           }

           Console.WriteLine($"[TareaAsincronaBase] Solicitando cancelación del hilo {_idHiloGestionado}.");
           _fuenteCancelacion.Cancel(); // Solicita la cancelación cooperativa

           if (_hiloInterno.Join(tiempoEsperaMs))
           {
               Console.WriteLine($"[TareaAsincronaBase] Hilo {_idHiloGestionado} detenido cooperativamente.");
               return 0;
           }
           else
           {
               Console.Error.WriteLine($"[TareaAsincronaBase] El hilo {_idHiloGestionado} no se detuvo en {tiempoEsperaMs}ms.");
               // En un sistema robusto, aquí se podría registrar el error y tomar acciones,
               // pero no se forzaría un Abort() por ser obsoleto y problemático.
               _estadoActual = EstadoTarea.FALLIDA; // No se detuvo correctamente
               return -1;
           }
       }

       /// <summary>
       /// Método abstracto que las clases derivadas deben implementar
       /// para definir la lógica de trabajo del hilo.
       /// </summary>
       /// <param name="token">Token de cancelación para la gestión cooperativa.</param>
       protected abstract void Ejecutar(CancellationToken token);

       public int IdHilo => _idHiloGestionado;
       public EstadoTarea Estado => _estadoActual;
       public bool EstaEnEjecucion => _estadoActual == EstadoTarea.EN_EJECUCION;

       // Implementación de IDisposable para liberar recursos
       public void Dispose()
       {
           Dispose(true);
           GC.SuppressFinalize(this);
       }

       protected virtual void Dispose(bool disposing)
       {
           if (disposing)
           {
               if (_fuenteCancelacion != null)
               {
                   _fuenteCancelacion.Dispose();
                   _fuenteCancelacion = null;
               }
               // Si el hilo aún está vivo, se debe manejar su detención antes de la disposición
               if (_hiloInterno != null && _hiloInterno.IsAlive)
               {
                   Detener(500); // Intento de detención rápida al disponer
               }
               _hiloInterno = null;
           }
       }

       ~TareaAsincronaBase()
       {
           Dispose(false);
       }
   }
}
   

Implementación de Tarea Concreta

La clase TareaContadora hereda de TareaAsincronaBase y proporciona una implementación concreta del método Ejecutar. Su lógica es simple: imprimir un contador creciente en la consola. Es crucial que el método Ejecutar verifique periódicamente el CancellationToken para responder a las solicitudes de cancelación de manera cooperativa.


using System;
using System.Threading;
using SistemaHilos; // Asegúrate de que el namespace de la clase base sea accesible

namespace EjemploHilos
{
   public class TareaContadora : TareaAsincronaBase
   {
       private int _contador = 0;

       public TareaContadora() { }

       /// <summary>
       /// Lógica de trabajo de la tarea: incrementa un contador y lo imprime.
       /// </summary>
       /// <param name="token">Token de cancelación para detener la ejecución cooperativamente.</param>
       protected override void Ejecutar(CancellationToken token)
       {
           Console.WriteLine("[TareaContadora] Iniciando proceso de conteo.");
           while (!token.IsCancellationRequested) // Verifica si se ha solicitado la cancelación
           {
               Console.WriteLine($"[TareaContadora] Valor actual: {_contador++}");
               Thread.Sleep(500); // Simula algún trabajo que toma tiempo

               // Opcional: lanzar una excepción de cancelación si se prefiere una salida más agresiva
               // token.ThrowIfCancellationRequested(); 
           }
           Console.WriteLine("[TareaContadora] Proceso de conteo finalizado por solicitud de cancelación.");
       }

       public int ValorActual => _contador;
   }
}
   

Programa Principal para Demostración

El programa principal (Main) orquesta el inicio y la detención de la TareaContadora. Demeustra cómo se instancia la tarea, se inicia, se le permite ejecutar por un tiempo y luego se detiene de forma controlada.


using System;
using System.Threading;
using EjemploHilos; // Asegúrate de que el namespace de la tarea concreta sea accesible

namespace ProgramaDemostracion
{
   class ProgramaPrincipal
   {
       static void Main(string[] args)
       {
           Console.WriteLine("--- Iniciando demostración de Tareas Asíncronas ---");

           using (TareaContadora miTarea = new TareaContadora())
           {
               Console.WriteLine("Creando instancia de TareaContadora.");
               miTarea.Iniciar(); // Inicia la tarea en un hilo separado

               Console.WriteLine("Esperando 5 segundos para que la tarea cuente...");
               Thread.Sleep(5000); // Espera 5 segundos

               Console.WriteLine($"Solicitando detención de la tarea. Valor actual del contador: {miTarea.ValorActual}");
               miTarea.Detener(2000); // Intenta detener la tarea, esperando hasta 2 segundos

               Console.WriteLine($"Estado final de la tarea: {miTarea.Estado}. Valor final del contador: {miTarea.ValorActual}");
           }

           Console.WriteLine("--- Demostración finalizada. Presione cualquier tecla para salir ---");
           Console.ReadKey();
       }
   }
}
   

Con esta estructura, el desarrollador que utiliza el marco solo necesita concentrarse en la implementación del método Ejecutar(CancellationToken token), delegando toda la complejidad de la gestión de hilos, su inicio, detención y manejo básico de errores a la clase base TareaAsincronaBase.

Etiquetas: C# Multithreading Concurrency CancellationToken Thread Management

Publicado el 6-11 22:57