Как добавить таймаут в Console.ReadLine ()?

123

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

Какой самый простой способ подойти к этому?

Larsenal
источник

Ответы:

113

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

  • Используется функция, отличная от ReadLine, что приводит к потере функциональности. (Удалить / возврат / клавишу вверх для предыдущего ввода).
  • Функция плохо себя ведет при многократном вызове (порождение нескольких потоков, зависание многих строк ReadLine или иное неожиданное поведение).
  • Функция полагается на ожидание занятости. Это ужасная трата, поскольку ожидается, что ожидание будет длиться от нескольких секунд до тайм-аута, который может составлять несколько минут. Ожидание с занятостью, которое выполняется в течение такого количества времени, представляет собой ужасную нехватку ресурсов, что особенно плохо в сценарии многопоточности. Если busy-wait модифицируется с помощью сна, это отрицательно сказывается на скорости отклика, хотя я признаю, что это, вероятно, не является большой проблемой.

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

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

JSQuareD
источник
1
После этого кода у меня возникло исключение NullReferenceException. Я думаю, что я мог бы исправить запуск потока один раз, когда создаются автоматические события.
Аугусто Педраса,
1
@JSQuareD Я не думаю, что ожидание с ожиданием со сном (200 мс) - это так много horrible waste, но, конечно, ваша сигнализация лучше. Кроме того, использование одного блокирующего Console.ReadLineвызова в бесконечном цикле во второй угрозе предотвращает проблемы, когда множество таких вызовов остается в фоновом режиме, как и в других решениях, получивших большое количество голосов ниже. Спасибо, что поделились своим кодом. +1
Роланд
2
Если вы не введете вовремя, этот метод, похоже, не работает при первом последующем Console.ReadLine()вызове, который вы делаете. В итоге вы получаете «фантом», ReadLineкоторый нужно выполнить в первую очередь.
Дерек
1
@Derek, к сожалению, вы не можете смешивать этот метод с обычными вызовами ReadLine, все вызовы должны выполняться через Reader. Решением этой проблемы было бы добавить метод чтения к читателю, который ждет gotInput без тайм-аута. В настоящее время я использую мобильный телефон, поэтому не могу легко добавить его к ответу.
JSQuareD 06
1
Я не вижу необходимости в getInput.
Silvalli
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();
зм.
источник
2
Не знаю, почему за это не проголосовали - работает абсолютно безупречно. Многие другие решения включают «ReadKey ()», который не работает должным образом: это означает, что вы теряете всю мощь ReadLine (), например, нажатие клавиши «вверх» для получения ранее набранной команды с использованием Backspace и клавиши со стрелками и т. д.
Contango
9
@Gravitas: Это не работает. Хорошо, работает один раз. Но каждый, ReadLineкого вы звоните, сидит и ждет ввода. Если вы вызовете его 100 раз, он создаст 100 потоков, которые не исчезнут, пока вы не нажмете Enter 100 раз!
Гейб
2
Осторожно. Это решение выглядит аккуратным, но в итоге у меня остались зависшие тысячи незавершенных вызовов. Поэтому не подходит, если звонить повторно.
Том Макин
@Gabe, shakinfree: для решения не рассматривались множественные вызовы, а только один асинхронный вызов с тайм-аутом. Я предполагаю, что пользователю было бы сложно напечатать 10 сообщений на консоли, а затем вводить их по одному в соответствующем порядке. Тем не менее, не могли бы вы попробовать прокомментировать строку TimedoutException и вернуть нулевую / пустую строку?
gp.
нет ... проблема в том, что Console.ReadLine все еще блокирует поток пула потоков, который выполняет метод Console.ReadLine из ReadLineDelegate.
gp.
27

Поможет ли этот подход с использованием 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, похоже, хочет заблокировать вызов, хотя я немного вздрагиваю от этой мысли ... Это, вероятно, лучшее решение.
ГЕОЧЕТ
Я уверен, вы это видели. Получил из быстрого google social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…
Гульзар Назим
Я не понимаю, как это «тайм-аут», если пользователь ничего не делает. Все, что это будет делать, - это, возможно, продолжать выполнение логики в фоновом режиме, пока не будет нажата клавиша и не продолжится другая логика.
mphair
Правда, это нужно исправить. Но достаточно просто добавить тайм-аут к условию цикла.
Джонатан Аллен
KeyAvailableуказывает только на то, что пользователь начал вводить данные в ReadLine, но нам нужно событие при нажатии Enter, которое заставит ReadLine вернуться. Это решение работает только для ReadKey, т.е. получение только одного символа. Поскольку это не решает фактический вопрос для ReadLine, я не могу использовать ваше решение. -1 извините
Roland
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);
user980750
источник
4
Я считаю, что это лучшее и простое решение с использованием уже встроенных инструментов. Прекрасная работа!
Vippy
2
Прекрасный! На самом деле простота - это высшая изощренность. Congrats!
BrunoSalvino
10

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

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

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

