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

最简单的方法是什么?

#1 楼

令我惊讶的是,五年后,所有答案仍然遭受以下一个或多个问题的困扰:


使用了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。我认为我可以固定在创建自动事件时启动线程的时间。

–奥古斯托·佩德拉萨(Augusto Pedraza)
2014年8月7日在18:58

@JSQuareD我不认为一个带有Sleep(200ms)的繁忙等待会带来很多可怕的浪费,但是您的信号传输当然是出色的。同样,在第二种威胁中在无限循环中使用一个阻塞的Console.ReadLine调用,可以防止出现许多此类调用在后台挂起的问题,就像下面另一个在其他方面提出的严重支持的解决方案一样。感谢您分享您的代码。 +1

–罗兰
15年3月4日在17:20

如果您无法及时输入,则此方法似乎在您进行的第一个后续Console.ReadLine()调用上中断。您最终需要首先完成“虚拟” ReadLine。

–德里克
16年8月4日在22:13

@Derek不幸的是,您不能将此方法与正常的ReadLine调用混合使用,所有调用必须通过Reader进行。解决此问题的方法是向阅读器添加一个方法,该方法等待getInput而不超时。我目前在移动设备上,因此无法轻松将其添加到答案中。

– JSQuareD
16年8月6日在9:04

我看不到需要getInput。

– silvalli
18年7月29日在15:39

#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()的所有功能,例如按下“ up”键以使用退格键来获取先前键入的命令,方向键等

– Contango
2011年11月17日9:59



@Gravitas:这不起作用。好吧,它可以工作一次。但是您调用的每个ReadLine都坐在那里等待输入。如果您调用它100次,它将创建100个线程,直到您按Enter 100次后,所有线程才会消失!

–加布
2011-12-19 19:17

谨防。这个解决方案看起来很整洁,但是我最终留下了数千个未完成的呼叫。因此,如果反复调用不适合。

–汤姆·马金(Tom Makin)
2012年12月3日在22:09

@ Gabe,shakinfree:解决方案不考虑多个调用,而只是一个具有超时的异步调用。我想让用户在控制台上打印10条消息,然后按各自的顺序一一输入输入内容将使用户感到困惑。不过,对于挂起的电话,您可以尝试注释TimedoutException行并返回null / empty字符串吗?

– gp。
2012-12-10 12:16



是的,问题出在Console.ReadLine仍然阻止了正在从ReadLineDelegate运行Console.ReadLine方法的线程池线程。

– gp。
2012-12-10 13:06

#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似乎确实想要一个阻塞的调用……这可能是一个更好的解决方案。

–GEOCHET
08年9月11日在21:17

我相信你已经看到了。从快速的Googlesocial.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…获得了它

– Gulzar Nazim
08年9月11日在21:20

如果用户不执行任何操作,我将看不到此“超时”。所有可能要做的就是在后台继续执行逻辑,直到按下某个键并且其他逻辑继续。

– mphair
2010-2-17在16:51

是的,这需要解决。但是很容易将超时添加到循环条件中。

–乔纳森·艾伦
13年8月17日在21:36



KeyAvailable仅指示用户已开始向ReadLine输入输入,但是我们需要按Enter的事件,该事件使ReadLine返回。该解决方案仅适用于ReadKey,即仅获得一个字符。由于这不能解决ReadLine的实际问题,因此我无法使用您的解决方案。 -1对不起

–罗兰
15年3月4日在17:25

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


评论


我认为这是使用内置工具的最好,最简单的解决方案。很好!

– Vippy
15年9月30日在17:08

美丽!确实,简单是终极的尖端。恭喜!

– BrunoSalvino
18-2-25的2:45

#5 楼

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


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

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

评论


这或多或少是“接受的解决方案”所做的。

–罗兰
15年3月4日在17:26

#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 楼

我认为您将需要创建一个辅助线程并在控制台上轮询密钥。我知道没有内置的方法可以实现这一目标。

评论


是的,如果您有第二个线程对密钥进行轮询,并且您的应用程序在等待期间关闭,则该密钥轮询线程将一直坐在那里,等待永远。

–凯利·埃尔顿(Kelly Elton)
2011-2-24在21:37

实际上:是第二个线程,还是带有“ BeginInvoke”的委托(在后台使用了一个线程,请参阅@gp的答案)。

– Contango
2011年11月17日14:34



