Вопрос

У меня есть консольное приложение, в котором я хочу предоставить пользователю Икс секунд, чтобы ответить на запрос.Если в течение определенного периода времени ввод не производится, логика программы должна продолжаться.Мы предполагаем, что тайм-аут означает пустой ответ.

Каков самый простой способ приблизиться к этому?

Это было полезно?

Решение

Я удивлен, узнав, что спустя 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) соглашение, как предложил Шмуэли:

  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 в любом случае.

Другие советы

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

Будет ли этот подход использовать 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);
    }
}

Так или иначе, вам нужен второй поток.Вы можете использовать асинхронный ввод-вывод, чтобы не объявлять свой собственный:

  • объявите ManualResetEvent, назовите его «evt»
  • вызовите System.Console.OpenStandardInput, чтобы получить входной поток.Укажите метод обратного вызова, который будет хранить свои данные, и установите evt.
  • вызовите метод BeginRead этого потока, чтобы начать асинхронную операцию чтения
  • затем введите запланированное ожидание для ManualResetEvent
  • если время ожидания истекло, отмените чтение

Если чтение возвращает данные, установите событие, и ваш основной поток продолжится, в противном случае вы продолжите работу после таймаута.

// 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.
}

Это сработало для меня.

ConsoleKeyInfo k = new ConsoleKeyInfo();
Console.WriteLine("Press any key in the next 5 seconds.");
for (int cnt = 5; cnt > 0; cnt--)
  {
    if (Console.KeyAvailable == true)
      {
        k = Console.ReadKey();
        break;
      }
    else
     {
       Console.WriteLine(cnt.ToString());
       System.Threading.Thread.Sleep(1000);
     }
 }
Console.WriteLine("The key pressed was " + k.Key);

Я думаю, вам нужно будет создать дополнительный поток и опросить ключ на консоли.Я не знаю ни одного встроенного способа добиться этого.

Я боролся с этой проблемой 5 месяцев, прежде чем нашел решение, которое идеально работает в корпоративных условиях.

Проблема большинства решений на данный момент заключается в том, что они полагаются на что-то другое, а не на Console.ReadLine(), а Console.ReadLine() имеет множество преимуществ:

  • Поддержка удаления, возврата, клавиш со стрелками и т. д.
  • Возможность нажать клавишу «вверх» и повторить последнюю команду (это очень удобно, если вы реализуете консоль фоновой отладки, которая часто используется).

Мое решение заключается в следующем:

  1. Создать отдельная тема для обработки пользовательского ввода с помощью Console.ReadLine().
  2. По истечении времени ожидания разблокируйте Console.ReadLine(), отправив клавишу [enter] в текущее окно консоли, используя http://inputsimulator.codeplex.com/.

Образец кода:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Дополнительная информация об этом методе, включая правильный метод прерывания потока, использующего Console.ReadLine:

.NET-вызов для отправки нажатия клавиши [enter] в текущий процесс, который является консольным приложением?

Как прервать другой поток в .NET, когда указанный поток выполняет Console.ReadLine?

Вызов Console.ReadLine() в делегате плох, потому что, если пользователь не нажмет «ввод», этот вызов никогда не вернется.Поток, выполняющий делегат, будет заблокирован до тех пор, пока пользователь не нажмет «ввод», без возможности его отмены.

Выполнение последовательности этих вызовов не будет вести себя так, как вы ожидаете.Рассмотрим следующее (используя пример класса Console выше):

System.Console.WriteLine("Enter your first name [John]:");

string firstName = Console.ReadLine(5, "John");

System.Console.WriteLine("Enter your last name [Doe]:");

string lastName = Console.ReadLine(5, "Doe");

Пользователь позволяет истечь тайм-ауту для первого запроса, а затем вводит значение для второго запроса.И firstName, и LastName будут содержать значения по умолчанию.Когда пользователь нажимает «Ввод», первый Вызов ReadLine завершится, но код отказался от этого вызова и, по сути, отбросил результат.А второй Вызов ReadLine продолжит блокироваться, время ожидания в конечном итоге истечет, и возвращаемое значение снова станет значением по умолчанию.

Кстати, в приведенном выше коде есть ошибка.Вызывая waitHandle.Close(), вы закрываете событие из рабочего потока.Если пользователь нажмет «ввод» после истечения тайм-аута, рабочий поток попытается сигнализировать о событии, которое выдает исключение ObjectDisposeException.Исключение генерируется из рабочего потока, и если вы не настроили обработчик необработанных исключений, ваш процесс завершится.

Возможно, я слишком много вникаю в этот вопрос, но предполагаю, что ожидание будет похоже на меню загрузки, где оно ждет 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);
}

Если вы находитесь в 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;

К сожалению, я не могу комментировать пост Гульзар, но вот более полный пример:

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

РЕДАКТИРОВАТЬ:исправили проблему, выполнив фактическую работу в отдельном процессе и уничтожив этот процесс, если время его ожидания истекло.Подробности см. ниже.Фу!

Только что попробовал, и вроде все работает хорошо.У моего коллеги была версия, в которой использовался объект 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());
      }
   }
}

.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 или выполнять другие опасные хаки.Задачи позволяют решить вопрос очень естественным образом.

Как будто здесь уже недостаточно ответов: 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");
    }

Простой пример потоковой обработки для решения этой проблемы

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

или статическая строка вверху для получения всей строки.

В моем случае это работает нормально:

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

Разве это не красиво и коротко?

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

    // Handle keyInfo value here...
}

Это более полный пример решения Глена Слейдена.Я случайно сделал это при создании тестового примера для другой проблемы.Он использует асинхронный ввод-вывод и событие ручного сброса.

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

Еще один дешевый способ получить второй поток — обернуть его делегатом.

Пример реализации поста Эрика выше.Этот конкретный пример использовался для чтения информации, которая была передана консольному приложению через канал:

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

Обратите внимание: если вы пойдете по маршруту «Console.ReadKey», вы потеряете некоторые интересные функции ReadLine, а именно:

  • Поддержка удаления, возврата, клавиш со стрелками и т. д.
  • Возможность нажать клавишу «вверх» и повторить последнюю команду (это очень удобно, если вы реализуете консоль фоновой отладки, которая часто используется).

Чтобы добавить тайм-аут, измените цикл while соответствующим образом.

Пожалуйста, не ненавидьте меня за то, что я добавил еще одно решение к множеству существующих ответов!Это работает для 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);
}

Вот решение, которое использует 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 обеспечивает (прокрутку со стрелками вверх/вниз и т. д.).
  • Это вводит символы '\0' во ввод, если нажата специальная клавиша (F1, PrtScn и т. д.).Однако вы можете легко отфильтровать их, изменив код.

Оказался здесь, потому что был задан повторяющийся вопрос.Я придумал следующее решение, которое выглядит простым.Я уверен, что у него есть некоторые недостатки, которые я упустил.

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

Гораздо более современный код, основанный на задачах, будет выглядеть примерно так:

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 для получения стандартного ввода/вывода.Чтобы процесс не завершился во время выполнения работы, поток пользовательского интерфейса вызывает Console.ReadKey(false).Я хотел отменить ожидание потока пользовательского интерфейса из другого потока, поэтому я придумал модификацию решения @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();
  }    
}

Кажется, это самое простое рабочее решение, не использующее собственные 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");
        });
    }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top