La sincronización de hilos es un aspecto crítico en el desarrollo de aplicaciones multihilo. En este artículo, exploraremos tres mecanismos avanzados proporcionados por el espacio de nombres System.Threading para gestionar la ejecución concurrente y el accceso a recursos compartidos.
Coordinación de fases con la clase Barrier
La clase Barrier permite que múltiples hilos trabajen de forma cooperativa en un algoritmo dividido por fases. Todos los hilos deben alcanzar la barrera antes de que cualquiera de ellos pueda continuar con la siguiente etapa. Es ideal para cálculos iterativos complejos.
using System;
using System.Threading;
namespace SincronizacionAvanzada
{
class EjemploBarrier
{
// Se define una barrera para 2 hilos con una acción al completar cada fase
static Barrier puntoDeEncuentro = new Barrier(2, b =>
Console.WriteLine($"--- Fase {b.CurrentPhaseNumber + 1} finalizada ---"));
static void EjecutarProceso(string nombreHilo, int duracion)
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"{nombreHilo} trabajando en la etapa {i + 1}...");
Thread.Sleep(TimeSpan.FromSeconds(duracion));
Console.WriteLine($"{nombreHilo} ha llegado a la barrera.");
puntoDeEncuentro.SignalAndWait();
}
}
static void Main()
{
var hiloA = new Thread(() => EjecutarProceso("Hilo Alfa", 2));
var hiloB = new Thread(() => EjecutarProceso("Hilo Beta", 4));
hiloA.Start();
hiloB.Start();
hiloA.Join();
hiloB.Join();
Console.WriteLine("Proceso completo.");
}
}
}
En este ejemplo, el hilo más rápido esperará al más lento en cada iteración. El delegado definido en el constructor de Barrier se ejecuta una vez que todos los participantes han invocado SignalAndWait, permitiendo realizar validaciones o consolidación de datos entre fases.
Gestión de acceso concurrente con ReaderWriterLockSlim
Cuando un recurso se lee con frecuencia pero se escribe rara vez, un bloqueo convencional (como lock) puede penalizar el rendimiento. ReaderWriterLockSlim permite que múltiples hilos lean simultáneamente, manteniendo la exclusividad únicamente para las operaciones de escritura.
using System;
using System.Collections.Generic;
using System.Threading;
namespace SincronizacionAvanzada
{
class GestionRecursos
{
private static ReaderWriterLockSlim controlAcceso = new ReaderWriterLockSlim();
private static List<int> contenedorDatos = new List<int>();
static void Lector()
{
while (true)
{
controlAcceso.EnterReadLock();
try
{
foreach (var valor in contenedorDatos)
{
Console.WriteLine($"Lectura: {valor}");
Thread.Sleep(100);
}
}
finally
{
controlAcceso.ExitReadLock();
}
}
}
static void Escritor(string id)
{
while (true)
{
int nuevoDato = new Random().Next(1, 100);
controlAcceso.EnterUpgradeableReadLock();
try
{
if (!contenedorDatos.Contains(nuevoDato))
{
controlAcceso.EnterWriteLock();
try
{
contenedorDatos.Add(nuevoDato);
Console.WriteLine($"[Escritor {id}] Agregado: {nuevoDato}");
}
finally
{
controlAcceso.ExitWriteLock();
}
}
}
finally
{
controlAcceso.ExitUpgradeableReadLock();
}
Thread.Sleep(1500);
}
}
static void Main()
{
new Thread(Lector) { IsBackground = true }.Start();
new Thread(() => Escritor("A")) { IsBackground = true }.Start();
new Thread(() => Escritor("B")) { IsBackground = true }.Start();
Console.WriteLine("Presione Enter para salir...");
Console.ReadLine();
}
}
}
El uso de EnterUpgradeableReadLock es fundamental cuando necesitamos verificar una condición antes de escribir. Esto permite transicionar de un estado de lectura a uno de escritura de forma eficiente y segura, evitando interbloqueos (deadlocks) comunes.
Sincronización híbrida mediante SpinWait
SpinWait es una estructura diseñada para escenarios donde se espera que el tiempo de espera sea muy corto. En lugar de poner el hilo en estado de reposo inmediatamente (lo que implica un costoso cambio de contexto en el núcleo), realiza una espera activa ("spin") durante un breve periodo.
using System;
using System.Threading;
namespace SincronizacionAvanzada
{
class DemoSpinWait
{
private static volatile bool _listo = false;
static void EsperaHibrida()
{
var sw = new SpinWait();
Console.WriteLine("Iniciando espera híbrida...");
while (!_listo)
{
sw.SpinOnce();
// NextSpinWillYield indica si el hilo está a punto de ceder el procesador
Console.WriteLine($"¿Cederá el CPU en el siguiente ciclo?: {sw.NextSpinWillYield}");
}
Console.WriteLine("Condición cumplida.");
}
static void Main()
{
var hiloTrabajo = new Thread(EsperaHibrida);
hiloTrabajo.Start();
Thread.Sleep(50); // Simulación de carga
_listo = true;
hiloTrabajo.Join();
Console.WriteLine("Ejecución finalizada.");
}
}
}
SpinWait ajusta dinámicamente su comportamiento: inicialmente realiza ciclos de CPU y, si la condición no se cumple tras varios intentos, empieza a ceder el tiempo de procesador (Yield) o a dormir el hilo, optimizando así tanto la latencia como el consumo de recursos.