How to add a Timeout to Console.ReadLine()?

Larsenal picture Larsenal · Sep 11, 2008 · Viewed 58.2k times · Source

I have a console app in which I want to give the user x seconds to respond to the prompt. If no input is made after a certain period of time, program logic should continue. We assume a timeout means empty response.

What is the most straightforward way of approaching this?

Answer

JSQuareD picture JSQuareD · Aug 20, 2013

I'm surprised to learn that after 5 years, all of the answers still suffer from one or more of the following problems:

  • A function other than ReadLine is used, causing loss of functionality. (Delete/backspace/up-key for previous input).
  • Function behaves badly when invoked multiple times (spawning multiple threads, many hanging ReadLine's, or otherwise unexpected behavior).
  • Function relies on a busy-wait. Which is a horrible waste since the wait is expected to run anywhere from a number of seconds up to the timeout, which might be multiple minutes. A busy-wait which runs for such an ammount of time is a horrible suck of resources, which is especially bad in a multithreading scenario. If the busy-wait is modified with a sleep this has a negative effect on responsiveness, although I admit that this is probably not a huge problem.

I believe my solution will solve the original problem without suffering from any of the above problems:

class Reader {
  private static Thread inputThread;
  private static AutoResetEvent getInput, gotInput;
  private static string input;

  static Reader() {
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(reader);
    inputThread.IsBackground = true;
    inputThread.Start();
  }

  private static void reader() {
    while (true) {
      getInput.WaitOne();
      input = Console.ReadLine();
      gotInput.Set();
    }
  }

  // omit the parameter to read a line without a timeout
  public static string ReadLine(int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      return input;
    else
      throw new TimeoutException("User did not provide input within the timelimit.");
  }
}

Calling is, of course, very easy:

try {
  Console.WriteLine("Please enter your name within the next 5 seconds.");
  string name = Reader.ReadLine(5000);
  Console.WriteLine("Hello, {0}!", name);
} catch (TimeoutException) {
  Console.WriteLine("Sorry, you waited too long.");
}

Alternatively, you can use the TryXX(out) convention, as shmueli suggested:

  public static bool TryReadLine(out string line, int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      line = input;
    else
      line = null;
    return success;
  }

Which is called as follows:

Console.WriteLine("Please enter your name within the next 5 seconds.");
string name;
bool success = Reader.TryReadLine(out name, 5000);
if (!success)
  Console.WriteLine("Sorry, you waited too long.");
else
  Console.WriteLine("Hello, {0}!", name);

In both cases, you cannot mix calls to Reader with normal Console.ReadLine calls: if the Reader times out, there will be a hanging ReadLine call. Instead, if you want to have a normal (non-timed) ReadLine call, just use the Reader and omit the timeout, so that it defaults to an infinite timeout.

So how about those problems of the other solutions I mentioned?

  • As you can see, ReadLine is used, avoiding the first problem.
  • The function behaves properly when invoked multiple times. Regardless of whether a timeout occurs or not, only one background thread will ever be running and only at most one call to ReadLine will ever be active. Calling the function will always result in the latest input, or in a timeout, and the user won't have to hit enter more than once to submit his input.
  • And, obviously, the function does not rely on a busy-wait. Instead it uses proper multithreading techniques to prevent wasting resources.

The only problem that I foresee with this solution is that it is not thread-safe. However, multiple threads can't really ask the user for input at the same time, so synchronization should be happening before making a call to Reader.ReadLine anyway.