关于c#:如何向Console.ReadLine()添加超时?

关于c#:如何向Console.ReadLine()添加超时?

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

我有一个控制台应用程序,我想在其中给用户x秒的时间来响应提示。 如果经过一定时间后仍未输入任何内容,则程序逻辑应继续。 我们假设超时意味着空响应。

解决这个问题的最直接方法是什么?


令我惊讶的是,五年后,所有答案仍然遭受以下一个或多个问题的困扰:

  • 使用ReadLine以外的功能,会导致功能丧失。 (删除/退格/上一个键用于上一个输入)。
  • 多次调用时,函数的行为不佳(产生多个线程,许多吊挂的ReadLine或其他意外行为)。
  • 功能依赖于繁忙等待。这是一个可怕的浪费,因为等待可能会在几秒到超时之间的任何地方运行,超时可能是数分钟。如此长时间的繁忙等待是对资源的可怕吸收,在多线程方案中尤其如此。如果将忙碌等待更改为睡眠,这会对响应性产生负面影响,尽管我承认这可能不是一个大问题。

我相信我的解决方案将解决原来的问题,而不会遇到上述任何问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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.");
  }
}

当然,调用非常简单:

1
2
3
4
5
6
7
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.");
}

另外,您可以使用TryXX(out)约定,如shmueli建议:

1
2
3
4
5
6
7
8
9
  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;
  }

称为如下:

1
2
3
4
5
6
7
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);

在这两种情况下,您都无法将对Reader的调用与普通的Console.ReadLine调用混合:如果Reader超时,将挂起ReadLine调用。相反,如果要进行普通的(非定时的)ReadLine调用,只需使用Reader并忽略超时,以便默认将其设置为无限超时。

那么我提到的其他解决方案的那些问题呢?

  • 如您所见,使用ReadLine可以避免第一个问题。
  • 多次调用该函数时,其行为正常。无论是否发生超时,都只会运行一个后台线程,并且最多只能激活一次对ReadLine的调用。调用该函数将始终导致最新输入或超时,并且用户无需多次按下Enter键即可提交输入。
  • 而且,显然,该功能不依赖于繁忙等待。相反,它使用适当的多线程技术来防止浪费资源。

我预见到该解决方案的唯一问题是它不是线程安全的。但是,多个线程实际上并不能同时要求用户输入,因此无论如何都要在调用Reader.ReadLine之前进行同步。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
string ReadLine(int timeoutms)
{
    ReadLineDelegate d = Console.ReadLine;
    IAsyncResult result = d.BeginInvoke(null, null);
    result.AsyncWaitHandle.WaitOne(timeoutms);//timeout e.g. 15000 for 15 secs
    if (result.IsCompleted)
    {
        string resultstr = d.EndInvoke(result);
        Console.WriteLine("Read:" + resultstr);
        return resultstr;
    }
    else
    {
        Console.WriteLine("Timed out!");
        throw new TimedoutException("Timed Out!");
    }
}

delegate string ReadLineDelegate();