Эрик
источник
Это более или менее то, что делает Принятое решение.
Роланд
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.
}
Гленн Слейден
источник
8

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

GEOCHET
источник
Да, если у вас есть второй поток опроса ключей, и ваше приложение закрывается, пока оно сидит там в ожидании, этот поток опроса ключей будет просто сидеть там, ждать, вечно.
Келли Элтон
Фактически: либо второй поток, либо делегат с «BeginInvoke» (который использует поток за кулисами - см. Ответ от @gp).
Contango
@ kelton52, вторичный поток выйдет, если завершить процесс в диспетчере задач?
Arlen Beiler
6

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

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

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

Мое решение таково:

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

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

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

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

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

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

Контанго
источник
5

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

Последовательность этих вызовов не приведет к ожидаемому результату. Рассмотрим следующее (используя приведенный выше пример класса 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 (), вы закрываете событие из-под рабочего потока. Если пользователь нажимает «ввод» после истечения тайм-аута, рабочий поток попытается сигнализировать о событии, которое вызывает исключение ObjectDisposedException. Исключение генерируется из рабочего потока, и если вы не настроили обработчик необработанных исключений, ваш процесс завершится.

Brannon
источник
1
Термин «выше» в вашем сообщении неоднозначен и сбивает с толку. Если вы имеете в виду другой ответ, сделайте соответствующую ссылку на этот ответ.
bzlm 06
5

Если вы используете 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

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

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

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();
Джейми Китсон
источник
Обратите внимание, что вы также можете использовать Console.In.Peek (), если консоль не видна (?) Или ввод осуществляется из файла.
Джейми Китсон,
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());
      }
   }
}
Джесси С. Слайсер
источник
2
Вызов отдельного исполняемого файла в новом процессе только для выполнения синхронизированного ReadLine () звучит как массовый перебор. По сути, вы решаете проблему невозможности прервать блокирующий поток ReadLine (), вместо этого настраивая и прерывая весь процесс.
bzlm 06
Затем сообщите об этом Microsoft, которая поставила нас в такое положение.
Джесси С. Слайсер,
Microsoft не ставила вас в такое положение. Посмотрите на некоторые другие ответы, которые выполняют ту же работу, в несколько строк. Я думаю, что приведенный выше код должен получить какую-то награду - но не ту, которую вы хотите :)
Contango
1
Нет, ни один из других ответов не сделал именно то, что хотел OP. Все они теряют свойства стандартных входных процедур или зацикливаться на том , что все запросы Console.ReadLine() будут блокирование и задержать ввод в следующем запросе. Принятый ответ довольно близок, но все же имеет ограничения.
Джесси С. Слайсер
1
Гм, нет, это не так. Входной буфер по-прежнему блокируется (даже если программа этого не делает). Попробуйте сами: введите несколько символов, но не нажимайте Enter. Пусть таймаут. Захватить исключение в вызывающей стороне. Затем добавьте ReadLine()в свою программу еще одну после вызова этой. Посмотри, что получится. Вы должны нажать return ДВАЖДЫ, чтобы заставить его работать из-за однопоточной природы Console. Это. Не имеет . Работай.
Джесси С. Слайсер
2

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

StevoInco
источник
2

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

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

mphair
источник
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;
}
Саша
источник
1

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

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

    // Handle keyInfo value here...
}
Джон Атак
источник
1
Что, черт возьми, такое SpinWait?
john ktejik
1

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

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!");
        }
    }
mikemay
источник
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.");
            }
        }
    }
}
Серджио Кабрал
источник
0

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

Джоэл Кохорн
источник
0

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

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

источник
0
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

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

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

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

Контанго
источник
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);
}
Дэвид Киркланд
источник
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(прокрутка вверх / вниз и т. Д.).
  • Это вводит символы '\ 0' во ввод, если нажата специальная клавиша (F1, PrtScn и т. Д.). Однако вы можете легко отфильтровать их, изменив код.
Брайан Гидеон
источник
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;
    }
}
Фрэнк Рем
источник
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);                
        }            
    }
Тоно Нам
источник
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");
}
cprcrack
источник
Что, если пользователь нажмет клавишу и отпустит ее в течение 2000 мс?
Иззи
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();
}
Шонн Лига
источник
0

У меня была уникальная ситуация с приложением Windows (службой Windows). При запуске программы в интерактивном режиме Environment.IsInteractive(VS Debugger или из cmd.exe) я использовал AttachConsole / AllocConsole для получения моего stdin / stdout. Чтобы не допустить завершения процесса во время выполнения работы, поток пользовательского интерфейса вызывает 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();
  }    
}
JJS
источник