如何添加一个超时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()之外的其他功能。
- 支持删除,退格键,箭头键等
- 能够按下“向上”键并重复上一个命令(如果你实现了一个后台调试控制台,这个控制台非常有用)。
我的解决方案如下:
- 产生一个单独的线程来处理使用Console.ReadLine()的用户输入。
- 超时之后,通过使用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项目提供了一个示例用户对话框实现。;
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"); }); }