C# 如何向 Console.ReadLine() 添加超时?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/57615/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to add a Timeout to Console.ReadLine()?
提问by Larsenal
I have a console app in which I want to give the user xseconds to respond to the prompt. If no input is made after a certain period of time, program logic should continue. We assume a timeout means empty response.
我有一个控制台应用程序,我想在其中给用户x秒来响应提示。如果一段时间后没有输入,程序逻辑应该继续。我们假设超时意味着空响应。
What is the most straightforward way of approaching this?
解决这个问题的最直接方法是什么?
采纳答案by JSQuareD
I'm surprised to learn that after 5 years, all of the answers still suffer from one or more of the following problems:
我很惊讶地发现 5 年后,所有的答案仍然存在以下一个或多个问题:
- A function other than ReadLine is used, causing loss of functionality. (Delete/backspace/up-key for previous input).
- Function behaves badly when invoked multiple times (spawning multiple threads, many hanging ReadLine's, or otherwise unexpected behavior).
- Function relies on a busy-wait. Which is a horrible waste since the wait is expected to run anywhere from a number of seconds up to the timeout, which might be multiple minutes. A busy-wait which runs for such an ammount of time is a horrible suck of resources, which is especially bad in a multithreading scenario. If the busy-wait is modified with a sleep this has a negative effect on responsiveness, although I admit that this is probably not a huge problem.
- 使用了 ReadLine 以外的函数,导致功能丢失。(删除/退格/向上键用于先前的输入)。
- 函数在多次调用时表现不佳(产生多个线程、许多挂起的 ReadLine 或其他意外行为)。
- 函数依赖于忙等待。这是一种可怕的浪费,因为预计等待会在任何地方运行,从几秒钟到超时,可能是几分钟。运行这么长时间的忙等待是一种可怕的资源消耗,这在多线程场景中尤其糟糕。如果用睡眠修改忙等待,这会对响应产生负面影响,尽管我承认这可能不是一个大问题。
I believe my solution will solve the original problem without suffering from any of the above problems:
我相信我的解决方案将解决原始问题而不会遇到上述任何问题:
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.");
}
}
Calling is, of course, very easy:
当然,调用非常简单:
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.");
}
Alternatively, you can use the TryXX(out)
convention, as shmueli suggested:
或者,您可以使用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;
}
Which is called as follows:
其名称如下:
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);
In both cases, you cannot mix calls to Reader
with normal Console.ReadLine
calls: if the Reader
times out, there will be a hanging ReadLine
call. Instead, if you want to have a normal (non-timed) ReadLine
call, just use the Reader
and omit the timeout, so that it defaults to an infinite timeout.
在这两种情况下,您都不能将呼叫Reader
与普通Console.ReadLine
呼叫混合使用:如果Reader
超时,将挂起ReadLine
呼叫。相反,如果您想进行正常(非定时)ReadLine
调用,只需使用Reader
并省略超时,使其默认为无限超时。
So how about those problems of the other solutions I mentioned?
那么我提到的其他解决方案的那些问题呢?
- As you can see, ReadLine is used, avoiding the first problem.
- The function behaves properly when invoked multiple times. Regardless of whether a timeout occurs or not, only one background thread will ever be running and only at most one call to ReadLine will ever be active. Calling the function will always result in the latest input, or in a timeout, and the user won't have to hit enter more than once to submit his input.
- And, obviously, the function does not rely on a busy-wait. Instead it uses proper multithreading techniques to prevent wasting resources.
- 如您所见,使用了 ReadLine,避免了第一个问题。
- 该函数在多次调用时表现正常。无论是否发生超时,只有一个后台线程在运行,并且最多只有一个 ReadLine 调用处于活动状态。调用该函数将始终导致最新输入或超时,并且用户不必多次按 Enter 键即可提交输入。
- 而且,显然,该功能不依赖于忙等待。相反,它使用适当的多线程技术来防止浪费资源。
The only problem that I foresee with this solution is that it is not thread-safe. However, multiple threads can't really ask the user for input at the same time, so synchronization should be happening before making a call to Reader.ReadLine
anyway.
我预见到此解决方案的唯一问题是它不是线程安全的。但是,多个线程并不能真正同时要求用户输入,因此Reader.ReadLine
无论如何都应该在调用之前进行同步。
回答by GEOCHET
I think you will need to make a secondary thread and poll for a key on the console. I know of no built in way to accomplish this.
我认为您需要创建一个辅助线程并轮询控制台上的密钥。我知道没有内置的方法来实现这一点。
回答by Gulzar Nazim
Will this approach using Console.KeyAvailablehelp?
这种使用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);
}
}
回答by Eric
One way or another you do need a second thread. You could use asynchronous IO to avoid declaring your own:
无论如何,您确实需要第二个线程。您可以使用异步 IO 来避免声明自己的:
- declare a ManualResetEvent, call it "evt"
- call System.Console.OpenStandardInput to get the input stream. Specify a callback method that will store its data and set evt.
- call that stream's BeginRead method to start an asynchronous read operation
- then enter a timed wait on a ManualResetEvent
- if the wait times out, then cancel the read
- 声明一个 ManualResetEvent,称之为“evt”
- 调用 System.Console.OpenStandardInput 获取输入流。指定将存储其数据并设置 evt 的回调方法。
- 调用该流的 BeginRead 方法以启动异步读取操作
- 然后在 ManualResetEvent 上输入定时等待
- 如果等待超时,则取消读取
If the read returns data, set the event and your main thread will continue, otherwise you'll continue after the timeout.
如果读取返回数据,则设置事件并且您的主线程将继续,否则您将在超时后继续。
回答by Joel Coehoorn
Another cheap way to get a 2nd thread is to wrap it in a delegate.
另一种获得第二个线程的廉价方法是将它包装在一个委托中。
回答by Jesse C. Slicer
EDIT: fixed the problem by having the actual work be done in a separate process and killing that process if it times out. See below for details. Whew!
编辑:通过在单独的进程中完成实际工作并在超时时终止该进程来解决问题。详情请见下文。哇!
Just gave this a run and it seemed to work nicely. My coworker had a version which used a Thread object, but I find the BeginInvoke() method of delegate types to be a bit more elegant.
只是试了一下,它似乎工作得很好。我的同事有一个使用 Thread 对象的版本,但我发现委托类型的 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);
}
}
}
}
The ReadLine.exe project is a very simple one which has one class which looks like so:
ReadLine.exe 项目是一个非常简单的项目,它有一个类,如下所示:
namespace ReadLine
{
internal static class Program
{
private static void Main()
{
System.Console.WriteLine(System.Console.ReadLine());
}
}
}
回答by Brannon
Calling Console.ReadLine() in the delegate is bad because if the user doesn't hit 'enter' then that call will never return. The thread executing the delegate will be blocked until the user hits 'enter', with no way to cancel it.
在委托中调用 Console.ReadLine() 是不好的,因为如果用户没有点击“输入”,那么该调用将永远不会返回。执行委托的线程将被阻塞,直到用户点击“输入”,无法取消它。
Issuing a sequence of these calls will not behave as you would expect. Consider the following (using the example Console class from above):
发出一系列这些调用的行为不会像您预期的那样。考虑以下(使用上面的示例 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");
The user lets the timeout expire for the first prompt, then enters a value for the second prompt. Both firstName and lastName will contain the default values. When the user hits 'enter', the firstReadLine call will complete, but the code has abandonded that call and essentially discarded the result. The secondReadLine call will continue to block, the timeout will eventually expire and the value returned will again be the default.
用户让第一个提示的超时到期,然后为第二个提示输入一个值。firstName 和 lastName 都将包含默认值。当用户点击“回车”时,第一个ReadLine 调用将完成,但代码已放弃该调用并基本上放弃了结果。在第二的ReadLine调用将继续阻止,超时最终将到期,返回的值将再次成为默认。
BTW- There is a bug in the code above. By calling waitHandle.Close() you close the event out from under the worker thread. If the user hits 'enter' after the timeout expires, the worker thread will attempt to signal the event which throws an ObjectDisposedException. The exception is thrown from the worker thread, and if you haven't setup an unhandled exception handler your process will terminate.
顺便说一句-上面的代码中有一个错误。通过调用waitHandle.Close(),您可以从工作线程下关闭事件。如果用户在超时到期后点击“输入”,则工作线程将尝试向引发 ObjectDisposedException 的事件发出信号。异常是从工作线程抛出的,如果您还没有设置未处理的异常处理程序,您的进程将终止。
回答by Ryan
I may be reading too much into the question, but I am assuming the wait would be similar to the boot menu where it waits 15 seconds unless you press a key. You could either use (1) a blocking function or (2) you could use a thread, an event, and a timer. The event would act as a 'continue' and would block until either the timer expired or a key was pressed.
我可能对这个问题读得太多了,但我假设等待类似于启动菜单,除非你按下一个键,否则它会等待 15 秒。您可以使用 (1) 阻塞函数或 (2) 您可以使用线程、事件和计时器。该事件将作为“继续”并阻塞,直到计时器到期或按键被按下。
Pseudo-code for (1) would be:
(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);
}
回答by Ryan
Example implementation of Eric's post above. This particular example was used to read information that was passed to a console app via pipe:
上面 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
}
}
回答by gp.
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();
回答by mphair
Simple threading example to solve this
解决这个问题的简单线程示例
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();
}
or a static string up top for getting an entire line.
或顶部的静态字符串以获得整条线。