Как добавить Timeout в Console.ReadLine ()? - PullRequest
116 голосов
/ 12 сентября 2008

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

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

Ответы [ 32 ]

102 голосов
/ 20 августа 2013

Я удивлен, узнав, что через 5 лет все ответы по-прежнему страдают одной или несколькими из следующих проблем:

  • Используется функция, отличная от ReadLine, что приводит к потере функциональности. (Удалить / backspace / up-key для предыдущего ввода).
  • Функция плохо работает при многократном вызове (порождение нескольких потоков, много зависаний 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. Вызов функции всегда приведет к последнему вводу или к истечению времени ожидания, и пользователю не придется нажимать клавишу ввода более одного раза, чтобы отправить свой ввод.
  • И, очевидно, функция не полагается на занятое ожидание. Вместо этого он использует правильные методы многопоточности, чтобы не тратить ресурсы.

Единственная проблема, которую я предвижу с этим решением, заключается в том, что оно не является поточно-ориентированным. Тем не менее, несколько потоков не могут действительно запрашивать ввод данных у пользователя одновременно, поэтому в любом случае должна выполняться синхронизация перед вызовом Reader.ReadLine.

33 голосов
/ 11 января 2010
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();
27 голосов
/ 12 сентября 2008

Поможет ли этот подход с помощью 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);
    }
}
10 голосов
/ 12 сентября 2008

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

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

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

9 голосов
/ 05 октября 2011

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

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);
9 голосов
/ 28 августа 2010
// 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.
}
8 голосов
/ 12 сентября 2008

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

6 голосов
/ 26 января 2012

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

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

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

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

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

Пример кода:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

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

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

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

4 голосов
/ 26 июня 2017

Если вы используете метод 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;
4 голосов
/ 24 октября 2008

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