使用Console.KeyAvailable的方法会有所帮助吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Sample
{
    public static void Main()
    {
    ConsoleKeyInfo cki = new ConsoleKeyInfo();

    do {
        Console.WriteLine("
Press 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);
    }
}

一种或另一种方法确实需要第二个线程。您可以使用异步IO来避免声明自己的:

  • 声明一个ManualResetEvent,将其称为" evt"
  • 调用System.Console.OpenStandardInput以获取输入流。指定将存储其数据并设置evt的回调方法。
  • 调用该流的BeginRead方法以启动异步读取操作
  • 然后在ManualResetEvent上输入定时等待
  • 如果等待超时,则取消读取

如果读取返回数据,则设置事件,您的主线程将继续,否则将在超时后继续。


这对我有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ConsoleKeyInfo k = new ConsoleKeyInfo();
Console.WriteLine("Press any key in the next 5 seconds.");
for (int cnt = 5; cnt > 0; cnt--)
  {
    if (Console.KeyAvailable == true)
      {
        k = Console.ReadKey();
        break;
      }
    else
     {
       Console.WriteLine(cnt.ToString());
       System.Threading.Thread.Sleep(1000);
     }
 }
Console.WriteLine("The key pressed was" + k.Key);

1
2
3
4
5
6
7
8
9
10
11
12
13
// Wait for 'Enter' to be pressed or 5 seconds to elapse
using (Stream s = Console.OpenStandardInput())
{
    ManualResetEvent stop_waiting = new ManualResetEvent(false);
    s.BeginRead(new Byte[1], 0, 1, ar => stop_waiting.Set(), null);

    // ...do anything else, or simply...

    stop_waiting.WaitOne(5000);
    // If desired, other threads could also set 'stop_waiting'
    // Disposing the stream cancels the async read operation. It can be
    // re-opened if needed.
}

我认为您将需要创建一个辅助线程并在控制台上轮询密钥。我知道没有内置的方法可以完成此任务。


我在这个问题上苦苦挣扎了5个月,然后才找到在企业环境中可以完美使用的解决方案。

到目前为止,大多数解决方案的问题在于它们依赖于Console.ReadLine()以外的其他东西,而Console.ReadLine()具有许多优点:

  • 支持删除,退格键,箭头键等
  • 按下"向上"键并重复上一个命令的功能(如果您实现了一个使用率很高的后台调试控制台,这将非常方便)。

我的解决方案如下:

  • 使用Console.ReadLine()生成一个单独的线程来处理用户输入。
  • 超时时间过后,使用http://inputsimulator.codeplex.com/将[enter]键发送到当前控制台窗口中,解除对Console.ReadLine()的阻止。
  • 样例代码:

    1
     InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

    有关此技术的更多信息,包括中止使用Console.ReadLine的线程的正确技术:

    .NET调用以将[enter]击键发送到当前进程中,这是一个控制台应用程序?

    当所说的线程正在执行Console.ReadLine时,如何中止.NET中的另一个线程?


    在委托中调用Console.ReadLine()很糟糕,因为如果用户未点击" enter",则该调用将永远不会返回。执行委托的线程将被阻塞,直到用户单击" enter"为止,而无法取消它。

    发出一系列这些调用将不会像您期望的那样运行。考虑以下内容(使用上面的示例Console类):

    1
    2
    3
    4
    5
    6
    7
    System.Console.WriteLine("Enter your first name [John]:");

    string firstName = Console.ReadLine(5,"John");

    System.Console.WriteLine("Enter your last name [Doe]:");

    string lastName = Console.ReadLine(5,"Doe");

    用户让第一个提示的超时时间到期,然后为第二个提示输入一个值。 firstName和lastName都将包含默认值。当用户单击" enter"时,将完成第一个ReadLine调用,但是代码已放弃该调用,并实际上丢弃了结果。第二个ReadLine调用将继续阻塞,超时最终将到期,并且返回的值将再次为默认值。

    顺便说一句-上面的代码中有一个错误。通过调用waitHandle.Close(),可以从工作线程下关闭事件。如果用户在超时到期后单击" enter",则工作线程将尝试发信号通知引发ObjectDisposedException的事件。异常是从工作线程中抛出的,如果您尚未设置未处理的异常处理程序,则该过程将终止。


    如果使用Main()方法,则不能使用await,因此必须使用Task.WaitAny()

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

    但是,C#7.1引入了创建异步Main()方法的可能性,因此,只要有以下选项,最好使用Task.WhenAny()版本:

    1
    2
    3
    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;

    我可能对这个问题读得太多,但是我假设等待类似于启动菜单,在该菜单中等待15秒,除非您按任何键。您可以使用(1)阻塞函数,也可以使用(2)使用线程,事件和计时器。该事件将充当"继续"事件,并将一直阻塞到计时器到期或按键被按下为止。

    (1)的伪代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // Get configurable wait time
    TimeSpan waitTime = TimeSpan.FromSeconds(15.0);
    int configWaitTimeSec;
    if (int.TryParse(ConfigManager.AppSetting["DefaultWaitTime"], out configWaitTimeSec))
        waitTime = TimeSpan.FromSeconds(configWaitTimeSec);

    bool keyPressed = false;
    DateTime expireTime = DateTime.Now + waitTime;

    // Timer and key processor
    ConsoleKeyInfo cki;
    // EDIT: adding a missing ! below
    while (!keyPressed && (DateTime.Now < expireTime))
    {
        if (Console.KeyAvailable)
        {
            cki = Console.ReadKey(true);
            // TODO: Process key
            keyPressed = true;
        }
        Thread.Sleep(10);
    }

    不幸的是,我无法评论Gulzar的帖子,但这是一个更完整的示例:

    1
    2
    3
    4
    5
    6
    7
    8
                while (Console.KeyAvailable == false)
                {
                    Thread.Sleep(250);
                    i++;
                    if (i > 3)
                        throw new Exception("Timedout waiting for input.");
                }
                input = Console.ReadLine();

    好像这里没有足够的答案:0),下面的代码封装到上面的静态方法@kwl的解决方案中(第一个)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
        public static string ConsoleReadLineWithTimeout(TimeSpan timeout)
        {
            Task;lt;string;gt; task = Task.Factory.StartNew(Console.ReadLine);

            string result = Task.WaitAny(new Task[] { task }, timeout) == 0
                ? task.Result
                : string.Empty;
            return result;
        }

    用法

    1
    2
    3
    4
    5
    6
        static void Main()
        {
            Console.WriteLine("howdy");
            string result = ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(8.5));
            Console.WriteLine("bye");
        }

    编辑:通过在单独的进程中完成实际工作并在超时的情况下终止该进程来解决该问题。有关详情,请参见下文。 ew!

    只是运行了一下,它似乎运行良好。我的同事有一个使用Thread对象的版本,但是我发现委托类型的BeginInvoke()方法更加优雅。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    namespace TimedReadLine
    {
       public static class Console
       {
          private delegate string ReadLineInvoker();

          public static string ReadLine(int timeout)
          {
             return ReadLine(timeout, null);
          }

          public static string ReadLine(int timeout, string @default)
          {
             using (var process = new System.Diagnostics.Process
             {
                StartInfo =
                {
                   FileName ="ReadLine.exe",
                   RedirectStandardOutput = true,
                   UseShellExecute = false
                }
             })
             {
                process.Start();

                var rli = new ReadLineInvoker(process.StandardOutput.ReadLine);
                var iar = rli.BeginInvoke(null, null);

                if (!iar.AsyncWaitHandle.WaitOne(new System.TimeSpan(0, 0, timeout)))
                {
                   process.Kill();
                   return @default;
                }

                return rli.EndInvoke(iar);
             }
          }
       }
    }

    ReadLine.exe项目是一个非常简单的项目,具有一个如下所示的类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    namespace ReadLine
    {
       internal static class Program
       {
          private static void Main()
          {
             System.Console.WriteLine(System.Console.ReadLine());
          }
       }
    }


    .NET 4使用"任务"使这一操作变得异常简单。

    首先,建立您的助手:

    1
    2
    3
    4
       Private Function AskUser() As String
          Console.Write("Answer my question:")
          Return Console.ReadLine()
       End Function

    其次,执行任务并等待:

    1
    2
    3
    4
    5
    6
    7
          Dim askTask As Task(Of String) = New TaskFactory().StartNew(Function() AskUser())
          askTask.Wait(TimeSpan.FromSeconds(30))
          If Not askTask.IsCompleted Then
             Console.WriteLine("User failed to respond.")
          Else
             Console.WriteLine(String.Format("You responded, '{0}'.", askTask.Result))
          End If

    没有尝试重新创建ReadLine功能或执行其他危险的操作来使此工作正常进行。任务使我们能够非常自然地解决问题。


    简单的线程示例来解决这个问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Thread readKeyThread = new Thread(ReadKeyMethod);
    static ConsoleKeyInfo cki = null;

    void Main()
    {
        readKeyThread.Start();
        bool keyEntered = false;
        for(int ii = 0; ii < 10; ii++)
        {
            Thread.Sleep(1000);
            if(readKeyThread.ThreadState == ThreadState.Stopped)
                keyEntered = true;
        }
        if(keyEntered)
        { //do your stuff for a key entered
        }
    }

    void ReadKeyMethod()
    {
        cki = Console.ReadKey();
    }

    或顶部的静态字符串以获取整行。


    这不是很好又简短吗?

    1
    2
    3
    4
    5
    6
    if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout))
    {
        ConsoleKeyInfo keyInfo = Console.ReadKey();

        // Handle keyInfo value here...
    }

    我的情况下,这个工作很好:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public static ManualResetEvent evtToWait = new ManualResetEvent(false);

    private static void ReadDataFromConsole( object state )
    {
        Console.WriteLine("Enter "x" to exit or wait for 5 seconds.");

        while (Console.ReadKey().KeyChar != 'x')
        {
            Console.Out.WriteLine("");
            Console.Out.WriteLine("Enter again!");
        }

        evtToWait.Set();
    }

    static void Main(string[] args)
    {
            Thread status = new Thread(ReadDataFromConsole);
            status.Start();

            evtToWait = new ManualResetEvent(false);

            evtToWait.WaitOne(5000); // wait for evtToWait.Set() or timeOut

            status.Abort(); // exit anyway
            return;
    }

    我的代码完全基于朋友的回答@JSQuareD

    但是我需要使用Stopwatch进行计时,因为当我用Console.ReadKey()完成程序时,它仍在等待Console.ReadLine()并生成意外的行为。

    它对我来说是完美的。保持原始Console.ReadLine()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("What is the answer? (5 secs.)");
            try
            {
                var answer = ConsoleReadLine.ReadLine(5000);
                Console.WriteLine("Answer is: {0}", answer);
            }
            catch
            {
                Console.WriteLine("No answer");
            }
            Console.ReadKey();
        }
    }

    class ConsoleReadLine
    {
        private static string inputLast;
        private static Thread inputThread = new Thread(inputThreadAction) { IsBackground = true };
        private static AutoResetEvent inputGet = new AutoResetEvent(false);
        private static AutoResetEvent inputGot = new AutoResetEvent(false);

        static ConsoleReadLine()
        {
            inputThread.Start();
        }

        private static void inputThreadAction()
        {
            while (true)
            {
                inputGet.WaitOne();
                inputLast = Console.ReadLine();
                inputGot.Set();
            }
        }

        // omit the parameter to read a line without a timeout
        public static string ReadLine(int timeout = Timeout.Infinite)
        {
            if (timeout == Timeout.Infinite)
            {
                return Console.ReadLine();
            }
            else
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();

                while (stopwatch.ElapsedMilliseconds ;lt; timeout ;; !Console.KeyAvailable) ;

                if (Console.KeyAvailable)
                {
                    inputGet.Set();
                    inputGot.WaitOne();
                    return inputLast;
                }
                else
                {
                    throw new TimeoutException("User did not provide input within the timelimit.");
                }
            }
        }
    }


    这是Glen Slayden解决方案的完整示例。在构建另一个问题的测试用例时,我碰巧要这么做。它使用异步I / O和手动重置事件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public static void Main() {
        bool readInProgress = false;
        System.IAsyncResult result = null;
        var stop_waiting = new System.Threading.ManualResetEvent(false);
        byte[] buffer = new byte[256];
        var s = System.Console.OpenStandardInput();
        while (true) {
            if (!readInProgress) {
                readInProgress = true;
                result = s.BeginRead(buffer, 0, buffer.Length
                  , ar => stop_waiting.Set(), null);

            }
            bool signaled = true;
            if (!result.IsCompleted) {
                stop_waiting.Reset();
                signaled = stop_waiting.WaitOne(5000);
            }
            else {
                signaled = true;
            }
            if (signaled) {
                readInProgress = false;
                int numBytes = s.EndRead(result);
                string text = System.Text.Encoding.UTF8.GetString(buffer
                  , 0, numBytes);
                System.Console.Out.Write(string.Format(
                 "Thank you for typing: {0}", text));
            }
            else {
                System.Console.Out.WriteLine("oy, type something!");
            }
        }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    string readline ="?";
    ThreadPool.QueueUserWorkItem(
        delegate
        {
            readline = Console.ReadLine();
        }
    );
    do
    {
        Thread.Sleep(100);
    } while (readline =="?");

    请注意,如果您沿" Console.ReadKey"路线走,则会失去ReadLine的一些出色功能,即:

    • 支持删除,退格键,箭头键等
    • 按下"向上"键并重复上一个命令的功能(如果您实现了一个使用率很高的后台调试控制台,这将非常方便)。

    要添加超时,请更改while循环以适合。


    使用Console.KeyAvailable的简单示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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");
    }

    我有一个Windows应用程序(Windows服务)的特殊情况。在Environment.IsInteractive(VS调试器或cmd.exe中)以交互方式运行程序时,我使用AttachConsole / AllocConsole来获取标准输入/标准输出。
    为了防止进程在完成工作时结束,UI线程调用Console.ReadKey(false)。我想取消等待UI线程正在从另一个线程执行的操作,因此我想出了@JSquaredD对解决方案的修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    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();
        }
      }

      /// ;lt;summary;gt;
      /// Signals the thread that called WaitOne should be allowed to continue
      /// ;lt;/summary;gt;
      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();
      }

      /// ;lt;summary;gt;
      /// Wait until a key is pressed or ;lt;see cref="Cancel"/;gt; is called by another thread
      /// ;lt;/summary;gt;
      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();
      }    
    }

    更现代的,基于任务的代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public string ReadLine(int timeOutMillisecs)
    {
        var inputBuilder = new StringBuilder();

        var task = Task.Factory.StartNew(() =;gt;
        {
            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();
    }


    这似乎是最简单,有效的解决方案,它不使用任何本机API:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
        static Task;lt;string;gt; ReadLineAsync(CancellationToken cancellation)
        {
            return Task.Run(() =;gt;
            {
                while (!Console.KeyAvailable)
                {
                    if (cancellation.IsCancellationRequested)
                        return null;

                    Thread.Sleep(100);
                }
                return Console.ReadLine();
            });
        }

    用法示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
        static void Main(string[] args)
        {
            AsyncContext.Run(async () =;gt;
            {
                CancellationTokenSource cancelSource = new CancellationTokenSource();
                cancelSource.CancelAfter(1000);
                Console.WriteLine(await ReadLineAsync(cancelSource.Token) ??"null");
            });
        }

    上面的Eric帖子的示例实现。此特定示例用于读取通过管道传递到控制台应用程序的信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
     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
        }
    }

    请不要讨厌我为现有答案的过多添加另一种解决方案!这适用于Console.ReadKey(),但可以轻松修改以与ReadLine()等一起使用。

    由于" Console.Read"方法被阻塞,因此有必要"微调" StdIn流以取消读取。

    调用语法:

    1
    2
    3
    ConsoleKeyInfo keyInfo;
    bool keyPressed = AsyncConsole.ReadKey(500, out keyInfo);
    // where 500 is the timeout

    码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    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);
    }

    这是使用Console.KeyAvailable的解决方案。这些阻塞了调用,但是如果需要的话,通过TPL异步调用它们应该是很简单的。我使用标准的取消机制来简化与Task Asynchronous Pattern和所有这些好东西的连接。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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 ;lt; latest);
        return null;
      }
    }

    这有一些缺点。

    • 您没有获得ReadLine提供的标准导航功能(上/下箭头滚动等)。
    • 如果按下特殊键(F1,PrtScn等),则会在输入中插入" 0"字符。您可以通过修改代码轻松地将它们过滤掉。

    获得第二个线程的另一种便宜的方法是将其包装在委托中。


    到这里结束了,因为有人问了一个重复的问题。我想出了以下看起来很简单的解决方案。我确信它有一些我错过的缺点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    static void Main(string[] args)
    {
        Console.WriteLine("Hit q to continue or wait 10 seconds.");

        Task task = Task.Factory.StartNew(() =;gt; loop());

        Console.WriteLine("Started waiting");
        task.Wait(10000);
        Console.WriteLine("Stopped waiting");
    }

    static void loop()
    {
        while (true)
        {
            if ('q' == Console.ReadKey().KeyChar) break;
        }
    }

    我来到这个答案,最终做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
        /// ;lt;summary;gt;
        /// Reads Line from console with timeout.
        /// ;lt;/summary;gt;
        /// ;lt;exception cref="System.TimeoutException";gt;If user does not enter line in the specified time.;lt;/exception;gt;
        /// ;lt;param name="timeout";gt;Time to wait in milliseconds. Negative value will wait forever.;lt;/param;gt;        
        /// ;lt;returns;gt;;lt;/returns;gt;        
        public static string ReadLine(int timeout = -1)
        {
            ConsoleKeyInfo cki = new ConsoleKeyInfo();
            StringBuilder sb = new StringBuilder();

            // if user does not want to spesify a timeout
            if (timeout ;lt; 0)
                return Console.ReadLine();

            int counter = 0;

            while (true)
            {
                while (Console.KeyAvailable == false)
                {
                    counter++;
                    Thread.Sleep(1);
                    if (counter ;gt; timeout)
                        throw new System.TimeoutException("Line was not entered in timeout specified");
                }

                cki = Console.ReadKey(false);

                if (cki.Key == ConsoleKey.Enter)
                {
                    Console.WriteLine();
                    return sb.ToString();
                }
                else
                    sb.Append(cki.KeyChar);                
            }            
        }

    这是一个安全的解决方案,它伪造控制台输入以在超时后取消阻塞线程。
    https://github.com/Igorium/ConsoleReader项目提供了一个示例用户对话框实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    var inputLine = ReadLine(5);

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

        var timeoutMilliseconds = timeoutSeconds * 1000;

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

        CancellationTokenSource cts = new CancellationTokenSource();

        Task.Factory
            .StartNew(() =;gt; SpinUserDialog(timeoutMilliseconds, countDownMessage, samplingFrequencyMilliseconds, cts.Token), cts.Token)
            .ContinueWith(t =;gt; {
                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;lt;uint, string;gt; countDownMessage, uint samplingFrequencyMilliseconds,
        CancellationToken token)
    {
        while (countDownMilliseconds ;gt; 0)
        {
            token.ThrowIfCancellationRequested();

            Thread.Sleep((int)samplingFrequencyMilliseconds);

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


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

    推荐阅读