我有一个控制台应用程序,我想让用户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);
在这两种情况下,您都无法将呼叫Reader
与正常Console.ReadLine
呼叫混合:如果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 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帮助吗?
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/将[enter]键发送到当前控制台窗口来解锁Console.ReadLine().
示例代码:
InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);
有关此技术的更多信息,包括中止使用Console.ReadLine的线程的正确技术:
.NET调用将[enter]键发送到当前进程,这是一个控制台应用程序?
当所述线程正在执行Console.ReadLine时,如何在.NET中中止另一个线程?
在委托中调用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都将包含默认值。当用户单击“ enter”时,将完成第一个 ReadLine调用,但是代码已放弃该调用,并实际上丢弃了结果。在第二的ReadLine调用将继续阻止,超时最终将到期,返回的值将再次成为默认。
顺便说一句-上面的代码中有一个错误。通过调用waitHandle.Close(),可以从工作线程下关闭事件。如果用户在超时到期后单击“ enter”,则工作线程将尝试发信号通知引发ObjectDisposedException的事件。异常是从工作线程中抛出的,如果您尚未设置未处理的异常处理程序,则该过程将终止。