Як додати тайм-аут до Console.ReadLine ()?


122

У мене є консольний додаток, в якому я хочу надати користувачеві x секунд для відповіді на підказку. Якщо через певний проміжок часу введення не робиться, логіка програми повинна продовжуватися. Ми припускаємо, що час очікування означає порожню відповідь.

Який найпростіший спосіб підійти до цього?

Відповіді:


112

Я здивований, дізнавшись, що через 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 коли-небудь буде активним. Виклик функції завжди призведе до останнього вводу або до тайм-ауту, і користувачеві не доведеться натискати клавішу вводити більше одного разу, щоб подати свій вклад.
  • І, очевидно, функція не покладається на зайнятого очікування. Натомість він використовує належні багатопотокові методи, щоб запобігти витрачанню ресурсів.

Єдина проблема, яку я передбачу в цьому рішенні, - це те, що воно не є безпечним для потоків. Однак декілька потоків не можуть попросити користувача одночасно ввести інформацію, тому синхронізація повинна відбуватися перед тим, як Reader.ReadLineвсе-таки здійснити дзвінок .


1
Після цього коду я отримав NullReferenceException. Я думаю, що я міг зафіксувати запуск потоку один раз, коли створюються автоматичні події.
Augusto Pedraza

1
@JSQuareD Я не думаю, що зайнятий режим очікування зі сном (200 мс) - це велика частина horrible waste, але, звичайно, ваша сигналізація є кращою. Крім того, використання одного Console.ReadLineвиклику блокування у нескінченному циклі, у другому загроза запобігає проблемам із безліччю таких дзвінків, що звисають на задньому плані, як і в інших, сильно обтяжених рішеннями нижче. Дякуємо, що поділилися кодом. +1
Роланд

2
Якщо не вдалося ввести час, цей спосіб, здається, порушиться під час першого наступного Console.ReadLine()дзвінка, який ви здійснюєте. Ви закінчуєте "фантом", ReadLineякий потрібно спочатку завершити.
Дерек

1
@Derek, на жаль, ви не можете поєднувати цей метод із звичайними викликами ReadLine, всі дзвінки повинні здійснюватися через програму Reader. Рішенням цієї проблеми було б додати методу до читача, який чекає на отримання входу без тайм-ауту. Зараз я перебуваю на мобільному, тому не можу легко додати його до відповіді.
JSQuareD

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 та клавіші зі стрілками тощо
Контанго

9
@Gravitas: Це не працює. Ну, це працює один раз. Але кожен, кого ReadLineви телефонуєте, сидить там і чекає введення. Якщо ви називаєте це 100 разів, він створює 100 ниток, які не всі згасають, поки ви не натиснете Enter 100 разів!
Гейб

2
Остерігайся. Це рішення виглядає акуратним, але в мене закінчилося 1000 невиконаних дзвінків. Тому не підходить, якщо дзвонить повторно.
Том Макін

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

Це правда, ОП, схоже, хоче блокувати дзвінок, хоча я трохи здригаюся від цієї думки ... Це, мабуть, краще рішення.
GEOCHET

Я впевнений, що ви це бачили. Отримав це з швидкого google social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…
Gulzar Nazim,

Я не бачу, як це "тайм-аут", якщо користувач нічого не робить. Все це могло б зробити, можливо, продовжувати виконувати логіку у фоновому режимі, поки не буде натиснута клавіша та не буде продовжена інша логіка.
mphair

Щоправда, це потрібно виправити. Але досить просто додати час очікування до умови циклу.
Джонатан Аллен

KeyAvailableвказує лише на те, що користувач почав вводити введення в ReadLine, але нам потрібна подія після натискання клавіші Enter, яка змушує ReadLine повернутися. Це рішення працює лише для ReadKey, тобто отримує лише один символ. Оскільки це не вирішує власне питання щодо ReadLine, я не можу використовувати ваше рішення. -1 вибачте
Роланд

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

4
Я думаю, що це найкраще і найпростіше рішення з використанням вже вбудованих інструментів. Чудова робота!
Vippy

2
Гарний! Простота справді є вищою вишуканістю. Вітаю!
BrunoSalvino

10

Так чи інакше вам потрібна друга нитка. Ви можете використовувати асинхронний IO, щоб уникнути оголошення власного:

  • оголосити ManualResetEvent, назвати його "evt"
  • зателефонуйте System.Console.OpenStandardInput, щоб отримати вхідний потік. Вкажіть метод зворотного дзвінка, який буде зберігати його дані та встановити evt.
  • виклик методу BeginRead цього потоку для запуску асинхронної операції зчитування
  • потім введіть примірне очікування на ManualResetEvent
  • якщо час очікування вийшов, скасуйте прочитане

