如何向 Console.ReadLine() 添加超时?

Posted

技术标签:

【中文标题】如何向 Console.ReadLine() 添加超时?【英文标题】:How to add a Timeout to Console.ReadLine()? 【发布时间】:2008-09-11 20:55:57 【问题描述】:

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

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

【问题讨论】:

【参考方案1】:

我惊讶地发现,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.");

或者,您可以按照 shmueli 的建议使用 TryXX(out) 约定:

  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 的调用会一直处于活动状态。调用该函数将始终导致最新输入或超时,并且用户无需多次按 Enter 即可提交其输入。 而且,很明显,该函数不依赖忙等待。相反,它使用适当的多线程技术来防止资源浪费。

我预见到这个解决方案的唯一问题是它不是线程安全的。但是,多个线程并不能真正同时要求用户输入,因此无论如何都应该在调用Reader.ReadLine 之前进行同步。

【讨论】:

我在这段代码之后得到了 NullReferenceException。我认为我可以在创建自动事件时修复启动线程。 @JSQuareD 我不认为带有睡眠(200 毫秒)的忙等待是 horrible waste 的大部分,但当然你的信号是优越的。此外,在第二个威胁中使用无限循环中的一个阻塞 Console.ReadLine 调用可以防止大量此类调用在后台徘徊的问题,就像在下面的另一个被大力支持的解决方案中一样。感谢您分享您的代码。 +1 如果您未能及时输入,此方法似乎会在您随后的第一个Console.ReadLine() 调用时中断。你最终会得到一个需要先完成的“幻象”ReadLine @Derek 不幸的是,您不能将此方法与普通的 ReadLine 调用混合使用,所有调用都必须通过 Reader 进行。解决这个问题的方法是在没有超时的情况下等待 gotInput 的阅读器中添加一个方法。我目前正在使用移动设备,因此无法轻松将其添加到答案中。 我认为不需要getInput【参考方案2】:
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();

【讨论】:

我不知道为什么这没有被投票 - 它绝对完美无瑕。许多其他解决方案都涉及“ReadKey()”,它无法正常工作:这意味着您失去了 ReadLine() 的所有功能,例如按“向上”键以获取先前键入的命令,使用退格键和方向键等 @Gravitas:这行不通。嗯,它工作一次。但是您呼叫的每个ReadLine 都坐在那里等待输入。如果你调用它 100 次,它会创建 100 个线程,直到你按 Enter 100 次才会全部消失! 当心。这个解决方案看起来很简洁,但我最终留下了 1000 个未完成的呼叫。所以不适合反复调用。 @Gabe, shakinfree:解决方案没有考虑多次调用,而只考虑了一次超时异步调用。我想在控制台上打印 10 条消息然后按各自的顺序一一输入输入会让用户感到困惑。尽管如此,对于挂起的呼叫,您能否尝试评论 TimedoutException 行并返回 null/空字符串? nope... 问题是 Console.ReadLine 仍然阻塞从 ReadLineDelegate 运行 Console.ReadLine 方法的线程池线程。【参考方案3】:

这种使用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);
    

【讨论】:

这是真的,OP 似乎确实想要一个阻塞调用,虽然我一想到有点不寒而栗......这可能是一个更好的解决方案。 我相信你已经看到了。从快速谷歌social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/… 得到它 如果用户什么都不做,我看不到这个“超时”。所有这一切都可能会在后台继续执行逻辑,直到按下一个键并且其他逻辑继续。 没错,这需要修复。但是将超时添加到循环条件中很容易。 KeyAvailable 仅表示用户已开始向 ReadLine 输入输入,但我们需要在按下 Enter 时发生事件,以使 ReadLine 返回。此解决方案仅适用于 ReadKey,即仅获取一个字符。由于这不能解决 ReadLine 的实际问题,因此我无法使用您的解决方案。 -1 抱歉【参考方案4】:

这对我有用。

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】:

您确实需要第二个线程。您可以使用异步 IO 来避免声明自己的:

声明一个ManualResetEvent,称之为“evt” 调用 System.Console.OpenStandardInput 获取输入流。指定将存储其数据并设置 evt 的回调方法。 调用该流的 BeginRead 方法以启动异步读取操作 然后在 ManualResetEvent 上输入定时等待 如果等待超时,则取消读取

