Imagina que estamos comparando un autobús escolar (ConcurrentQueue) contra un teleférico de alta velocidad (Disruptor). El autobús es flexible, pero tiene que parar, abrir puertas y esperar a que todos se sienten. El teleférico simplemente no para: tú te enganchas en movimiento.

1. Preparando los contendientes
Primero, instalamos el paquete: dotnet add package BenchmarkDotNet.
Aquí tienes el código del experimento. Vamos a medir cuánto tiempo tarda cada estructura en procesar 1 millón de eventos.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Concurrent;
using Disruptor;
using Disruptor.Dsl;
[MemoryDiagnoser] // Para ver quién gasta más en "bolsas de basura" (GC)
public class LatencyBenchmark
{
private const int BufferSize = 1024 * 64; // Potencia de 2, siempre.
private const int ItemsCount = 1_000_000;
private Disruptor<PriceEvent> _disruptor;
private RingBuffer<PriceEvent> _ringBuffer;
private ConcurrentQueue<PriceEvent> _queue;
[GlobalSetup]
public void Setup()
{
// Setup Disruptor
_disruptor = new Disruptor<PriceEvent>(() => new PriceEvent(), BufferSize, TaskScheduler.Default);
_disruptor.HandleEventsWith(new NoOpHandler());
_ringBuffer = _disruptor.Start();
// Setup Queue
_queue = new ConcurrentQueue<PriceEvent>();
}
[Benchmark]
public void TestConcurrentQueue()
{
for (int i = 0; i < ItemsCount; i++)
{
_queue.Enqueue(new PriceEvent { Price = i });
}
// Simulamos el consumo (simplificado para el bench)
while (_queue.TryDequeue(out _)) { }
}
[Benchmark]
public void TestDisruptor()
{
for (int i = 0; i < ItemsCount; i++)
{
long sequence = _ringBuffer.Next();
var data = _ringBuffer[sequence];
data.Price = i;
_ringBuffer.Publish(sequence);
}
}
}
public class PriceEvent { public double Price { get; set; } }
public class NoOpHandler : IEventHandler<PriceEvent> {
public void OnEvent(PriceEvent data, long sequence, bool endOfBatch) { /* No hacemos nada, solo procesar */ }
}
¿Qué deberías observar?
Cuando ejecutes esto con dotnet run -c Release, prepárate para ver algo parecido a esto en tu terminal:
| Method | Mean (Tiempo) | Gen 0 (Basura) | Allocated (Memoria) |
| ConcurrentQueue | ~45.5 ms | 1500.00 | 24 MB |
| Disruptor | ~8.2 ms | 0.00 | N/A (Zero!) |
El Análisis Sincericida:
- La Masacre de la Memoria: La ConcurrentQueue crea un objeto por cada Enqueue. Eso significa que el Garbage Collector tiene que trabajar horas extras. El Disruptor tiene cero asignaciones durante el bucle. Es memoria plana y estática.
- Contención de Hilos: La cola usa mecanismos de sincronización internos que, aunque eficientes, causan fricción. El Disruptor usa Memory Barriers y Sequence Tracking, lo que permite que el productor y el consumidor casi nunca se toquen el hombro.
El Consejo Pro: No midas en «Debug»
Si ejecutas este benchmark en modo Debug, los resultados serán basura. El compilador de .NET (RyuJIT) hace trucos de magia en modo Release (como inlining de métodos) que son vitales para que el Disruptor brille. Es como probar un motor de carreras con el freno de mano puesto.
Conclusión Final con Reto Maestro
Ahora tienes los datos. Ya no eres un programador que «cree» que su código es rápido; eres un ingeniero que sabe cuánto tarda.
Reto (The Final Boss): Intenta cambiar la WaitStrategy en el benchmark a BusySpinWaitStrategy y observa cómo el tiempo de ejecución baja aún más, pero tu CPU empieza a pedir clemencia. ¿Te atreves a probarlo en un entorno con 8 productores simultáneos? Ahí es donde verás a la ConcurrentQueue llorar de verdad.
