如何给Console.ReadLine()添加超时时间?

138

我有一个控制台应用程序,我想在其中给用户x秒来响应提示。如果一定时间后没有输入,则应继续执行程序逻辑。我们假设超时意味着空响应。

最直接的方法是什么?

33个回答

1
我有一个使用Windows API解决此问题的方案,相比于这里的许多解决方案,它具有一些优点:
  • 使用Console.ReadLine检索输入,因此您可以获得与此相关的所有好处(输入历史记录等)
  • 在超时后强制Console.ReadLine调用完成,因此您不会为每个超时调用累积新线程。
  • 不会不安全地中止线程。
  • 不像输入伪造方法那样存在焦点问题。

两个主要缺点:

  • 仅适用于Windows。
  • 相当复杂。

基本思路是Windows API有一个函数来取消未完成的I/O请求:CancelIoEx。 当您将其用于取消STDIN上的操作时,Console.ReadLine会抛出OperationCanceledException。

所以这就是你该怎么做:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleHelper
{
    public static class ConsoleHelper
    {
        public static string ReadLine(TimeSpan timeout)
        {
            return ReadLine(Task.Delay(timeout));
        }

        public static string ReadLine(Task cancel_trigger)
        {
            var status = new Status();

            var cancel_task = Task.Run(async () =>
            {
                await cancel_trigger;

                status.Mutex.WaitOne();
                bool io_done = status.IODone;
                if (!io_done)
                    status.CancellationStarted = true;
                status.Mutex.ReleaseMutex();

                while (!status.IODone)
                {
                    var success = CancelStdIn(out int error_code);

                    if (!success && error_code != 0x490) // 0x490 is what happens when you call cancel and there is not a pending I/O request
                        throw new Exception($"Canceling IO operation on StdIn failed with error {error_code} ({error_code:x})");
                }
            });

            ReadLineWithStatus(out string input, out bool read_canceled);
            
            if (!read_canceled)
            {
                status.Mutex.WaitOne();
                bool must_wait = status.CancellationStarted;
                status.IODone = true;
                status.Mutex.ReleaseMutex();

                if (must_wait)
                    cancel_task.Wait();

                return input;
            }
            else // read_canceled == true
            {
                status.Mutex.WaitOne();
                bool cancel_started = status.CancellationStarted;
                status.IODone = true;
                status.Mutex.ReleaseMutex();

                if (!cancel_started)
                    throw new Exception("Received cancelation not triggered by this method.");
                else
                    cancel_task.Wait();

                return null;
            }
        }

        private const int STD_INPUT_HANDLE = -10;

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr GetStdHandle(int nStdHandle);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CancelIoEx(IntPtr handle, IntPtr lpOverlapped);


        private static bool CancelStdIn(out int error_code)
        {
            var handle = GetStdHandle(STD_INPUT_HANDLE);
            bool success = CancelIoEx(handle, IntPtr.Zero);

            if (success)
            {
                error_code = 0;
                return true;
            }
            else
            {
                var rc = Marshal.GetLastWin32Error();
                error_code = rc;
                return false;
            }
        }

        private class Status
        {
            public Mutex Mutex = new Mutex(false);
            public volatile bool IODone;
            public volatile bool CancellationStarted;
        }

        private static void ReadLineWithStatus(out string result, out bool operation_canceled)
        {
            try
            {
                result = Console.ReadLine();
                operation_canceled = false;
            }
            catch (OperationCanceledException)
            {
                result = null;
                operation_canceled = true;
            }
        }
    }
}


避免简化此过程,正确处理线程非常棘手。您需要处理以下所有情况:
  • 触发取消并在Console.ReadLine开始之前调用CancelStdIn(这就是为什么需要在cancel_trigger中使用循环的原因)。
  • Console.ReadLine在触发取消之前返回(可能早得多)。
  • Console.ReadLine在触发取消之后但在调用CancelStdIn之前返回。
  • Console.ReadLine由于响应取消触发器而由于调用CancelStdIn而引发异常。

鸣谢: 从SO答案中获得了CancelIoEx的想法,该答案来自Gérald Barré's博客。但是这些解决方案存在微妙的并发错误。


1
这里有一个安全的解决方案,可以通过模拟控制台输入,在超时后解除线程阻塞。https://github.com/Igorium/ConsoleReader 项目提供了一个示例用户对话框实现。
var inputLine = ReadLine(5);

public static string ReadLine(uint timeoutSeconds, Func<uint, string> countDownMessage, uint samplingFrequencyMilliseconds)
{
    if (timeoutSeconds == 0)
        return null;

    var timeoutMilliseconds = timeoutSeconds * 1000;

    if (samplingFrequencyMilliseconds > timeoutMilliseconds)
        throw new ArgumentException("Sampling frequency must not be greater then timeout!", "samplingFrequencyMilliseconds");

    CancellationTokenSource cts = new CancellationTokenSource();

    Task.Factory
        .StartNew(() => SpinUserDialog(timeoutMilliseconds, countDownMessage, samplingFrequencyMilliseconds, cts.Token), cts.Token)
        .ContinueWith(t => {
            var hWnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
            PostMessage(hWnd, 0x100, 0x0D, 9);
        }, TaskContinuationOptions.NotOnCanceled);


    var inputLine = Console.ReadLine();
    cts.Cancel();

    return inputLine;
}