如果读取返回数据,设置事件,你的主线程会继续,否则超时后你会继续。

【讨论】:

这或多或少是 Accepted Solution 的作用。【参考方案6】:
// 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.

【讨论】:

【参考方案7】:

我认为您需要创建一个辅助线程并在控制台上轮询一个键。我知道没有内置的方法来实现这一点。

【讨论】:

是的,如果您有第二个线程轮询密钥,并且您的应用程序在它坐在那里等待时关闭,那么该密钥轮询线程将永远坐在那里等待。 实际上:要么是第二个线程,要么是带有“BeginInvoke”的委托(在幕后使用线程 - 请参阅@gp 的答案)。 @kelton52,如果在任务管理器中结束进程,辅助线程会退出吗?【参考方案8】:

在委托中调用 Console.ReadLine() 是不好的,因为如果用户没有点击“输入”,那么该调用将永远不会返回。执行委托的线程将被阻塞,直到用户点击“输入”,无法取消它。

发出这些调用的序列不会像您预期的那样运行。考虑以下(使用上面的示例控制台类):

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 都将包含默认值。当用户点击“输入”时,first ReadLine 调用将完成,但代码已放弃该调用并基本上放弃了结果。 ReadLine 调用将继续阻塞,超时最终将到期,返回的值将再次成为默认值。

顺便说一句-上面的代码中有一个错误。通过调用 waitHandle.Close() 您可以从工作线程下关闭事件。如果用户在超时到期后点击“输入”,工作线程将尝试发出引发 ObjectDisposedException 的事件。异常是从工作线程抛出的,如果您没有设置未处理的异常处理程序,您的进程将终止。

【讨论】:

您帖子中的“以上”一词模棱两可且令人困惑。如果您指的是另一个答案,您应该建立一个指向该答案的正确链接。【参考方案9】:

我在这个问题上苦苦挣扎了 5 个月,然后才找到在企业环境中完美运行的解决方案。

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

支持删除、退格、方向键等 能够按“向上”键并重复上一个命令(如果您实现了一个经常使用的后台调试控制台,这将非常方便)。

我的解决方法如下:

    使用 Console.ReadLine() 生成一个单独的线程来处理用户输入。 在超时期限之后,通过使用http://inputsimulator.codeplex.com/ 向当前控制台窗口发送 [enter] 键来解除对 Console.ReadLine() 的阻止。

示例代码:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

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

.NET call to send [enter] keystroke into the current process, which is a console app?

How to abort another thread in .NET, when said thread is executing Console.ReadLine?

【讨论】:

【参考方案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() 版本:

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;

【讨论】:

【参考方案11】:

我可能对这个问题读得太多了,但我假设等待类似于引导菜单,除非你按下一个键,否则它会等待 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);

【讨论】:

【参考方案12】:

好像这里还没有足够的答案: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");
    

【讨论】:

【参考方案13】:

.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 功能或执行其他危险的 hack 来使其正常工作。任务让我们以非常自然的方式解决问题。

【讨论】:

【参考方案14】:

很遗憾,我无法对 Gulzar 的帖子发表评论,但这里有一个更完整的例子:

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

【讨论】:

请注意,如果控制台不可见(?)或输入来自文件,您也可以使用 Console.In.Peek()。【参考方案15】:

编辑:通过在单独的进程中完成实际工作并在超时时终止该进程来解决问题。详情见下文。呼!

刚刚试了一下,它似乎工作得很好。我的同事有一个使用 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);
         
      
   

ReadLine.exe 项目是一个非常简单的项目,它有一个类,如下所示:

namespace ReadLine

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

【讨论】:

