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

后端 未结 30 2560
耶瑟儿~
耶瑟儿~ 2020-11-22 04:57

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

相关标签:
30条回答
  • 2020-11-22 05:56

    As if there weren't already enough answers here :0), the following encapsulates into a static method @kwl's solution above (the first one).

        public static string ConsoleReadLineWithTimeout(TimeSpan timeout)
        {
            Task<string> task = Task.Factory.StartNew(Console.ReadLine);
    
            string result = Task.WaitAny(new Task[] { task }, timeout) == 0
                ? task.Result 
                : string.Empty;
            return result;
        }
    

    Usage

        static void Main()
        {
            Console.WriteLine("howdy");
            string result = ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(8.5));
            Console.WriteLine("bye");
        }
    
    0 讨论(0)
  • 2020-11-22 05:56

    Ended up here because a duplicate question was asked. I came up with the following solution which looks straightforward. I am sure it has some drawbacks I missed.

    static void Main(string[] args)
    {
        Console.WriteLine("Hit q to continue or wait 10 seconds.");
    
        Task task = Task.Factory.StartNew(() => loop());
    
        Console.WriteLine("Started waiting");
        task.Wait(10000);
        Console.WriteLine("Stopped waiting");
    }
    
    static void loop()
    {
        while (true)
        {
            if ('q' == Console.ReadKey().KeyChar) break;
        }
    }
    
    0 讨论(0)
  • 2020-11-22 05:58

    Here is a solution that uses Console.KeyAvailable. These are blocking calls, but it should be fairly trivial to call them asynchronously via the TPL if desired. I used the standard cancellation mechanisms to make it easy to wire in with the Task Asynchronous Pattern and all that good stuff.

    public static class ConsoleEx
    {
      public static string ReadLine(TimeSpan timeout)
      {
        var cts = new CancellationTokenSource();
        return ReadLine(timeout, cts.Token);
      }
    
      public static string ReadLine(TimeSpan timeout, CancellationToken cancellation)
      {
        string line = "";
        DateTime latest = DateTime.UtcNow.Add(timeout);
        do
        {
            cancellation.ThrowIfCancellationRequested();
            if (Console.KeyAvailable)
            {
                ConsoleKeyInfo cki = Console.ReadKey();
                if (cki.Key == ConsoleKey.Enter)
                {
                    return line;
                }
                else
                {
                    line += cki.KeyChar;
                }
            }
            Thread.Sleep(1);
        }
        while (DateTime.UtcNow < latest);
        return null;
      }
    }
    

    There are some disadvantages with this.

    • You do not get the standard navigation features that ReadLine provides (up/down arrow scrolling, etc.).
    • This injects '\0' characters into input if a special key is press (F1, PrtScn, etc.). You could easily filter them out by modifying the code though.
    0 讨论(0)
  • 2020-11-22 05:59

    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.

    0 讨论(0)
  • 2020-11-22 06:00

    Will this approach using Console.KeyAvailable help?

    class Sample 
    {
        public static void Main() 
        {
        ConsoleKeyInfo cki = new ConsoleKeyInfo();
    
        do {
            Console.WriteLine("\nPress a key to display; press the 'x' key to quit.");
    
    // Your code could perform some useful task in the following loop. However, 
    // for the sake of this example we'll merely pause for a quarter second.
    
            while (Console.KeyAvailable == false)
                Thread.Sleep(250); // Loop until input is entered.
            cki = Console.ReadKey(true);
            Console.WriteLine("You pressed the '{0}' key.", cki.Key);
            } while(cki.Key != ConsoleKey.X);
        }
    }
    
    0 讨论(0)
  • 2020-11-22 06:00

    If you're in the Main() method, you can't use await, so you'll have to use Task.WaitAny():

    var task = Task.Factory.StartNew(Console.ReadLine);
    var result = Task.WaitAny(new Task[] { task }, TimeSpan.FromSeconds(5)) == 0
        ? task.Result : string.Empty;
    

    However, C# 7.1 introduces the possiblity to create an async Main() method, so it's better to use the Task.WhenAny() version whenever you have that option:

    var task = Task.Factory.StartNew(Console.ReadLine);
    var completedTask = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(5)));
    var result = object.ReferenceEquals(task, completedTask) ? task.Result : string.Empty;
    
    0 讨论(0)
提交回复
热议问题