Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cancel Stream.ReadAsync?

Why doesn't the code below ever complete if you don't type any input, and why does it still respond to a key being pressed even after the cancellation token has been canceled?

// Set up a cancellation token
var cancellationSource = new CancellationTokenSource();

// Cancel the cancellation token after a little bit of time
Task.Run(async () =>
{
    await Task.Delay(TimeSpan.FromSeconds(2));
    cancellationSource.Cancel();
    Console.WriteLine("Canceled the cancellation token");
});

// Wait for user input, or the cancellation token
Task.Run(async () =>
{
    try
    {
        using (var input = Console.OpenStandardInput())
        {
            var buffer = new byte[1];
            Console.WriteLine("Waiting for input");
            await input.ReadAsync(buffer, 0, 1, cancellationSource.Token); // This is impossible to cancel???
            Console.WriteLine("Done waiting for input"); // This never happens until you press a key, regardless of the cancellation token
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message); // No errors
    }
})
.Wait(); // Block until complete

The documentation for Stream.ReadAsync says:

If the operation is canceled before it completes, the returned task contains the Canceled value for the Status property.

This implies that canceling the cancellation token will cancel the operation, right? Yet for some reason the source code for Stream.ReadAsync doesn't do anything with the cancellation token if it isn't canceled beforehand:

public virtual Task<int> ReadAsync(Byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
    // If cancellation was requested, bail early with an already completed task.
    // Otherwise, return a task that represents the Begin/End methods.
    return cancellationToken.IsCancellationRequested
                ? Task.FromCancellation<int>(cancellationToken)
                : BeginEndReadAsync(buffer, offset, count);
}

Therefore the cancellation token parameter is pointless--how can I cancel that async read?

like image 843
Matt Thomas Avatar asked Oct 30 '22 11:10

Matt Thomas


1 Answers

In the particular case of Console input, there appears to be no other way than to poll the Console.KeyAvailable property:

var buffer = new byte[1];
Console.WriteLine("Waiting for input");

while (!Console.KeyAvailable && !cancellationSource.Token.IsCancellationRequested)
    await Task.Delay(10); // You can add the cancellation token as a second parameter here, but then canceling it will cause .Delay to throw an exception

if (cancellationSource.Token.IsCancellationRequested)
{
    Console.WriteLine("Canceled; no longer waiting for input");
}
else
{
    await input.ReadAsync(buffer, 0, 1);
    Console.WriteLine("Got user input");
}

To me, this suggests that you cannot reliably use Stream.ReadAsync in a general way, because you must do different things depending on which implementation of Stream you're dealing with.

Edit:

Thinking about this a little more, it makes sense that you can't cancel ReadAsync, because the Stream abstract class does not have any abstract methods dealing with asynchronous operations; all you must do to implement a Stream is implement some getters and some blocking methods, which is all Microsoft has done with the __ConsoleStream class.

Since the only methods that can be guaranteed to exist on a Stream are blocking methods, and since it's impossible to cancel a blocking invocation (you can't even do a blocking IO operation on another thread, cancel the thread, and have the operation stop), it's impossible to have cancelable asynchronous operations.

Therefore Microsoft either should have removed the cancellation token parameter, or should have put abstract asynchronous cancelable methods into the Stream class so that the folks who made __ConsoleStream would have been forced to implement them.

like image 161
Matt Thomas Avatar answered Nov 15 '22 05:11

Matt Thomas