如何向控制台添加超时?

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

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

67319 次浏览

我认为您需要在控制台上创建一个辅助线程并轮询一个键。我不知道有什么方法可以做到这一点。

这种使用 控制台,钥匙可用的方法有帮助吗?

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”
  • 呼叫系统。控制台。OpenStandardInput 获取输入流。指定将存储其数据并设置 evt 的回调方法。
  • 调用该流的 BeginRead 方法以启动异步读取操作
  • 然后在 ManualResetEvent 上输入定时等待
  • 如果等待超时,则取消读操作

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

另一种获得第二个线程的廉价方法是将它包装在一个委托中。

EDIT : 通过让实际工作在一个单独的进程中完成来修复问题,如果超时,则终止该进程。详情见下文。呼!

试了一下,看起来效果不错。我的同事有一个使用 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);
}
}
}
}

Exe 项目是一个非常简单的项目,它有一个类,看起来是这样的:

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

呼叫控制台。委托中的 ReadLine ()是不好的,因为如果用户没有点击‘ 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 调用将继续阻塞,超时最终将过期,返回的值将再次成为默认值。

顺便说一下-在上面的代码中有一个 bug。通过调用 waitHandle。Close ()从工作线程下关闭事件。如果用户在超时过期后点击“ Enter”,工作线程将尝试向引发 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);
}

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

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

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

或者顶部的静态字符串来获取整个行。

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

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

我的情况下,这工作很好:

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

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

  • 支持删除、退格、箭头键等。
  • 按下“ up”键并重复最后一个命令的能力(如果您实现了大量使用的后台调试控制台,这将非常方便)。

若要添加超时,请更改 while 循环以适应。

这招对我很管用。

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

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

到目前为止,大多数解决方案的问题在于它们依赖于控制台以外的东西。ReadLine ()和控制台。ReadLine ()有很多优点:

  • 支持删除、退格、箭头键等。
  • 按下“ up”键并重复最后一个命令的能力(如果您实现了大量使用的后台调试控制台,这将非常方便)。

我的解决办法如下:

  1. 生成一个 分开的线来使用 Console. ReadLine ()处理用户输入。
  2. 超时后,通过使用 http://inputsimulator.codeplex.com/向当前控制台窗口发送一个[ enter ]键来解除 Console. ReadLine ()的阻塞。

示例代码:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

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

NET 调用发送[输入]击键到当前进程,这是一个控制台应用程序?

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

这不是很简短吗?

if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout))
{
ConsoleKeyInfo keyInfo = Console.ReadKey();


// Handle keyInfo value here...
}

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

请不要因为我在过多的现有答案中添加了另一个解决方案而恨我!这对控制台有效。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);
}

我惊讶地发现,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之前应该进行同步。

下面是一个使用 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");
}

NET 4通过使用 Tasks 使这个过程变得非常简单。

首先,构建你的帮手:

   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 功能或执行其他危险的黑客行为来使其工作。任务让我们用一种非常自然的方式来解决问题。

更现代的基于任务的代码应该是这样的:

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 获取 stdin/stdout。 为了在完成工作时保持流程不结束,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/igorium/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);

这似乎是不使用任何本地 API 的最简单、可行的解决方案:

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

如果使用 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;

似乎这里还没有足够的答案: 0) ,下面的内容封装成了上面(第一个)的静态方法@kwl 的解决方案。

    public static string ConsoleReadLineWithTimeout(TimeSpan timeout)
{
Task<string> task = Task.Factory.StartNew(Console.ReadLine);


string result = Task.WaitAny(new Task[] { task }, timeout) == 0
? task.Result
: string.Empty;
return result;
}

用法

    static void Main()
{
Console.WriteLine("howdy");
string result = ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(8.5));
Console.WriteLine("bye");
}

我的代码完全基于朋友的回答@JSQuareD

但我需要使用 Stopwatch定时器,因为当我完成与 Console.ReadKey()的程序,它仍然在等待 Console.ReadLine()和它产生意外的行为。

它对我来说工作得非常好。维护原始的控制台

class Program
{
static void Main(string[] args)
{
Console.WriteLine("What is the answer? (5 secs.)");
try
{
var answer = ConsoleReadLine.ReadLine(5000);
Console.WriteLine("Answer is: {0}", answer);
}
catch
{
Console.WriteLine("No answer");
}
Console.ReadKey();
}
}


