如何添加一个超时Console.ReadLine()?

我有一个控制台应用程序,我想让用户x秒响应提示。 如果在一段时间后没有输入,程序逻辑应该继续。 我们假设超时意味着空的回应。

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

我很惊讶地发现,5年后,所有的答案仍然存在以下一个或多个问题:

  • 使用ReadLine以外的功能,导致功能丧失。 (删除/退格键/上一个键为以前的输入)。
  • 函数调用多次(产生多个线程,许多悬挂的ReadLine或其他意外的行为)的行为非常糟糕。
  • 功能依赖于忙等待。 这是一个可怕的浪费,因为等待时间预计会从几秒钟到超时,这可能是几分钟。 一个忙着等待这么多的时间是一个可怕的资源吸收,这在多线程的情况下尤其糟糕。 如果忙碌的等待是用睡眠来修改的,这会对响应性产生负面影响,尽管我承认这可能不是一个大问题。

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

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."); } } 

通话当然非常简单:

 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所示:

  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; } 

这被称为如下:

 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); 

在这两种情况下,您都不能将调用与正常的Console.ReadLine调用混合到Reader :如果Reader超时,将会挂起ReadLine调用。 相反,如果你想有一个正常的(非定时的) ReadLine调用,只需使用Reader并省略超时,以便默认为无限超时。

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

  • 正如你所见,使用了ReadLine,避免了第一个问题。
  • 该函数在多次调用时表现正常。 无论是否发生超时,只有一个后台线程可以运行,只有最多一次ReadLine调用才会被激活。 调用该函数将始终导致最新的输入或超时,并且用户将不必多次输入以提交输入。
  • 而且,显然,这个功能并不依赖于一个忙碌的等待。 相反,它使用适当的多线程技术来防止浪费资源。

我认为这个解决方案唯一的问题是它不是线程安全的。 但是,多线程不能同时向用户请求输入,所以在进行Reader.ReadLine调用之前应该进行同步。

 string ReadLine(int timeoutms) { ReadLineDelegate d = Console.ReadLine; IAsyncResult result = d.BeginInvoke(null, null); result.AsyncWaitHandle.WaitOne(timeoutms);//timeout eg 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的帮助?

 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); } } 

无论如何,你需要第二个线程。 您可以使用异步IO来避免声明自己的:

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

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

 // 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. } 

我想你将需要做一个辅助线程和轮询在控制台上的一个键。 我知道没有建立的方式来实现这一点。

这对我有效。

 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); 

我在这个问题上挣扎了5个月,才找到了一个在企业环境中完美工作的解决方案。

到目前为止,大多数解决方案的问题在于它们依赖于Console.ReadLine()和Console.ReadLine()之外的其他功能。

  • 支持删除,退格键,箭头键等
  • 能够按下“向上”键并重复上一个命令(如果你实现了一个后台调试控制台,这个控制台非常有用)。

我的解决方案如下:

  1. 产生一个单独的线程来处理使用Console.ReadLine()的用户输入。
  2. 超时之后,通过使用http://inputsimulator.codeplex.com/向当前控制台窗口发送一个%5Benter%5D键,以解除对Console.ReadLine()的阻止。

示例代码:

  InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN); 

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

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

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

在委托中调用Console.ReadLine()是不好的,因为如果用户没有按“enter”,那么这个调用将永远不会返回。 执行代理的线程将被阻塞,直到用户点击“enter”,无法取消它。

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

 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都将包含默认值。 当用户点击“输入”时,第一个 ReadLine调用将完成,但代码已经放弃了该调用并基本上放弃了结果。 第二个 ReadLine调用将继续阻塞,超时将最终失效,返回的值将再次成为默认值。

BTW-上面的代码有一个错误。 通过调用waitHandle.Close(),可以关闭工作线程下的事件。 如果用户在超时到期后点击“输入”,工作线程将尝试发信号通知引发ObjectDisposedException的事件。 从工作线程抛出异常,如果你没有设置一个未处理的异常处理程序,你的进程将终止。