private static void SpinUserDialog(uint countDownMilliseconds, Func<uint, string> countDownMessage, uint samplingFrequencyMilliseconds,
    CancellationToken token)
{
    while (countDownMilliseconds > 0)
    {
        token.ThrowIfCancellationRequested();

        Thread.Sleep((int)samplingFrequencyMilliseconds);

        countDownMilliseconds -= countDownMilliseconds > samplingFrequencyMilliseconds
            ? samplingFrequencyMilliseconds
            : countDownMilliseconds;
    }
}


[DllImport("User32.Dll", EntryPoint = "PostMessageA")]
private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);

0
一个使用Console.KeyAvailable的简单示例:
Console.WriteLine("Press any key during the next 2 seconds...");
Thread.Sleep(2000);
if (Console.KeyAvailable)
{
    Console.WriteLine("Key pressed");
}
else
{
    Console.WriteLine("You were too slow");
}

如果用户按下并在2000毫秒内松开键,会发生什么? - Izzy
如果在2秒之前按下按钮,它将无法工作,因为它处于睡眠模式。 - Marc Dirven

0

因为有一个重复的问题被问到了,所以我来到这里。我想出了以下看起来很简单的解决方案。我相信我可能错过了一些缺点。

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

我有一个独特的情况,即拥有一个Windows应用程序(Windows服务)。当以交互方式运行程序Environment.IsInteractive(从VS调试器或cmd.exe中),我使用AttachConsole/AllocConsole来获取我的stdin/stdout。 为了在工作进行时保持进程不结束,UI线程调用Console.ReadKey(false)。我想从另一个线程取消UI线程正在等待的操作,因此我对@JSquaredD的解决方案进行了修改。

using System;
using System.Diagnostics;

internal class PressAnyKey
{
  private static Thread inputThread;
  private static AutoResetEvent getInput;
  private static AutoResetEvent gotInput;
  private static CancellationTokenSource cancellationtoken;

  static PressAnyKey()
  {
    // Static Constructor called when WaitOne is called (technically Cancel too, but who cares)
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(ReaderThread);
    inputThread.IsBackground = true;
    inputThread.Name = "PressAnyKey";
    inputThread.Start();
  }

  private static void ReaderThread()
  {
    while (true)
    {
      // ReaderThread waits until PressAnyKey is called
      getInput.WaitOne();
      // Get here 
      // Inner loop used when a caller uses PressAnyKey
      while (!Console.KeyAvailable && !cancellationtoken.IsCancellationRequested)
      {
        Thread.Sleep(50);
      }
      // Release the thread that called PressAnyKey
      gotInput.Set();
    }
  }

  /// <summary>
  /// Signals the thread that called WaitOne should be allowed to continue
  /// </summary>
  public static void Cancel()
  {
    // Trigger the alternate ending condition to the inner loop in ReaderThread
    if(cancellationtoken== null) throw new InvalidOperationException("Must call WaitOne before Cancelling");
    cancellationtoken.Cancel();
  }

  /// <summary>
  /// Wait until a key is pressed or <see cref="Cancel"/> is called by another thread
  /// </summary>
  public static void WaitOne()
  {
    if(cancellationtoken==null || cancellationtoken.IsCancellationRequested) throw new InvalidOperationException("Must cancel a pending wait");
    cancellationtoken = new CancellationTokenSource();
    // Release the reader thread
    getInput.Set();
    // Calling thread will wait here indefiniately 
    // until a key is pressed, or Cancel is called
    gotInput.WaitOne();
  }    
}

0