class ConsoleReadLine
{
private static string inputLast;
private static Thread inputThread = new Thread(inputThreadAction) { IsBackground = true };
private static AutoResetEvent inputGet = new AutoResetEvent(false);
private static AutoResetEvent inputGot = new AutoResetEvent(false);


static ConsoleReadLine()
{
inputThread.Start();
}


private static void inputThreadAction()
{
while (true)
{
inputGet.WaitOne();
inputLast = Console.ReadLine();
inputGot.Set();
}
}


// omit the parameter to read a line without a timeout
public static string ReadLine(int timeout = Timeout.Infinite)
{
if (timeout == Timeout.Infinite)
{
return Console.ReadLine();
}
else
{
var stopwatch = new Stopwatch();
stopwatch.Start();


while (stopwatch.ElapsedMilliseconds < timeout && !Console.KeyAvailable) ;


if (Console.KeyAvailable)
{
inputGet.Set();
inputGot.WaitOne();
return inputLast;
}
else
{
throw new TimeoutException("User did not provide input within the timelimit.");
}
}
}
}

我有一个使用 Windows API 解决这个问题的解决方案,它比这里的许多解决方案都有一些好处:

  • 使用 Console. ReadLine 检索输入,因此您可以获得与之相关的所有细节(输入历史等)
  • 强迫控制台。超时后完成的 ReadLine 调用,这样就不会为每个超时的调用累积新线程。
  • 不会不安全地中止线程。
  • 不像输入伪造方法那样有焦点问题。

两个主要缺点:

  • 只能在 Windows 上使用。
  • 这很复杂。

基本思想是 Windows API 有一个函数来取消未处理的 I/O 请求: 取消约会。当你用它取消在 STDIN 的行动时。ReadLine 引发 OperationCanceledException。

所以你要这么做:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;


namespace ConsoleHelper
{
public static class ConsoleHelper
{
public static string ReadLine(TimeSpan timeout)
{
return ReadLine(Task.Delay(timeout));
}


public static string ReadLine(Task cancel_trigger)
{
var status = new Status();


var cancel_task = Task.Run(async () =>
{
await cancel_trigger;


status.Mutex.WaitOne();
bool io_done = status.IODone;
if (!io_done)
status.CancellationStarted = true;
status.Mutex.ReleaseMutex();


while (!status.IODone)
{
var success = CancelStdIn(out int error_code);


if (!success && error_code != 0x490) // 0x490 is what happens when you call cancel and there is not a pending I/O request
throw new Exception($"Canceling IO operation on StdIn failed with error {error_code} ({error_code:x})");
}
});


ReadLineWithStatus(out string input, out bool read_canceled);
            

if (!read_canceled)
{
status.Mutex.WaitOne();
bool must_wait = status.CancellationStarted;
status.IODone = true;
status.Mutex.ReleaseMutex();


if (must_wait)
cancel_task.Wait();


return input;
}
else // read_canceled == true
{
status.Mutex.WaitOne();
bool cancel_started = status.CancellationStarted;
status.IODone = true;
status.Mutex.ReleaseMutex();


if (!cancel_started)
throw new Exception("Received cancelation not triggered by this method.");
else
cancel_task.Wait();


return null;
}
}


private const int STD_INPUT_HANDLE = -10;


[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetStdHandle(int nStdHandle);


[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CancelIoEx(IntPtr handle, IntPtr lpOverlapped);




private static bool CancelStdIn(out int error_code)
{
var handle = GetStdHandle(STD_INPUT_HANDLE);
bool success = CancelIoEx(handle, IntPtr.Zero);


if (success)
{
error_code = 0;
return true;
}
else
{
var rc = Marshal.GetLastWin32Error();
error_code = rc;
return false;
}
}


private class Status
{
public Mutex Mutex = new Mutex(false);
public volatile bool IODone;
public volatile bool CancellationStarted;
}


private static void ReadLineWithStatus(out string result, out bool operation_canceled)
{
try
{
result = Console.ReadLine();
operation_canceled = false;
}
catch (OperationCanceledException)
{
result = null;
operation_canceled = true;
}
}
}
}


避免简化这一点的诱惑,获得正确的线程是相当棘手的。你需要处理所有这些案子:

  • Console.ReadLine启动之前触发 Cancel 并调用 CancelStdIn(这就是为什么需要 cancel_trigger中的循环)。
  • ReadLine 在取消被触发之前返回(可能在很久之前)。
  • 在触发取消之后但在调用 CancelStdIn之前,Console. ReadLine 返回。
  • 由于调用 CancelStdIn以响应取消触发器,Console. ReadLine 引发异常。

图片来源: CancelIoEx 的想法来自于一个从 杰拉尔德 · 巴雷的博客得到它的 回答我。然而,这些解决方案有微妙的并发错误。