1

BlockingCollection<T> has the handy static TakeFromAny method allowing you to consume multiple collections "I want the next item from any of these collections".

ChannelReader<T> doesn't have an equivalent so if you did want to consume multiple channels into a single stream - say to print received items to Console 1 by 1, how might this be done?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Mr. Boy
  • 60,845
  • 93
  • 320
  • 589
  • Maybe you may add items to the single `BlockingCollection` from multiple producers such as Channels and consume it with a single loop. – aepot May 20 '20 at 21:33
  • I would be seeking to use `Channel` to _replace_ `BlockingCollection` but the the same idea, of multiple `ChannelsReader`s feeding another `ChannelWriter`, which is then consumed by another `ChannelReader`, could work. It seems pretty clunky though. – Mr. Boy May 20 '20 at 21:39

1 Answers1

0

The fast path is easy, but the slow path is quite tricky. The implementation below returns a Task<ValueTuple<T, int>> that contains the value taken from one of the readers, and the zero-based index of that reader in the input array.

public static Task<(T Item, int Index)> ReadFromAnyAsync<T>(
    params ChannelReader<T>[] channelReaders) =>
    ReadFromAnyAsync(channelReaders, CancellationToken.None);

public static async Task<(T Item, int Index)> ReadFromAnyAsync<T>(
    ChannelReader<T>[] channelReaders,
    CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    // Fast path
    for (int i = 0; i < channelReaders.Length; i++)
    {
        if (channelReaders[i].TryRead(out var item)) return (item, i);
    }

    // Slow path
    var locker = new object();
    int resultIndex = -1;
    T resultItem = default;
    while (true)
    {
        using (var cts = CancellationTokenSource
            .CreateLinkedTokenSource(cancellationToken, default))
        {
            bool availableAny = false;
            Task[] tasks = channelReaders
                .Select(async (reader, index) =>
                {
                    try
                    {
                        bool available = await reader.WaitToReadAsync(cts.Token)
                            .ConfigureAwait(false);
                        if (!available) return;
                    }
                    catch // Cancellation, or channel completed with exception
                    {
                        return;
                    }
                    availableAny = true;
                    lock (locker) // Take from one reader only
                    {
                        if (resultIndex == -1 && reader.TryRead(out var item))
                        {
                            resultIndex = index;
                            resultItem = item;
                            cts.Cancel();
                        }
                    }
                })
                .ToArray();

            await Task.WhenAll(tasks).ConfigureAwait(false);

            if (resultIndex != -1) return (resultItem, resultIndex);

            cancellationToken.ThrowIfCancellationRequested();

            if (!availableAny) throw new ChannelClosedException(
                "All channels are marked as completed.");
        }
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104