0

I am programming a console based speed reader. I have an automatic mode, where the console prints all the words — one at a time — and shows each word for a period of time set by the user. While printing the individual words I wish to continually check for a key input from the user, without blocking the printing to the console.

My method AutomaticallyReadWordsAsync prints these words asynchronously, but I have found no way to check for a key press asynchronously. The best solution I have come up with is calling the AutomaticallyReadWordsAsync method and not awaiting its results and then calling the IsSpacebarPressed method which leads to first method running asynchronously in the background while the second — synchronous — method continuously checks for user input or cancellation. After the IsSpacebarPressed method I await the task from the AutomaticallyReadWordsAsync. If one the methods completes it cancels the CancellationTokenSource, which leads to the other method completing, as it checks for the state of the token.

Here's a simplified version of my project code to describe this issue:

// See https://aka.ms/new-console-template for more information
var cancellationTokenSource = new CancellationTokenSource();

var automaticallyReadWordsTask = AutomaticallyReadWordsAsync(500, ["Hi", "I", "have", "a", "problem", "please", "help", "me."], cancellationTokenSource);
IsSpacebarPressed(cancellationTokenSource);

await automaticallyReadWordsTask;

async Task AutomaticallyReadWordsAsync(int millisecondsPerWord, string[] words, CancellationTokenSource cancellationTokenSource)
{
    CancellationToken cancellationToken = cancellationTokenSource.Token;

    int index = 0;

    while (index >= 0 && index < words.Length && !cancellationToken.IsCancellationRequested)
    {

        Console.Clear();
        Console.WriteLine(words[index++]);

        await Task.Delay(millisecondsPerWord);
    }

    if (cancellationToken.IsCancellationRequested)
        return;

    cancellationTokenSource.Cancel();
    return;
}

void IsSpacebarPressed(CancellationTokenSource cancellationTokenSource)
{
    CancellationToken cancellationToken = cancellationTokenSource.Token;

    ConsoleKeyInfo keyPressed;

    while (!cancellationToken.IsCancellationRequested)
    {
        if (Console.KeyAvailable && !cancellationToken.IsCancellationRequested)
        {
            keyPressed = Console.ReadKey();

            if (keyPressed.Key == ConsoleKey.Spacebar)
            {
                cancellationTokenSource.Cancel();
                return;
            }
        }
    }

    return;
}

Is there a better way to do this? I feel like there should be one. I don't feel like my solution is bad, but I do feel like there should be a way to do it better.

I could always include useless asynchronous methods in the IsSpacebarPressed method, but that I consider that way worse than the way I do it currently.

7
  • It’s better to have the keyboard inputs in the seperate thread and printing on the main thread. That way you can end the program cleanly from both branches on either a timer or a keyboard input Commented Nov 4 at 12:57
  • The answer is yes no matter what the code does - this is unreadable and probably not working. async doesn't mean` two things at the same time` either, that's concurrent. The Console doesn't allow concurrent modifications. When you read or write the Console class itself ensures only one operation will happen at a time Commented Nov 4 at 13:01
  • @Stratisco the Console doesn't allow concurrent modifications so using multiple threads will only add confusion and output that's possibly out of sync with the input Commented Nov 4 at 13:03
  • 2
    I suggest using Spectre.Console, Terminal.Gui or similar libraries to create console applications instead of trying to write everything from scratch. At least check how these libraries solve the problem. Commented Nov 4 at 13:08
  • 1
    BTW await Task.Delay(millisecondsPerWord); doesn't listen to the CancellationToken, so this code is no different from checking Readkey on every iteration. If you wanted to cancel waiting even during Task.Delay you'd have to pass cancellationToken to Task.Delay Commented Nov 4 at 13:21

1 Answer 1

0

A better way is to run the printing loop in a background task and concurrently monitor input in Task.Run, using a shared CancellationToken. This avoids blocking console output while checking for key presses.

Sign up to request clarification or add additional context in comments.

1 Comment

avoids blocking console output not really. Console access is always synchronized. Using another thread won't prevent Console.Write from blocking ReadKey and vice versa. The built-in buffering could reduce the perceived blocking, but wouldn't prevent it

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.