Якщо зчитування повертає дані, встановіть подію, і ваш основний потік продовжиться, інакше ви продовжуватиметесь після очікування.


Це більш-менш те, що робить Прийняте рішення.
Roland

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

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


Так, якщо у вас є другий потік опитування ключів, і ваш додаток закривається, поки він сидить там і чекає, ця ключова нитка для опитування просто сидітиме там і чекає назавжди.
Келлі Елтон

Насправді: або другий потік, або делегат із "BeginInvoke" (який використовує нитку поза кадром - див. Відповідь від @gp).
Контанго

@ kelton52, Чи закриється вторинна нитка, якщо ви закінчите процес у диспетчері завдань?
Арлен Бейлер

6

Я боровся з цією проблемою 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?


5

Виклик Console.ReadLine () у делегата поганий, тому що якщо користувач не натисне "Enter", цей виклик ніколи не повернеться. Потік, що виконує делегат, буде заблокований, поки користувач не натисне "Enter", не маючи можливості його скасувати.

Видача послідовності цих дзвінків буде не так, як ви очікували. Розглянемо наступне (використовуючи приклад класу "Консоль" зверху):

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

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

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

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

Користувач дозволяє закінчити час очікування для першої підказки, а потім вводить значення для другого підказки. І firstName, і lastName будуть містити значення за замовчуванням. Коли користувач натисне "Enter", перший виклик ReadLine завершиться, але код відмовився від цього виклику і фактично відкинув результат. Другий ReadLine виклик буде продовжувати блокувати, тайм - аут, в кінцевому рахунку закінчується , і яке значення знову буде за замовчуванням.

BTW. У коді вище помилка. Зателефонувавши на waitHandle.Close (), ви закриєте подію з-під робочої нитки. Якщо користувач натискає «enter» після закінчення тайм-ауту, робоча нитка спробує подати сигнал про подію, яка видає ObjectDisposedException. Виняток викидається з робочої нитки, і якщо ви не встановили необроблений обробник винятків, ваш процес припиниться.


1
Термін "вище" у вашій посаді неоднозначний і заплутаний. Якщо ви посилаєтесь на іншу відповідь, вам слід зробити належне посилання на цю відповідь.
bzlm

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

EDIT : виправлено проблему, виконавши фактичну роботу в окремому процесі та вбивши цей процес, якщо він вичерпається. Детальніше дивіться нижче. Вау!

Просто дав цьому пробіг, і, здавалося, це працює добре. У мого колеги була версія, яка використовувала об’єкт Thread, але я вважаю, що метод делегатів Beginіvoke () трохи елегантніший.

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

Потім скажіть це Microsoft, який поставив нас у таке становище.
Джессі К. Слікер

Microsoft не поставив вас у таке становище. Подивіться на деякі інші відповіді, які виконують ту саму роботу в декількох рядках. Я думаю, що код вище повинен отримати якусь нагороду - але не таку, яку ви хочете :)
Контанго

1
Ні, жодна з інших відповідей не робила саме того, чого хотіла ОП. Всі вони втрачають властивості стандартних вхідних процедур або зациклюватися на тому , що всі запити Console.ReadLine() будуть блокування і затримати введення в наступному запиті. Прийнята відповідь досить близька, але все ж має обмеження.
Jesse C. Slicer

1
Гм, ні, це не так. Буфер введення все ще блокується (навіть якщо програма цього не робить). Спробуйте самі: введіть деякі символи, але не вводьте. Нехай це таймаут. Зробіть виняток у абонента. Потім ReadLine()зателефонуйте в програму ще одного. Подивіться, що станеться. Ви повинні натиснути кнопку Двічі, щоб змусити її працювати через однопотоковий характер 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 або виконувати інші небезпечні хаки, щоб це не працювало. Завдання дозволяють вирішити питання дуже природним чином.


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

або статичний рядок вгорі для отримання цілого рядка.


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

1

Мій код повністю заснований на відповіді друга @JSQuareD

Але мені потрібно було використовувати Stopwatchтаймер, тому що коли я закінчив програму, Console.ReadKey()її все ще чекали, Console.ReadLine()і це породжувало несподівану поведінку.

Для мене це працювало досконало. Підтримує оригінальну консоль.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

Приклад реалізації поста Еріка вище. Цей конкретний приклад був використаний для читання інформації, яка передається в консольний додаток через трубу:

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

Що робити, якщо користувач натисне клавішу і відпустить протягом 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. Щоб процес не закінчувався під час роботи, зателефонував UI Thread 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();
  }    
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.