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.
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
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.