0

High Speed #4: 8 Hilos vs 1 RingBuffer

En una ConcurrentQueue, cada hilo productor tiene que pelear por el puntero de «cola». En el Disruptor, los productores también tienen que coordinarse para no pisarse, pero lo hacen mediante un objeto llamado MultiProducerSequencer.

El Código del Caos (Multi-Threaded Benchmark)

Vamos a modificar nuestro benchmark para lanzar una horda de hilos contra ambas estructuras.

[Params(1, 4, 8)] // BenchmarkDotNet ejecutará el test con 1, 4 y 8 hilos
public int NumeroProductores;

[Benchmark]
public void MultiProducer_Queue()
{
    var tasks = new Task[NumeroProductores];
    for (int t = 0; t < NumeroProductores; t++)
    {
        tasks[t] = Task.Run(() => {
            for (int i = 0; i < ItemsCount / NumeroProductores; i++)
                _queue.Enqueue(new PriceEvent { Price = i });
        });
    }
    Task.WaitAll(tasks);
}

[Benchmark]
public void MultiProducer_Disruptor()
{
    var tasks = new Task[NumeroProductores];
    for (int t = 0; t < NumeroProductores; t++)
    {
        tasks[t] = Task.Run(() => {
            for (int i = 0; i < ItemsCount / NumeroProductores; i++)
            {
                long sequence = _ringBuffer.Next();
                _ringBuffer[sequence].Price = i;
                _ringBuffer.Publish(sequence);
            }
        });
    }
    Task.WaitAll(tasks);
}

Lo que vas a descubrir (La cruda realidad)

A medida que aumentas el NumeroProductores, notarás algo fascinante:

  1. ConcurrentQueue: El tiempo de ejecución no baja linealmente. De hecho, a veces con 8 hilos es más lenta que con 4 debido al «costo de coordinación» (Lock contention).
  2. Disruptor: Escala mucho mejor porque usa operaciones CAS (Compare-And-Swap) optimizadas. Sin embargo, incluso en el Disruptor, el escenario «Multi-Producer» es más lento que el «Single-Producer».

Nota: En sistemas de ultra-alta fidelidad, el diseño ideal es Single-Producer. Si puedes rediseñar tu sistema para que solo un hilo escriba en el RingBuffer (quizás usando un paso previo de agregación), habrás alcanzado el Nirvana de la programación.

El Veredicto: La «Falsa» Concurrencia

Muchos desarrolladores creen que tirar más hilos a un problema lo resuelve. Pero la memoria RAM es como un pasillo estrecho: si 8 personas intentan correr por él, se chocarán.

  • El Disruptor minimiza los choques usando Padding. Agrega «espacio vacío» alrededor de las variables críticas para asegurarse de que dos núcleos de CPU no intenten modificar la misma línea de caché al mismo tiempo (evitando el False Sharing).
  • Si ves que el rendimiento no mejora al añadir hilos, es que has llegado al límite de tu hardware, no de tu código.

Conclusión Final

Has pasado de usar una simple lista a dominar el Ring Buffer, las Wait Strategies y la Contención de Caché. Eres oficialmente el terror de los cuellos de botella.

Reto (Nivel Dios): Configura el Disruptor con ProducerType.Single y compáralo contra ProducerType.Multi. Verás que si logras que tu arquitectura sea de un solo productor, la velocidad se triplica.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *