The ChannelReader<T>
class has a ReadAllAsync
method that exposes the data of the reader as an IAsyncEnumerable<T>
. Below is an overload of this method that accepts also a timeout
parameter. This parameter has the effect that in case the reader fails to emit any items during the specified span of time, a TimeoutException
is thrown.
For reducing the allocations it uses the same clever technique from Greg's answer, with a single CancellationTokenSource
that is rescheduled for cancellation after each iteration. After some thought I removed the line CancelAfter(int.MaxValue)
because it's probably more harmful than useful in the general case, but I might be wrong.
public static async IAsyncEnumerable<TSource> ReadAllAsync<TSource>(
this ChannelReader<TSource> source, TimeSpan timeout,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
while (true)
{
using var cts = CancellationTokenSource
.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeout);
while (true)
{
try
{
if (!await source.WaitToReadAsync(cts.Token).ConfigureAwait(false))
yield break;
}
catch (OperationCanceledException)
{
cancellationToken.ThrowIfCancellationRequested();
throw new TimeoutException();
}
while (source.TryRead(out var item))
{
yield return item;
cancellationToken.ThrowIfCancellationRequested();
}
cts.CancelAfter(timeout);
// It is possible that the CTS timed-out during the yielding
if (cts.IsCancellationRequested) break; // Start a new loop with a new CTS
}
}
}
As a side note, the System.Interactive.Async package includes a Timeout
operator with the signature shown below, that could be used in combination with the built-in ReadAllAsync
, and provide the same functionality with the above implementation. That method is not optimized for low allocations though.
public static IAsyncEnumerable<TSource> Timeout<TSource>(
this IAsyncEnumerable<TSource> source, TimeSpan timeout);
Note: In retrospect the ReadAllAsync().Timeout()
idea is dangerous, because the ReadAllAsync
is a consuming method. In other words enumerating it has the side-effect of removing items from the channel. The Timeout
operator is unaware of what's happening internally in the source sequence, so a timeout occurring at an unfortunate moment could cause an item to be lost. This leaves the implementation on the top as the only robust solution to the problem (in the scope of this answer).