5

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?

Matt Thomas
  • 5,279
  • 4
  • 27
  • 59
  • Note that Console.OpenStandardInput is returning an instance of [__ConsoleStream](https://referencesource.microsoft.com/#mscorlib/system/io/__consolestream.cs,de9f3a925342686c), which does not override .ReadAsync – Matt Thomas Feb 17 '17 at 19:27
  • 1
    Next time before you do some unnecessary magic to cancel the token after an amount of time use the proper constructor: `var cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));`. https://msdn.microsoft.com/en-us/library/hh139229(v=vs.110).aspx ` – Peter Bons Feb 17 '17 at 19:30
  • @PeterBons To me, my way more readably communicates that something will be printed out to the console after – Matt Thomas Feb 17 '17 at 19:31
  • Your way is more readable, are you sure? what about the `cancellationToken.CancelAfter(TimeSpan.FromSeconds(2));` would that be better? now you are making thing more unclear with a custom task delay and task.run – Peter Bons Feb 17 '17 at 19:36
  • @PeterBons May we focus on the question? Sure I could .CancelAfter and .Register(() => Console.WriteLine("...")), but it doesn't help with canceling the async operation – Matt Thomas Feb 17 '17 at 19:37
  • I imagine you can't because doing so could leave the stream in an inconsistent state. –  Feb 17 '17 at 19:46
  • @Amy, could you explain that a little more? – Matt Thomas Feb 17 '17 at 19:53
  • When you read from a stream, the read needs to copy bytes into a buffer, advance the current position, perform any relevant conversions or whatever, etc. Interrupting that could, for instance, copy bytes into the buffer but fail to advance the stream, or fail to do whatever else. If you do the read and attempt to abort it, what happens if the stream can't be returned to its original position? That's what I mean by an inconsistent state. It simplifies the stream code to simply not allow that. –  Feb 17 '17 at 20:18
  • 2
    @Amy That makes sense, but the ReadAsync method _does_ accept a CancellationToken. And it's not a difficult problem to solve. For example, the copy and advance operations could be treated atomically, and the thing being waited upon--a byte to be ready for copying--is what could be aborted with no ill consequence. Indeed, this is how people generally deal with cancellation tokens in long-executing code: they react to cancellations at strategic locations where there's no chance of leaving stuff in an untidy mess – Matt Thomas Feb 17 '17 at 20:30
  • Maybe its not difficult, but there might be other reasons for not making such changes. For instance, backwards compatibility, or it was deemed not worth the cost, or whatever. There are numerous possible reasons Microsoft didn't modify the streams. Maybe they were afraid of crossing the streams? Streams have support from the underlying OS. Maybe changing *that* isn't worth the benefits? Who knows. –  Feb 17 '17 at 22:50
  • Any movement on this? I'm finding the same issue. It makes me think that cancellation tokens are a kind of afterthought in some APIs and each each needs attention to make sure it is correctly honoring the cancellation token. That means that swatches of the .net ecosystem pay no attention to tokens. – Christian Findlay Jan 22 '21 at 21:53
  • 1
    @ChristianFindlay That's what I think, too. Unfortunately this isn't the only scar .Net bears from its long history. In this case I'm not too hard on them though because async-await didn't even exist when .Net came out – Matt Thomas Jan 25 '21 at 18:51
  • 1
    @MattThomas that may be true, but you think that they'd revisit this in .NET Core and .NET 5... – Christian Findlay Jan 25 '21 at 21:13

2 Answers2

3

Matt, I had the same problem. I could not find a way to cancel it.

If you use Port.BaseStream.ReadAsync() and if fails to read the required number of bytes, then the only way I found to be able to read the port again using Port.BaseStream.ReadAsync() after this event is to call Port.DiscardInBuffer() after Port.BaseStream.ReadAsync() failed to read the required number of bytes.

Then you can write to the port again to request the data you want to read again and read it correctly using Port.Basestream.ReadAsync() again assuming the correct number of bytes are available to be read.

I used this successfully it implement a Retry function.

The other way that could work would be to check if there are bytes to available read by checking if Port.BytesToRead == 0 and if this is true then do not call Port.BaseStream.ReadAsync().

Lou
  • 4,244
  • 3
  • 33
  • 72
BJE
  • 31
  • 3
1

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.

Matt Thomas
  • 5,279
  • 4
  • 27
  • 59