@ kelton52,如果您在任务管理器中结束进程,辅助线程会退出吗?

– Arlen Beiler
2012年6月9日下午0:09

#8 楼

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

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


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

我的解决方案如下:


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

示例代码:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);


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

.NET调用将[enter]击键发送到当前进程是控制台应用程序?

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

#9 楼

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


#10 楼

在委托中调用Console.ReadLine()不好,因为如果用户未点击“ enter”,则该调用将永远不会返回。执行委托的线程将被阻塞,直到用户单击“ enter”为止,而无法取消它。

发出一系列这样的调用将不会像您期望的那样工作。请考虑以下内容(使用上面的示例控制台类):

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调用将继续阻塞,超时最终将到期,并且返回的值将再次为默认值。

BTW-上面的代码中有错误。通过调用waitHandle.Close(),可以从工作线程下关闭事件。如果用户在超时到期后单击“ enter”,则工作线程将尝试发信号通知引发ObjectDisposedException的事件。从工作线程引发异常,并且如果您尚未设置未处理的异常处理程序,则您的进程将终止。

评论


您帖子中的“上方”一词含糊不清且令人困惑。如果您指的是其他答案,则应正确链接到该答案。

– bzlm
08年11月6日在10:02

#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 楼

不幸的是,我无法评论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()。

–杰米·基特森(Jamie Kitson)
2010年6月3日,16:26

#14 楼

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

只需运行一下,它看起来就很好了。我的同事有一个使用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()阻塞线程的问题。

– bzlm
08年11月6日在10:17

然后告诉微软,微软使我们处于这个位置。

–Jesse C. Slicer
08年11月6日14:53

微软没有把你放在那个位置。看看几行可以完成相同工作的其他答案。我认为上面的代码应该获得某种奖励-但不是您想要的那种:)

– Contango
2011年9月6日在11:42



不,没有其他答案完全符合OP的要求。它们全部都失去了标准输入例程的功能,或者由于对Console.ReadLine()的所有请求都处于阻塞状态并将在下一个请求时保持输入的事实而挂断了电话。可接受的答案相当接近,但仍然有局限性。

–Jesse C. Slicer
2011年9月6日12:21

嗯,不,不是。输入缓冲区仍会阻塞(即使程序没有阻塞)。自己尝试:输入一些字符,但不要按Enter。让它超时。在调用方中捕获异常。然后,在调用该程序后在程序中添加另一个ReadLine()。走着瞧吧。由于控制台的单线程特性,您必须单击return TWICE才能使它运行。它。不。工作。

–Jesse C. Slicer
2011年11月17日下午13:33

#15 楼

.NET 4使用“任务”使此操作变得异常简单。

首先,构建您的助手:

   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功能或执行其他危险的操作来使此工作正常进行。任务使我们能够非常自然地解决问题。

#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到底是什么?

– John Ktejik
2014年2月25日在22:12

#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 楼

我的代码完全基于朋友的回答@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.");
            }
        }
    }
}


#21 楼

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

#22 楼

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


#23 楼

string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");


请注意,如果您沿“ Console.ReadKey”路线行驶,则会失去ReadLine的一些出色功能,即:


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

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

#24 楼

请不要讨厌我为现有答案的过多添加另一种解决方案!这适用于Console.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);
}


#25 楼

这是使用Console.KeyAvailable的解决方案。这些阻塞了调用,但是如果需要的话,通过TPL异步调用它们应该是很简单的。我使用了标准的取消机制,以使其易于与Task Asynchronous Pattern和所有这些好东西连接。

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提供的标准导航功能(上/下箭头滚动等)。
如果特殊输入,这会将'\ 0'字符注入到输入中按键(F1,PrtScn等)。您可以通过修改代码轻松地将它们过滤掉。


#26 楼

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

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


#27 楼

我来到这个答案,最后做:

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


#28 楼

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


评论


如果用户按下按键并在2000ms内放开怎么办?

–晕
16-3-29在14:29

如果您在2秒钟之前按下,则该按钮将不起作用,因为它处于睡眠模式。

– Marc Dirven
11月10日14:45

#29 楼

更现代的,基于任务的代码如下所示:

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


#30 楼

我有一个Windows应用程序(Windows服务)的特殊情况。以交互方式运行程序Environment.IsInteractive(VS调试器或从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();
  }    
}