在一个新进程中调用一个单独的可执行文件只是为了做一个定时的 ReadLine() 听起来像是大材小用。您实际上是通过设置和拆除整个进程来解决无法中止 ReadLine() 阻塞线程的问题。 然后告诉微软,他们把我们置于这个位置。 微软没有把你放在那个位置。看看其他一些在几行中完成相同工作的答案。我认为上面的代码应该得到某种奖励 - 但不是你想要的那种:) 不,其他答案都没有完全符合 OP 的要求。他们都失去了标准输入例程的功能,或者被所有对Console.ReadLine()的请求阻塞并在下一个请求时保留输入这一事实挂断了。接受的答案相当接近,但仍有局限性。 嗯,不,不是。输入缓冲区仍然阻塞(即使程序没有)。自己尝试一下:输入一些字符,但不要按 Enter。让它超时。在调用者中捕获异常。然后在你的程序中调用另一个ReadLine()。走着瞧吧。由于Console 的单线程性质,您必须按两次返回才能运行它。它。没有。工作。【参考方案16】:

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

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

或者是一个静态字符串,用于获取整行。

【讨论】:

【参考方案17】:

我的情况是这样的:

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;

【讨论】:

【参考方案18】:

这不是很好很短吗?

if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout))

    ConsoleKeyInfo keyInfo = Console.ReadKey();

    // Handle keyInfo value here...

【讨论】:

SpinWait 到底是什么?【参考方案19】:

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

【讨论】:

【参考方案20】:

这是一个安全的解决方案,它在超时后伪造控制台输入以解除阻塞线程。 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);

【讨论】:

【参考方案21】:

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

但我需要使用Stopwatch 来计时,因为当我使用Console.ReadKey() 完成程序时,它仍在等待Console.ReadLine(),并且会产生意外行为。

它非常适合我。保持原来的 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.");
            
        
    

【讨论】:

【参考方案22】:

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

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

两个主要缺点:

仅适用于 Windows。 这很复杂。

基本思想是 Windows API 具有取消未完成的 I/O 请求的功能:CancelIoEx。当您使用它取消对 STDIN 的操作时,Console.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 开始之前调用CancelStdIn(这就是为什么需要cancel_trigger 中的循环)。 Console.ReadLine 在触发取消之前返回(可能很久之前)。 Console.ReadLine 在触发取消之后但在调用 CancelStdIn 之前返回。 Console.ReadLine 因调用 CancelStdIn 以响应取消触发器而引发异常。

学分: 从 SO answer 那里得到了 CancelIoEx 的想法,他从 Gérald Barré's blog 得到它。然而,这些解决方案存在细微的并发错误。

【讨论】:

【参考方案23】:

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

【讨论】:

【参考方案24】:

上面 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
    

【讨论】:

【参考方案25】:
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    
        readline = Console.ReadLine();
    
);
do

    Thread.Sleep(100);
 while (readline == "?");

请注意,如果您走“Console.ReadKey”路线,您将失去 ReadLine 的一些很酷的功能,即:

支持删除、退格、方向键等 能够按“向上”键并重复上一个命令(如果您实现了一个经常使用的后台调试控制台,这将非常方便)。

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

【讨论】:

【参考方案26】:

请不要恨我为过多的现有答案添加了另一个解决方案!这适用于 Console.ReadKey(),但可以轻松修改为与 ReadLine() 等一起使用。

由于“Console.Read”方法处于阻塞状态,因此需要“nudge”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);

【讨论】:

【参考方案27】:

这是一个使用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”字符注入输入。不过,您可以通过修改代码轻松过滤掉它们。

【讨论】:

【参考方案28】:

因为问了一个重复的问题,所以在这里结束。我想出了以下看起来很简单的解决方案。我确信它有一些我错过的缺点。

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;
    

【讨论】:

【参考方案29】:

我得到了这个答案并最终做了:

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

【讨论】:

【参考方案30】:

一个使用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");

【讨论】:

如果用户在 2000 毫秒内按下按键并松开怎么办? 如果在2秒之前按这个是不行的,因为它处于睡眠模式。

以上是关于如何向 Console.ReadLine() 添加超时?的主要内容,如果未能解决你的问题,请参考以下文章

read()与readLine()有啥不同; write()与writeLine()有啥不同

如何在 Linux 下使用 MonoDevelop 的项目中使用 Console.ReadLine()

如何在简单计算器中将Console.ReadLine()转换为两倍?问题C#,例外情况

Console.Read() 和 Console.ReadLine() 之间的区别?

Console.ReadLine(); 获取用户输入内容

重载 Console.ReadLine 可能吗? (或任何静态类方法)