Search code examples
c#performance.net-coretask-parallel-librarysystem.threading.channels

Allocation-free timeout on Task


I'd like to add a timeout to ChannelReader.ReadyAsync. Here are two solutions I've found:

var cts = new CancellationTokenSource();
cts.CancelAfter(2000);
try {
  var data = chan.ReadAsync(cts.Token);
} catch (OperationCanceledException) {
  // timeout
}
var tasks = new Task[] { Task.Delay(2000), chan.ReadAsync(CancellationToken.None) };
var completedTask = await Task.WhenAny(tasks);
if (completedTask == tasks[0])
  // timeout
else
  var data = ((T)completedTask).Result;

However, both these solutions aren't allocation-free. The first allocates a CancellationTokenSource and the second one a Timer in the Task.Delay. Is there a way to make a similar code without any allocation?

EDIT 1: dotTrace output when using the first solution Call Tree Allocations type


Solution

  • Thanks for your answers, they made me think again of exactly I was looking for: reuse CancellationTokenSource. Once a CancellationTokenSource is cancelled, you can't reuse it. But in my case, ChannelReader.ReadAsync would most of the time return before the timeout triggers so I've used the fact that CancelAfter doesn't recreate a timer the second time you call it to avoid cancelling the CancellationTokenSource after ChannelReader.ReadAsync returns.

    var timeoutCancellation = new CancellationTokenSource();
    
    while (true)
    {
        if (timeoutCancellation.IsCancellationRequested)
        {
            timeoutCancellation.Dispose();
            timeoutCancellation = new CancellationTokenSource();
        }
    
        T data;
        try
        {
            timeoutCancellation.CancelAfter(2000);
            data = await _queue.Reader.ReadAsync(timeoutCancellation.Token);
            // make sure it doesn't get cancelled so it can be reused in the next iteration
            // Timeout.Infinite won't work because it would delete the underlying timer
            timeoutCancellation.CancelAfter(int.MaxValue);
        }
        catch (OperationCanceledException) // timeout reached
        {
            // handle timeout
            continue;
        }
    
        // process data
    }
    

    This is not allocation-free but it reduces drastically the number of allocated objects.

    EDIT 1: In .NET 6 you can also use CancellationTokenSource.TryReset to reuse a CancellationTokenSource.