Search code examples
c#performance.net-6.0channelsystem.threading.channels

Which is the fastest way to tell if a Channel<T> is empty?


I am consuming a Channel<object> in an await foreach loop, and on each iteration I want to know if the channel is empty. Channels don't have an IsEmpty property, so I am seeing two ways to get this information: the Count property and the TryPeek method:

await foreach (var item in channel.Reader.ReadAllAsync())
{
    ProcessItem(item);
    if (channel.Reader.Count == 0) DoSomething();
}
await foreach (var item in channel.Reader.ReadAllAsync())
{
    ProcessItem(item);
    if (!channel.Reader.TryPeek(out _)) DoSomething();
}

My question is: Which is the most performant way to get the IsEmpty information? Is it one of the above, or is it something else?

I should mention that my channel is unbounded, but I am open to switching to a bounded channel (Channel.CreateBounded<object>(Int32.MaxValue)) in case this would be beneficial performance-wise.

My unbounded channel is configured with the SingleReader option equal to false (the default value).

Another detail that might be important: most of the time the check for emptiness will be negative. The producer of the channel tends to write hundreds of items in frequent bursts. The DoSomething method triggers such a burst.


Solution

  • I measured the performance of both approaches, with both unbounded and bounded channels, using a home-made benchmark. Essentially I filled a channel with 1,000 elements, and retrieved the IsEmpty information 20,000,000 times in a loop. Here are the results:

    Platform: .NET 6.0.0-rtm.21522.10
    Unbounded.Reader.Count: 1,000
    Bounded.Reader.Count:   1,000
    LoopsCount:             20,000,000
    
    Unbounded-Count    Duration: 706 msec
    Unbounded-TryPeek  Duration: 360 msec
    Bounded-Count      Duration: 470 msec
    Bounded-TryPeek    Duration: 506 msec
    

    Online demo.

    So it seems that for my unbounded channel the .TryPeek(out _) is the faster approach. There is no need to switch to a bounded channel. The performance is pretty good regardless though.

    It should be noted that in my particular use case, it is possible to obtain the IsEmpty information essentially for free, by switching from the await foreach loop to a nested while loop like this:

    while (await channel.Reader.WaitToReadAsync())
    {
        while (channel.Reader.TryRead(out var item))
        {
            ProcessItem(item);
        }
        DoSomething();
    }
    

    Each time the inner while loop completes, the channel is temporarily empty.

    The ChannelReader<T>.ReadAllAsync method is implemented internally with a similar nested while loop. So I shouldn't lose anything by replicating the same pattern.