I had originally planned to defer learning about how to cancel out of an asynchronous operation, but the unexpected "timeout doesn't time out" behavior of asynchronous serial port read gave me the... opportunity... to learn about cancellation now.

My first approach was hilariously clunky in hindsight: I found Task.WhenAny first, which will complete the await operation if any of the given list of Task objects completed. So I built a new Task object whose only job is to wait through a short time and complete. I packed it and the serial read operation task together into an array, and when the await operation completed I could see whether the read or the timeout Task completed first.

It seemed to work, but I was unsatisfied. I felt this must be a common enough operation that there would be other options, and I was right: digging through the documentation revealed there's a very similar-sounding Task.WaitAny which has an overload that accepts a TimeSpan as one of the parameters. This is a shorter version of what I did earlier, but I still had to pack the read operation into a Task array of a single element.

Two other overloads of Task.WaitAny accepted a CancellationToken instead, and I initially dismissed them. Creating a CancellationTokenSource is the most flexible way to give me control over when to trigger a cancellation, but I thought that was for times when I had more sophisticated logic deciding when to cancel. Spinning up a whole separate timer callback to call Cancel() felt like overkill.

Except it didn't have to be that bad: CancellationTokenSource has a constructor that accepts a count of milliseconds before canceling, so that timer mechanism was already built-in to the class. Furthermore, by using CancellationTokenSource, I still retain the flexibility of canceling earlier than the timeout if I should choose. This felt like the best choice when I only have a single Task at hand. I can reserve Task.WhenAny or Task.WaitAny for times when I have multiple Task objects to coordinate. Which is also something I hope to defer until later, as I'm having a hard enough time understanding all the nuances of a single Task in practice. Maybe some logging can help?

[This Hello3DP programming exercise is publicly available on GitHub]