我有一个控制台应用程序,我想在其中给用户x秒来响应提示。如果一定时间后没有输入,则应继续执行程序逻辑。我们假设超时意味着空响应。
最直接的方法是什么?
我惊讶地发现,经过5年后,所有答案仍然存在以下一个或多个问题:
我相信我的解决方案将解决原始问题,而且不会遭受以上任何问题:
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
并省略超时,以使其默认为无限超时。
那么其他解决方案的问题怎么样呢?
我预见到这个解决方案唯一的问题是它不是线程安全的。但是,多个线程实际上不能同时向用户请求输入,因此在调用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();
ReadLine
都会卡在那里等待输入。如果你调用它100次,它将创建100个线程,直到你按下100次回车键之前并不会全部消失! - GabeEndInvoke
调用的问题无法解决。另一方面,接受的解决方案在第二个线程中运行一个永久的同步调用是完美的。感谢您的努力,但我不能使用您的解决方案。-1 抱歉。 - Roland使用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);
}
}
KeyAvailable
只表示用户已经开始输入ReadLine的内容,但我们需要在按下Enter键时触发一个事件,使ReadLine返回。这个解决方案仅适用于ReadKey,即只获取一个字符。由于这不能解决ReadLine的实际问题,所以我无法使用你的解决方案。-1抱歉。 - Roland这对我起作用了。
ConsoleKeyInfo k = new ConsoleKeyInfo();
Console.WriteLine("Press any key in the next 5 seconds.");
for (int cnt = 5; cnt > 0; cnt--)
{
if (Console.KeyAvailable)
{
k = Console.ReadKey();
break;
}
else
{
Console.WriteLine(cnt.ToString());
System.Threading.Thread.Sleep(1000);
}
}
Console.WriteLine("The key pressed was " + k.Key);
如果你在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()
版本:
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;
无论如何,您确实需要第二个线程。您可以使用异步IO来避免声明自己的线程:
如果读取返回数据,则设置事件,您的主线程将继续执行,否则在超时后继续执行。
// 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.
}
我认为你需要创建一个次线程并轮询控制台上的按键。我不知道有没有内置的方法可以实现这个功能。
在企业环境中,我花了五个月的时间才找到了一个完美解决这个问题的方法。
目前大多数解决方案的问题在于它们依赖于除Console.ReadLine()之外的其他东西,而Console.ReadLine()具有许多优点:
我的解决方案如下:
示例代码:
InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);
关于这种技术的更多信息,包括正确的技术来中止使用Console.ReadLine的线程:
在委托中调用Console.ReadLine()是不好的,因为如果用户没有按下“回车”键,那么该调用将永远不会返回。执行委托的线程将被阻塞,直到用户按下“回车”键,无法取消它。
发出一系列这些调用将不会像您期望的那样工作。考虑以下示例(使用上面的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");
Console.ReadLine()
调用时,该方法似乎会出现错误。您最终会遇到一个需要先完成的“幽灵”ReadLine
。 - DerekgetInput
。 - silvalli