我可能读了太多的问题,但我假设等待将类似于启动菜单,它等待15秒,除非你按下一个键。 你可以使用(1)一个阻塞函数或者(2)你可以使用一个线程,一个事件和一个计时器。 事件将作为“继续”,并将阻塞,直到计时器过期或按下一个键。

(1)的伪代码将是:

 // 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); } 

如果你在Main()方法中,你不能使用await ,所以你必须使用Task.WaitAny()

 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() ,因此,只要具有该选项,最好使用Task.WhenAny()版本:

 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; 

不幸的是,我不能评论Gulzar的帖子,但是这里有一个更完整的例子:

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

编辑 :通过在一个单独的过程中完成实际的工作,并且如果超时,杀死该过程来解决问题。 详情请参阅下文。 呼!

刚刚给了这个运行,它似乎很好地工作。 我的同事有一个使用Thread对象的版本,但是我发现Delegate类型的BeginInvoke()方法有点优雅。

 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项目是一个非常简单的项目,它有一个看起来像这样的类:

 namespace ReadLine { internal static class Program { private static void Main() { System.Console.WriteLine(System.Console.ReadLine()); } } } 

使用任务.NET 4使这个令人难以置信的简单。

首先,建立你的帮手:

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

其次,执行任务并等待:

  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功能或执行其他危险的黑客得到这个工作。 任务让我们以非常自然的方式解决问题。

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

 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(); } 

或者一个静态的字符串,以获得一整行。

我的情况下这工作很好:

 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; } 

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

 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!"); } } 

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

上面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 } } 
 string readline = "?"; ThreadPool.QueueUserWorkItem( delegate { readline = Console.ReadLine(); } ); do { Thread.Sleep(100); } while (readline == "?"); 

请注意,如果沿着“Console.ReadKey”路线走下去,您将失去ReadLine的一些很酷的功能,即:

  • 支持删除,退格键,箭头键等
  • 能够按下“向上”键并重复上一个命令(如果你实现了一个后台调试控制台,这个控制台非常有用)。

要添加超时,请修改适合的while循环。

这不是很好吗?

 if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout)) { ConsoleKeyInfo keyInfo = Console.ReadKey(); // Handle keyInfo value here... } 

请不要恨我为现有的答案增加另一个解决方案! 这适用于Console.ReadKey(),但可以很容易地修改为使用ReadLine()等。

由于“Console.Read”方法被阻塞,因此需要“ 轻推 ”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); } 

这是一个使用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”字符输入到输入中。 你可以很容易地通过修改代码来过滤掉它们。

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

 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; } } 

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

  /// <summary> /// Reads Line from console with timeout. /// </summary> /// <exception cref="System.TimeoutException">If user does not enter line in the specified time.</exception> /// <param name="timeout">Time to wait in milliseconds. Negative value will wait forever.</param> /// <returns></returns> 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 < 0) return Console.ReadLine(); int counter = 0; while (true) { while (Console.KeyAvailable == false) { counter++; Thread.Sleep(1); if (counter > 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); } } 

一个简单的例子使用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"); } 

更现代化和基于任务的代码将如下所示:

 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(); } 

我有一个Windows应用程序(Windows服务)的独特情况。 当以交互方式运行程序Environment.IsInteractive (VS Debugger或从cmd.exe),我使用AttachConsole / AllocConsole来得到我的标准输入/标准输出。 为了防止进程在作业完成时结束,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(); } } 

这是一个安全的解决方案,它使控制台输入在超时后解锁线程。 https://github.com/IgoSol/ConsoleReader项目提供了一个示例用户对话框实现&#x3002;

 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); 

This seems to be the simplest, working solution, that doesn't use any native APIs:

  static Task<string> ReadLineAsync(CancellationToken cancellation) { return Task.Run(() => { while (!Console.KeyAvailable) { if (cancellation.IsCancellationRequested) return null; Thread.Sleep(100); } return Console.ReadLine(); }); } 

用法示例:

  static void Main(string[] args) { AsyncContext.Run(async () => { CancellationTokenSource cancelSource = new CancellationTokenSource(); cancelSource.CancelAfter(1000); Console.WriteLine(await ReadLineAsync(cancelSource.Token) ?? "null"); }); }