以下是对Eric文章的示例实现。此特定示例用于通过管道传递信息并读取控制台应用程序中的信息:

 using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace PipedInfo
{
    class Program
    {
        static void Main(string[] args)
        {
            StreamReader buffer = ReadPipedInfo();

            Console.WriteLine(buffer.ReadToEnd());
        }

        #region ReadPipedInfo
        public static StreamReader ReadPipedInfo()
        {
            //call with a default value of 5 milliseconds
            return ReadPipedInfo(5);
        }

        public static StreamReader ReadPipedInfo(int waitTimeInMilliseconds)
        {
            //allocate the class we're going to callback to
            ReadPipedInfoCallback callbackClass = new ReadPipedInfoCallback();

            //to indicate read complete or timeout
            AutoResetEvent readCompleteEvent = new AutoResetEvent(false);

            //open the StdIn so that we can read against it asynchronously
            Stream stdIn = Console.OpenStandardInput();

            //allocate a one-byte buffer, we're going to read off the stream one byte at a time
            byte[] singleByteBuffer = new byte[1];

            //allocate a list of an arbitary size to store the read bytes
            List<byte> byteStorage = new List<byte>(4096);

            IAsyncResult asyncRead = null;
            int readLength = 0; //the bytes we have successfully read

            do
            {
                //perform the read and wait until it finishes, unless it's already finished
                asyncRead = stdIn.BeginRead(singleByteBuffer, 0, singleByteBuffer.Length, new AsyncCallback(callbackClass.ReadCallback), readCompleteEvent);
                if (!asyncRead.CompletedSynchronously)
                    readCompleteEvent.WaitOne(waitTimeInMilliseconds);

                //end the async call, one way or another

                //if our read succeeded we store the byte we read
                if (asyncRead.IsCompleted)
                {
                    readLength = stdIn.EndRead(asyncRead);
                    if (readLength > 0)
                        byteStorage.Add(singleByteBuffer[0]);
                }

            } while (asyncRead.IsCompleted && readLength > 0);
            //we keep reading until we fail or read nothing

            //return results, if we read zero bytes the buffer will return empty
            return new StreamReader(new MemoryStream(byteStorage.ToArray(), 0, byteStorage.Count));
        }

        private class ReadPipedInfoCallback
        {
            public void ReadCallback(IAsyncResult asyncResult)
            {
                //pull the user-defined variable and strobe the event, the read finished successfully
                AutoResetEvent readCompleteEvent = asyncResult.AsyncState as AutoResetEvent;
                readCompleteEvent.Set();
            }
        }
        #endregion ReadPipedInfo
    }
}

0
请不要因为我添加了另一个解决方案而讨厌我,因为已经有很多现有的答案了!这个解决方案适用于Console.ReadKey(),但可以轻松修改以适用于ReadLine()等其他方法。
由于"Console.Read"方法是阻塞的,所以需要“nudge”StdIn流来取消读取操作。
调用语法:
ConsoleKeyInfo keyInfo;
bool keyPressed = AsyncConsole.ReadKey(500, out keyInfo);
// where 500 is the timeout

代码:

public class AsyncConsole // not thread safe
{
    private static readonly Lazy<AsyncConsole> Instance =
        new Lazy<AsyncConsole>();

    private bool _keyPressed;
    private ConsoleKeyInfo _keyInfo;

    private bool DoReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        _keyPressed = false;
        _keyInfo = new ConsoleKeyInfo();

        Thread readKeyThread = new Thread(ReadKeyThread);
        readKeyThread.IsBackground = false;
        readKeyThread.Start();

        Thread.Sleep(millisecondsTimeout);

        if (readKeyThread.IsAlive)
        {
            try
            {
                IntPtr stdin = GetStdHandle(StdHandle.StdIn);
                CloseHandle(stdin);
                readKeyThread.Join();
            }
            catch { }
        }

        readKeyThread = null;

        keyInfo = _keyInfo;
        return _keyPressed;
    }

    private void ReadKeyThread()
    {
        try
        {
            _keyInfo = Console.ReadKey();
            _keyPressed = true;
        }
        catch (InvalidOperationException) { }
    }

    public static bool ReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        return Instance.Value.DoReadKey(millisecondsTimeout, out keyInfo);
    }

    private enum StdHandle { StdIn = -10, StdOut = -11, StdErr = -12 };

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(StdHandle std);

    [DllImport("kernel32.dll")]
    private static extern bool CloseHandle(IntPtr hdl);
}

0
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

请注意,如果使用“Console.ReadKey”方法,将会失去ReadLine的一些很酷的功能,包括:
  • 支持删除、退格、箭头键等。
  • 按“上”键并重复上一个命令的能力(如果您实现了一个后台调试控制台,它会非常有用)。
要添加超时,请更改while循环以适应。

0
这里有一个使用Console.KeyAvailable的解决方案。虽然这些是阻塞调用,但如果需要,通过TPL异步调用它们应该相当简单。我使用了标准的取消机制,以便轻松地与任务异步模式和所有好东西连接起来。
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;
  }
}

这有一些缺点。

  • 您无法获得 ReadLine 提供的标准导航功能(上/下箭头滚动等)。
  • 如果按下特殊键(F1、PrtScn 等),则会向输入中注入 '\0' 字符。不过,您可以通过修改代码轻松地过滤它们。

0

更现代化和基于任务的代码可能看起来像这样:

public string ReadLine(int timeOutMillisecs)
{
    var inputBuilder = new StringBuilder();

    var task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            var consoleKey = Console.ReadKey(true);
            if (consoleKey.Key == ConsoleKey.Enter)
            {
                return inputBuilder.ToString();
            }

            inputBuilder.Append(consoleKey.KeyChar);
        }
    });


    var success = task.Wait(timeOutMillisecs);
    if (!success)
    {
        throw new TimeoutException("User did not provide input within the timelimit.");
    }

    return inputBuilder.ToString();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接