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





Я думаю, вам нужно будет создать вторичный поток и опросить ключ на консоли. Я не знаю ни одного встроенного способа сделать это.
Фактически: либо второй поток, либо делегат с «BeginInvoke» (который использует поток за кулисами - см. Ответ от @gp).
@ kelton52, вторичный поток выйдет, если завершить процесс в диспетчере задач?
Поможет ли этот подход с использованием 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, похоже, хочет заблокировать вызов, хотя я немного вздрагиваю при этой мысли ... Это, вероятно, лучшее решение.
Я уверен, что вы это видели. Получил с быстрого гугла social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…
Я не понимаю, как это «тайм-аут», если пользователь ничего не делает. Все, что это будет делать, - это, возможно, продолжать выполнение логики в фоновом режиме, пока не будет нажата клавиша и не будет продолжена другая логика.
Правда, это нужно исправить. Но достаточно просто добавить тайм-аут к условию цикла.
KeyAvailable указывает только на то, что пользователь начал вводить данные в ReadLine, но нам нужно событие при нажатии Enter, которое заставляет ReadLine возвращаться. Это решение работает только для ReadKey, т.е. получение только одного символа. Поскольку это не решает фактический вопрос для ReadLine, я не могу использовать ваше решение. -1 извините
Так или иначе вам понадобится второй поток. Вы можете использовать асинхронный ввод-вывод, чтобы не объявлять свой собственный:
Если чтение возвращает данные, установите событие, и ваш основной поток будет продолжен, в противном случае вы продолжите работу по истечении тайм-аута.
Это более или менее то, что делает Принятое решение.
Еще один дешевый способ получить второй поток - обернуть его делегатом.
РЕДАКТИРОВАТЬ: устранена проблема, поскольку фактическая работа выполнялась в отдельном процессе и прекращение работы этого процесса при истечении времени ожидания. Подробнее см. Ниже. Ух!
Просто попробовал, и, похоже, он работал хорошо. У моего коллеги была версия, в которой использовался объект 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());
}
}
}
Вызов отдельного исполняемого файла в новом процессе только для выполнения синхронизированного ReadLine () звучит как огромное излишество. По сути, вы решаете проблему невозможности прервать блокирующий поток ReadLine (), вместо этого настраивая и прерывая весь процесс.
Затем сообщите об этом Microsoft, которая поставила нас в такое положение.
Microsoft не ставила вас в такое положение. Посмотрите на некоторые другие ответы, которые выполняют ту же работу, в несколько строк. Я думаю, что приведенный выше код должен получить какую-то награду, но не ту, которую вы хотите :)
Нет, ни один из других ответов не сделал именно то, что хотел OP. Все они теряют возможности стандартных процедур ввода или зацикливаются на том факте, что все запросы Console.ReadLine() на блокировку находятся будут задерживать ввод при следующем запросе. Принятый ответ довольно близок, но все же имеет ограничения.
Ответ от @gp безупречный - он использует ReadLine (), но не порождает другой процесс.
Гм, нет, это не так. Входной буфер по-прежнему блокируется (даже если программа этого не делает). Попробуйте сами: введите несколько символов, но не нажимайте Enter. Пусть таймаут. Захватить исключение в вызывающей стороне. Затем добавьте в свою программу еще один ReadLine() после вызова этого. Посмотри, что получится. Вы должны нажать return ДВАЖДЫ, чтобы запустить его из-за однопоточной природы Console. Это. Нет. Работа.
Ответ Браннона более подробно описывает, почему он не работает: stackoverflow.com/questions/57615/…
Вызов 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. Исключение генерируется из рабочего потока, и если вы не настроили обработчик необработанных исключений, ваш процесс завершится.
Термин «выше» в вашем сообщении неоднозначен и сбивает с толку. Если вы имеете в виду другой ответ, сделайте соответствующую ссылку на этот ответ.
Возможно, я слишком много читаю в вопросе, но я предполагаю, что ожидание будет похоже на меню загрузки, где оно ждет 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);
}
Пример реализации поста Эрика выше. Этот конкретный пример использовался для чтения информации, которая была передана консольному приложению через конвейер:
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(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();
Не знаю, почему за это не проголосовали - работает абсолютно безупречно. Многие другие решения включают «ReadKey ()», который не работает должным образом: это означает, что вы теряете всю мощь ReadLine (), например, нажатие клавиши «вверх» для получения ранее набранной команды с использованием backspace и клавиши со стрелками и т. д.
@Gravitas: Это не работает. Хорошо, работает один раз. Но каждый вызываемый вами ReadLine сидит и ждет ввода. Если вы вызовете его 100 раз, он создаст 100 потоков, которые не исчезнут, пока вы не нажмете Enter 100 раз!
Остерегаться. Это решение выглядит аккуратным, но в итоге у меня остались зависшие тысячи незавершенных вызовов. Так что не подходит, если звонить повторно.
@Gabe, shakinfree: для решения не рассматривались множественные вызовы, а только один асинхронный вызов с тайм-аутом. Я предполагаю, что пользователю было бы сложно напечатать 10 сообщений на консоли, а затем вводить их по одному в соответствующем порядке. Тем не менее, не могли бы вы попробовать прокомментировать строку TimedoutException и вернуть нулевую / пустую строку?
нет ... проблема в том, что Console.ReadLine все еще блокирует поток пула потоков, на котором выполняется метод Console.ReadLine из ReadLineDelegate.
Как исправить проблему с утечкой памяти? В остальном, что мне нравится в этом решении, так это то, что оно в значительной степени следует совету Microsoft на msdn.microsoft.com/en-us/library/2e08f6yc%28v=vs.110%29.aspx по общей проблеме преобразования вызовов синхронизации в асинхронные.
Сначала мне понравилось ваше решение, потому что оно соответствует информации Microsoft о том, как преобразовать вызов синхронизации в асинхронный, но я обнаружил, что проблема того, как исправить отсутствующий вызов EndInvoke по таймауту, не может быть решена. С другой стороны, принятое решение с одним вызовом синхронизации во втором потоке, работающем вечно, идеально. Спасибо за ваши усилия, но я не могу использовать ваше решение. -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();
}
или статическая строка вверху для получения всей строки.
К сожалению, я не могу комментировать пост Гульзар, но вот более полный пример:
while (Console.KeyAvailable == false)
{
Thread.Sleep(250);
i++;
if (i > 3)
throw new Exception("Timedout waiting for input.");
}
input = Console.ReadLine();
Обратите внимание, что вы также можете использовать Console.In.Peek (), если консоль не отображается (?) Или ввод осуществляется из файла.
В моем случае это работает нормально:
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;
}
// 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.
}
string readline = "?";
ThreadPool.QueueUserWorkItem(
delegate
{
readline = Console.ReadLine();
}
);
do
{
Thread.Sleep(100);
} while (readline == "?");
Обратите внимание, что если вы пойдете по маршруту Console.ReadKey, вы потеряете некоторые интересные функции ReadLine, а именно:
Чтобы добавить тайм-аут, измените цикл while по своему усмотрению.
Это сработало для меня.
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);
Я считаю, что это лучшее и простое решение с использованием уже встроенных инструментов. Отличная работа!
Красивый! На самом деле простота - это высшая изощренность. Поздравляю!
Я боролся с этой проблемой в течение 5 месяцев, прежде чем нашел решение, которое отлично работает в корпоративной среде.
Проблема с большинством решений до сих пор заключается в том, что они полагаются на что-то другое, кроме Console.ReadLine (), а Console.ReadLine () имеет множество преимуществ:
Мое решение таково:
Образец кода:
InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);
Дополнительная информация об этом методе, включая правильный метод прерывания потока, использующего Console.ReadLine:
Как прервать другой поток в .NET, когда указанный поток выполняет Console.ReadLine?
Разве это не мило и коротко?
if (SpinWait.SpinUntil(() => Console.KeyAvailable, millisecondsTimeout))
{
ConsoleKeyInfo keyInfo = Console.ReadKey();
// Handle keyInfo value here...
}
Что, черт возьми, такое SpinWait?
Это более полный пример решения Глена Слейдена. Я случайно сделал это при создании тестового примера для другой проблемы. Он использует асинхронный ввод-вывод и событие ручного сброса.
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!");
}
}
Пожалуйста, не ненавидьте меня за добавление еще одного решения к множеству существующих ответов! Это работает для 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);
}
Я удивлен, узнав, что по прошествии 5 лет все ответы по-прежнему страдают от одной или нескольких из следующих проблем:
Я считаю, что мое решение решит исходную проблему без каких-либо из перечисленных выше проблем:
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 и опустите тайм-аут, чтобы по умолчанию он был бесконечным.
Так как насчет проблем, связанных с другими упомянутыми мною решениями?
Единственная проблема, которую я предвижу в этом решении, заключается в том, что оно не является поточно-ориентированным. Однако несколько потоков не могут запрашивать у пользователя ввод одновременно, поэтому синхронизация должна происходить до вызова Reader.ReadLine.
После этого кода у меня возникло исключение NullReferenceException. Я думаю, что я мог бы исправить запуск потока один раз, когда создаются автоматические события.
@AugustoPedraza В какой строке вы получаете исключение NullReferenceException?
@AugustoPedraza Ах да, я понимаю, что вы имеете в виду, AutoResetEvent должны быть инициализированы перед запуском потока. Отредактировал код в посте. Спасибо!
@JSQuareD Я не думаю, что ожидание с ожиданием со сном (200 мс) - это такая же большая часть horrible waste, но, конечно, ваша сигнализация лучше. Кроме того, использование одного блокирующего вызова Console.ReadLine в бесконечном цикле во второй угрозе предотвращает проблемы с большим количеством таких вызовов, которые остаются в фоновом режиме, как и в других решениях, получивших большое количество голосов ниже. Спасибо, что поделились своим кодом. +1
Отличный код. Единственная модификация, которую я сделал в своем использовании, заключалась в том, что я использовал стандарт "TryXX (out)", поэтому мне не нужно фиксировать исключение, если произойдет тайм-аут
Если вы не введете время вовремя, этот метод, похоже, не работает при первом последующем вызове Console.ReadLine(), который вы делаете. В итоге вы получаете «фантом» ReadLine, который нужно выполнить в первую очередь.
@Derek, к сожалению, нельзя смешивать этот метод с обычными вызовами ReadLine, все вызовы должны выполняться через Reader. Решением этой проблемы было бы добавить метод чтения к читателю, который ожидает gotInput без тайм-аута. В настоящее время я использую мобильный телефон, поэтому не могу легко добавить его к ответу.
JSQuareD Не могли бы вы обновить свой код ответа, чтобы исправить проблему, о которой сообщил @Derek? Думаю, многим будет полезно, в том числе и мне! :)
@ loripino21 Это нормально?
Потому что считыватель предназначен для обработки нескольких чтений. После чтения с консоли он будет ждать сигнала для чтения следующей строки.
@JSQuareD Консольное приложение не может выйти, если вы не введете имя.
@KevinBui Вы уверены, что задали для свойства IsBackground значение true?
@JSQuareD Да, я полностью скопировал ваш код. Я думаю, что Console.ReadLine () вызывает консоль, ожидающую ввода, и она не может завершиться, пока вы не введете.
@KevinBui Я не могу воспроизвести вашу проблему. Пожалуйста, разместите отдельный вопрос с минимальный пример.
Не вижу необходимости в getInput.
Ах. Я понимаю. Возможно, есть более описательные имена для двух переменных, например consumerGreenLight и producerGreenLight.
Если вы не подтвердите Reader.ReadLine вовремя (т.е. вы наберете "он", а затем произойдет тайм-аут), неполный ввод будет частью последующего Reader.ReadLine (т.е. если вы введете "вот" во второй раз, результатом будет "привет" вместо "вот").
Это было очень хорошо. Но чтобы помочь мне, мне пришлось изменить метод ReadLine (). Так было, если я заканчивал программу с помощью Console.ReadKey (), она все еще ждала Console.ReadLine (). Поэтому я использовал класс Stopwatch для таймера. Я написал свой код ниже.
Я думал, что для остановки потока, созданного в статическом ctor, понадобится метод «Close», потому что я думал, что все потоки должны быть остановлены, прежде чем мое приложение закроется. НО использование JSQuareD «inputThread.IsBackground = true» решает эту проблему. Нет необходимости останавливать поток, он остановится сам по себе, когда приложение завершит работу.
@silvalli Я тоже не вижу необходимости в getInput. Поскольку следующий вызов Console.ReadLine() все равно блокируется. Я думаю, что единственное, что нужно для getInput(), - это сохранить самый первый вызов Console.Readline(), если Reader.Readline() НИКОГДА не будет вызываться в приложении llifetime.
Вот решение, использующее 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 (прокрутка стрелок вверх / вниз и т. д.).Заканчивается здесь, потому что был задан повторяющийся вопрос. Я придумал следующее решение, которое выглядит простым. Я уверен, что у него есть недостатки, которые я упустил.
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");
}
Что, если пользователь нажмет клавишу и отпустит ее в течение 2000 мс?
Это не сработает, если вы нажмете до 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 или выполнять другие опасные взломы, чтобы заставить это работать. Задачи позволяют решить вопрос самым естественным образом.
Гораздо более современный код, основанный на задачах, будет выглядеть примерно так:
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 для получения моего 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();
}
}
Вот безопасное решение, которое подделывает ввод консоли, чтобы разблокировать поток после тайм-аута. Проект https://github.com/Igorium/ConsoleReader предоставляет пример реализации пользовательского диалога.
var inputLine = ReadLine(5);
public static string ReadLine(uint timeoutSeconds, Func<uint, string> countDownMessage, uint samplingFrequencyMilliseconds)
{
if (timeoutSeconds == 0)
return null;
var timeoutMilliseconds = timeoutSeconds * 1000;
if (samplingFrequencyMilliseconds > timeoutMilliseconds)
throw new ArgumentException("Sampling frequency must not be greater then timeout!", "samplingFrequencyMilliseconds");
CancellationTokenSource cts = new CancellationTokenSource();
Task.Factory
.StartNew(() => SpinUserDialog(timeoutMilliseconds, countDownMessage, samplingFrequencyMilliseconds, cts.Token), cts.Token)
.ContinueWith(t => {
var hWnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
PostMessage(hWnd, 0x100, 0x0D, 9);
}, TaskContinuationOptions.NotOnCanceled);
var inputLine = Console.ReadLine();
cts.Cancel();
return inputLine;
}
private static void SpinUserDialog(uint countDownMilliseconds, Func<uint, string> countDownMessage, uint samplingFrequencyMilliseconds,
CancellationToken token)
{
while (countDownMilliseconds > 0)
{
token.ThrowIfCancellationRequested();
Thread.Sleep((int)samplingFrequencyMilliseconds);
countDownMilliseconds -= countDownMilliseconds > samplingFrequencyMilliseconds
? samplingFrequencyMilliseconds
: countDownMilliseconds;
}
}
[DllImport("User32.Dll", EntryPoint = "PostMessageA")]
private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);
Кажется, это самое простое рабочее решение, в котором не используются собственные 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");
});
}
Если вы используете метод 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;
Как будто здесь уже не было достаточно ответов: 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");
}
Мой код полностью основан на ответе друга @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.");
}
}
}
}
Да, если у вас есть второй поток опроса для ключей, и ваше приложение закрывается, пока оно сидит там в ожидании, этот поток опроса ключей будет просто сидеть там, ждать, вечно.