131

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

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

4

33 回答 33

121

令我惊讶的是,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 的调用会一直处于活动状态。调用该函数将始终导致最新的输入或超时,并且用户不必多次按 Enter 来提交他的输入。
  • 而且,显然,该功能不依赖于忙等待。相反,它使用适当的多线程技术来防止资源浪费。

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

于 2013-08-20T18:14:39.590 回答
33
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();
于 2010-01-11T11:30:36.787 回答
28

这种使用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);
    }
}
于 2008-09-11T21:06:45.963 回答
13

这对我有用。

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);
于 2011-10-05T16:19:49.770 回答
10

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

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

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

于 2008-09-11T21:18:13.580 回答
9
// 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.
}
于 2010-08-28T16:41:21.763 回答
8

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

于 2008-09-11T21:02:09.110 回答
6

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

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

于 2008-09-11T22:09:41.943 回答
6

在找到一个在企业环境中完美运行的解决方案之前,我为这个问题苦苦挣扎了 5 个月。

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

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

我的解决方案如下:

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

示例代码:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

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

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

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

于 2012-01-26T10:39:11.013 回答
6

如果你在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;
于 2017-06-26T12:48:08.993 回答
4

我可能对这个问题读得太多了,但我假设等待类似于启动菜单,除非你按下一个键,否则它会等待 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);
}
于 2008-10-23T20:20:49.527 回答
4

好像这里还没有足够的答案: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");
    }
于 2017-09-14T18:49:32.100 回答
3

.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 来使其正常工作。任务让我们以非常自然的方式解决问题。

于 2016-01-12T16:37:52.693 回答
2

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

只是试了一下,它似乎工作得很好。我的同事有一个使用 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());
      }
   }
}
于 2008-09-11T21:30:38.697 回答
2

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

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();
于 2010-06-03T15:36:58.550 回答
1

解决此问题的简单线程示例

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

或顶部的静态字符串以获得整行。

于 2010-02-17T16:59:03.773 回答
1

我的情况是这样的:

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;
}
于 2010-06-04T12:29:21.657 回答
1

这不是很好而且很短吗?

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

    // Handle keyInfo value here...
}
于 2013-05-12T02:56:55.480 回答
1

这是 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!");
        }
    }
于 2013-07-13T07:22:39.090 回答
1

这是一个安全的解决方案,它在超时后伪造控制台输入以解除阻塞线程。 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);
于 2017-03-22T00:18:54.960 回答
1

我的代码完全基于朋友的回答@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.");
            }
        }
    }
}
于 2019-03-24T03:22:16.290 回答
1

我有一个使用 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;
            }
        }
    }
}

避免简化这一点的诱惑,让线程正确是非常棘手的。您需要处理所有这些情况:

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

致谢:从Gérald Barré 的博客中获得的SO 答案中获得了 CancelIoEx 的想法。然而,这些解决方案存在细微的并发错误。

于 2021-03-26T17:48:58.260 回答
0

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

于 2008-09-11T21:20:54.697 回答
0

上面 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
    }
}
于 2009-05-19T23:26:29.610 回答
0
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

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

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

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

于 2011-09-06T11:13:55.620 回答
0

请不要恨我为过多的现有答案添加另一个解决方案!这适用于 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);
}
于 2013-07-30T12:02:03.103 回答
0

这是一个使用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”字符注入输入。不过,您可以通过修改代码轻松过滤掉它们。
于 2013-10-11T14:49:44.730 回答
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;
    }
}
于 2013-12-17T10:41:09.813 回答
0

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

    /// <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);                
        }            
    }
于 2014-10-03T18:20:53.960 回答
0

一个简单的例子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");
}
于 2015-06-27T13:29:26.067 回答
0

更现代和基于任务的代码看起来像这样:

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();
}
于 2016-06-19T13:26:28.263 回答
0

我有一个独特的情况,即拥有一个 Windows 应用程序(Windows 服务)。当以交互方式运行程序时Environment.IsInteractive(VS 调试器或从 cmd.exe),我使用 AttachConsole/AllocConsole 来获取我的标准输入/标准输出。为了防止进程在工作完成时结束,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();
  }    
}
于 2016-08-25T21:06:07.150 回答
0

这似乎是最简单的工作解决方案,不使用任何本机 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");
        });
    }
于 2017-04-27T07:27:06.987 回答