Jak dodać limit czasu do Console.ReadLine ()?


123

Mam aplikację konsolową, w której chcę dać użytkownikowi x sekund na odpowiedź na monit. Jeśli po pewnym czasie nie zostanie wprowadzone żadne wejście, logika programu powinna być kontynuowana. Zakładamy, że przekroczenie limitu czasu oznacza pustą odpowiedź.

Jaki jest najprostszy sposób rozwiązania tego problemu?

Odpowiedzi:


113

Jestem zaskoczony, gdy dowiaduję się, że po 5 latach we wszystkich odpowiedziach nadal występuje jeden lub więcej z następujących problemów:

  • Używana jest funkcja inna niż ReadLine, co powoduje utratę funkcjonalności. (Klawisz Delete / backspace / up-key dla poprzedniego wprowadzenia).
  • Funkcja zachowuje się źle, gdy jest wywoływana wiele razy (odradza wiele wątków, wiele zawiesza się ReadLine lub w inny sposób nieoczekiwane zachowanie).
  • Funkcja polega na oczekiwaniu zajętości. Co jest okropną stratą, ponieważ oczekuje się, że oczekiwanie będzie trwać od kilku sekund do limitu czasu, który może wynosić wiele minut. Zajęte oczekiwanie, które trwa przez taką ilość czasu, jest okropnym wysysaniem zasobów, co jest szczególnie złe w scenariuszu wielowątkowym. Jeśli modyfikacja zajętości jest uśpiona, ma to negatywny wpływ na responsywność, chociaż przyznaję, że prawdopodobnie nie jest to duży problem.

Wierzę, że moje rozwiązanie rozwiąże pierwotny problem bez żadnego z powyższych problemów:

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

Dzwonienie jest oczywiście bardzo proste:

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

Alternatywnie możesz skorzystać z TryXX(out)konwencji, zgodnie z sugestią shmueli:

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

Który nazywa się w następujący sposób:

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

W obu przypadkach nie można łączyć połączeń Readerz normalnymi Console.ReadLinepołączeniami: jeśli Readerupłynie limit czasu, ReadLinepołączenie zostanie zawieszone . Zamiast tego, jeśli chcesz mieć normalne (nieokreślone w czasie) ReadLinewywołanie, po prostu użyj Readeri pomiń limit czasu, aby domyślnie był nieskończony.

A co z tymi problemami innych rozwiązań, o których wspomniałem?

  • Jak widać, używany jest ReadLine, unikając pierwszego problemu.
  • Funkcja zachowuje się poprawnie, gdy jest wywoływana wiele razy. Niezależnie od tego, czy nastąpi przekroczenie limitu czasu, czy nie, tylko jeden wątek w tle będzie zawsze działał i tylko co najwyżej jedno wywołanie ReadLine będzie kiedykolwiek aktywne. Wywołanie funkcji zawsze spowoduje najnowsze dane wejściowe lub przekroczenie limitu czasu, a użytkownik nie będzie musiał wciskać enter więcej niż raz, aby przesłać swoje dane.
  • I, oczywiście, funkcja nie polega na oczekiwaniu zajętości. Zamiast tego wykorzystuje odpowiednie techniki wielowątkowości, aby zapobiec marnowaniu zasobów.

Jedynym problemem, jaki przewiduję w przypadku tego rozwiązania, jest to, że nie jest ono bezpieczne dla wątków. Jednak wiele wątków nie może tak naprawdę prosić użytkownika o wprowadzenie danych w tym samym czasie, więc synchronizacja powinna mieć miejsce przed wykonaniem połączenia Reader.ReadLine.


1
Otrzymałem NullReferenceException po tym kodzie. Myślę, że mógłbym naprawić uruchamianie wątku raz, gdy tworzone są zdarzenia automatyczne.
Augusto Pedraza

1
@JSQuareD Nie sądzę, żeby zajęte oczekiwanie z uśpieniem (200 ms) było aż tak trudne horrible waste, ale oczywiście twoja sygnalizacja jest lepsza. Ponadto użycie jednego Console.ReadLinepołączenia blokującego w nieskończonej pętli w drugim zagrożeniu zapobiega problemom z wieloma takimi połączeniami kręcącymi się w tle, jak w przypadku innych, mocno przegłosowanych, rozwiązań poniżej. Dziękujemy za udostępnienie kodu. +1
Roland

2
Jeśli nie wprowadzisz danych na czas, ta metoda wydaje się nie działać przy pierwszym kolejnym Console.ReadLine()wywołaniu, które wykonujesz. Otrzymujesz „fantom”, ReadLinektóry musi zostać ukończony jako pierwszy.
Derek

1
@Derek niestety nie możesz łączyć tej metody z normalnymi wywołaniami ReadLine, wszystkie wywołania muszą być wykonywane przez Reader. Rozwiązaniem tego problemu byłoby dodanie metody do czytnika, który czeka na gotInput bez limitu czasu. Obecnie korzystam z telefonu komórkowego, więc nie mogę łatwo dodać tego do odpowiedzi.
JSQuareD

1
Nie widzę takiej potrzeby 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
Nie wiem, dlaczego nie zostało to ocenione - działa absolutnie bez zarzutu. Wiele innych rozwiązań obejmuje „ReadKey ()”, który nie działa poprawnie: oznacza to, że tracisz całą moc ReadLine (), na przykład naciskając klawisz „w górę” w celu uzyskania wcześniej wpisanego polecenia, używając klawisza Backspace i klawisze strzałek itp.
Contango,

9
@Gravitas: To nie działa. Cóż, raz to działa. Ale każdy ReadLinedzwonisz czekając na wejście. Jeśli wywołasz to 100 razy, utworzy 100 wątków, które nie znikną, dopóki nie naciśniesz Enter 100 razy!
Gabe

2
Strzec się. To rozwiązanie wydaje się fajne, ale skończyło się na tysiącach niezakończonych połączeń zawieszonych. Więc nie nadaje się, jeśli jest wielokrotnie wywoływany.
Tom Makin

@ Gabe, shakinfree: dla rozwiązania nie uwzględniono wielu wywołań, ale tylko jedno wywołanie asynchroniczne z limitem czasu. Wydaje mi się, że byłoby mylące dla użytkownika wydrukowanie 10 komunikatów na konsoli, a następnie wprowadzenie dla nich danych wejściowych jeden po drugim w odpowiedniej kolejności. Niemniej jednak, w przypadku połączeń zawieszonych, czy mógłbyś spróbować skomentować wiersz TimedoutException i zwrócić pusty / pusty ciąg?
gp.

nie ... problem polega na tym, że Console.ReadLine nadal blokuje wątek puli wątków, w którym działa metoda Console.ReadLine z ReadLineDelegate.
gp.

27

Czy to podejście przy użyciu pomocy 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);
    }
}

To prawda, OP wydaje się chcieć połączenia blokującego, chociaż trochę wzdrygam się na tę myśl ... To chyba lepsze rozwiązanie.
GEOCHET

Jestem pewien, że to widziałeś. Mam to z szybkiego serwisu społecznościowego google.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…
Gulzar Nazim

Nie rozumiem, jak to „upływa czas”, jeśli użytkownik nic nie robi. Wszystko to spowodowałoby prawdopodobnie wykonywanie logiki w tle, aż do naciśnięcia klawisza i kontynuowania innej logiki.
mphair

To prawda, należy to naprawić. Ale dość łatwo jest dodać limit czasu do warunku pętli.
Jonathan Allen,

KeyAvailablewskazuje tylko, że użytkownik rozpoczął wpisywanie danych wejściowych do ReadLine, ale potrzebujemy zdarzenia po naciśnięciu klawisza Enter, które powoduje powrót ReadLine. To rozwiązanie działa tylko dla ReadKey, tzn. Uzyskuje tylko jeden znak. Ponieważ nie rozwiązuje to rzeczywistego pytania dotyczącego ReadLine, nie mogę skorzystać z Twojego rozwiązania. -1 przepraszam
Roland

13

To zadziałało dla mnie.

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
Myślę, że to najlepsze i najprostsze rozwiązanie wykorzystujące już wbudowane narzędzia. Dobra robota!
Vippy

2
Piękny! Prostota to naprawdę najwyższe wyrafinowanie. Gratulacje!
BrunoSalvino,

10

Tak czy inaczej, potrzebujesz drugiego wątku. Możesz użyć asynchronicznych operacji we / wy, aby uniknąć deklarowania własnego:

  • zadeklaruj zdarzenie ManualResetEvent, nazwij je „evt”
  • wywołaj System.Console.OpenStandardInput, aby uzyskać strumień wejściowy. Określ metodę wywołania zwrotnego, która będzie przechowywać swoje dane i ustawiać evt.
  • Wywołaj metodę BeginRead tego strumienia, aby rozpocząć asynchroniczną operację odczytu
  • następnie wprowadź czasowe oczekiwanie na zdarzenie ManualResetEvent
  • jeśli czas oczekiwania minie, anuluj odczyt

Jeśli odczyt zwróci dane, ustaw zdarzenie, a Twój główny wątek będzie kontynuowany, w przeciwnym razie będziesz kontynuował po upływie limitu czasu.


To mniej więcej to, co robi zaakceptowane rozwiązanie.
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

Myślę, że będziesz musiał utworzyć dodatkowy wątek i odpytać o klucz na konsoli. Nie znam żadnego sposobu, aby to osiągnąć.


Tak, jeśli masz drugi wątek odpytywania kluczy, a twoja aplikacja zamyka się, gdy czeka, ten wątek odpytywania kluczy po prostu będzie czekał na zawsze.
Kelly Elton

Właściwie: albo drugi wątek, albo delegat z „BeginInvoke” (który używa wątku za kulisami - patrz odpowiedź z @gp).
Contango

@ kelton52, Czy wątek dodatkowy zostanie zamknięty, jeśli zakończysz proces w Menedżerze zadań?
Arlen Beiler

6

Walczyłem z tym problemem przez 5 miesięcy, zanim znalazłem rozwiązanie, które doskonale sprawdza się w środowisku biznesowym.

Problem z większością dotychczasowych rozwiązań polega na tym, że opierają się one na czymś innym niż Console.ReadLine () i Console.ReadLine () ma wiele zalet:

  • Obsługa usuwania, cofania, klawiszy strzałek itp.
  • Możliwość naciśnięcia klawisza „w górę” i powtórzenia ostatniego polecenia (jest to bardzo przydatne, jeśli zaimplementujesz konsolę debugowania w tle, która jest często używana).

Moje rozwiązanie jest następujące:

  1. Stwórz oddzielny wątek do obsługi danych wejściowych użytkownika za pomocą Console.ReadLine ().
  2. Po upływie limitu czasu odblokuj Console.ReadLine (), wysyłając klawisz [enter] do bieżącego okna konsoli, używając adresu http://inputsimulator.codeplex.com/ .

Przykładowy kod:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Więcej informacji o tej technice, w tym o prawidłowej technice przerywania wątku korzystającego z Console.ReadLine:

Wywołanie .NET w celu wysłania naciśnięcia klawisza [enter] do bieżącego procesu, który jest aplikacją konsoli?

Jak przerwać inny wątek w .NET, gdy ten wątek wykonuje Console.ReadLine?


5

Wywołanie Console.ReadLine () w delegacie jest złe, ponieważ jeśli użytkownik nie naciśnie klawisza „enter”, to wywołanie to nigdy nie powróci. Wątek wykonujący delegata zostanie zablokowany, dopóki użytkownik nie naciśnie klawisza „enter”, bez możliwości jego anulowania.

Wysłanie sekwencji takich wywołań nie będzie przebiegać zgodnie z oczekiwaniami. Rozważmy następujące kwestie (używając przykładowej klasy Console z powyższego):

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

Użytkownik zezwala na upłynięcie limitu czasu dla pierwszego monitu, a następnie wprowadza wartość dla drugiego monitu. Zarówno firstName, jak i lastName będą zawierać wartości domyślne. Kiedy użytkownik naciśnie „enter”, pierwsze wywołanie ReadLine zostanie zakończone, ale kod porzucił to wywołanie i zasadniczo odrzucił wynik. sekund wezwanie ReadLine nadal będzie blokować, timeout ostatecznie wygaśnie i wartość zwracana znowu będzie domyślnym.

BTW - w powyższym kodzie jest błąd. Wywołując waitHandle.Close () zamykasz zdarzenie spod wątku roboczego. Jeśli użytkownik naciśnie „enter” po wygaśnięciu limitu czasu, wątek roboczy spróbuje zasygnalizować zdarzenie, które zgłasza ObjectDisposedException. Wyjątek jest generowany z wątku roboczego, a jeśli nie skonfigurowałeś obsługi nieobsługiwanego wyjątku, proces zostanie zakończony.


1
Termin „powyżej” w Twoim poście jest niejednoznaczny i mylący. Jeśli odnosisz się do innej odpowiedzi, powinieneś umieścić odpowiedni link do tej odpowiedzi.
bzlm

5

Jeśli jesteś w Main()metodzie, nie możesz użyć await, więc będziesz musiał użyć Task.WaitAny():

var task = Task.Factory.StartNew(Console.ReadLine);
var result = Task.WaitAny(new Task[] { task }, TimeSpan.FromSeconds(5)) == 0
    ? task.Result : string.Empty;

Jednak C # 7.1 wprowadza możliwość tworzenia Main()metody asynchronicznej , więc lepiej jest używać Task.WhenAny()wersji zawsze, gdy masz taką opcję:

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

Być może za dużo czytam w tym pytaniu, ale zakładam, że oczekiwanie byłoby podobne do menu rozruchu, w którym czeka 15 sekund, chyba że naciśniesz klawisz. Możesz użyć (1) funkcji blokującej lub (2) możesz użyć wątku, zdarzenia i licznika czasu. Zdarzenie będzie działać jako „kontynuuj” i będzie blokowane do czasu wygaśnięcia licznika czasu lub naciśnięcia klawisza.

Pseudokod dla (1) to:

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

Nie mogę niestety komentować postu Gulzara, ale oto pełniejszy przykład:

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

Zauważ, że możesz również użyć Console.In.Peek (), jeśli konsola nie jest widoczna (?) Lub wejście jest kierowane z pliku.
Jamie Kitson

2

EDYCJA : naprawiono problem, wykonując rzeczywistą pracę w oddzielnym procesie i kończąc ten proces, jeśli upłynie limit czasu. Szczegóły poniżej. Uff!

Po prostu spróbowałem i wydawało się, że działa dobrze. Mój współpracownik miał wersję, która używała obiektu Thread, ale uważam, że metoda BeginInvoke () typów delegatów jest nieco bardziej elegancka.

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

Projekt ReadLine.exe jest bardzo prostym projektem, który ma jedną klasę, która wygląda następująco:

namespace ReadLine
{
   internal static class Program
   {
      private static void Main()
      {
         System.Console.WriteLine(System.Console.ReadLine());
      }
   }
}

2
Wywołanie oddzielnego pliku wykonywalnego w nowym procesie tylko w celu wykonania czasowej ReadLine () brzmi jak ogromna przesada. Zasadniczo rozwiązujesz problem polegający na niemożności przerwania ReadLine () - blokowania wątku, zamiast tego konfigurując i niszcząc cały proces.
bzlm

Następnie powiedz to Microsoftowi, który umieścił nas w takiej sytuacji.
Jesse C. Slicer

Microsoft nie postawił cię w takiej sytuacji. Spójrz na niektóre inne odpowiedzi, które wykonują tę samą pracę w kilku wierszach. Myślę, że powyższy kod powinien dostać jakąś nagrodę - ale nie taką, jaką chcesz :)
Contango

1
Nie, żadna z pozostałych odpowiedzi nie przyniosła dokładnie tego, czego chciał PO. Wszystkie z nich tracą cechy standardowych procedur wejściowych lub kończą się faktem, że wszystkie żądania Console.ReadLine() blokowane i wstrzymują wejście w następnym żądaniu. Przyjęta odpowiedź jest dość bliska, ale nadal ma ograniczenia.
Jesse C. Slicer,

1
Nie, nie jest. Bufor wejściowy nadal się blokuje (nawet jeśli program tego nie robi). Spróbuj sam: wprowadź kilka znaków, ale nie naciskaj klawisza Enter. Niech upłynie limit czasu. Przechwyć wyjątek w dzwoniącym. Następnie ReadLine()po wywołaniu tego miej w programie inny. Zobacz co się dzieje. Musisz dwukrotnie nacisnąć przycisk powrotu, aby go uruchomić ze względu na jednowątkowy charakter pliku Console. To. Nie. Praca.
Jesse C. Slicer

2

NET 4 sprawia, że ​​jest to niezwykle proste przy użyciu zadań.

Najpierw zbuduj pomocnika:

   Private Function AskUser() As String
      Console.Write("Answer my question: ")
      Return Console.ReadLine()
   End Function

Po drugie, wykonaj zadanie i poczekaj:

      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

Nie ma potrzeby odtwarzania funkcji ReadLine ani wykonywania innych niebezpiecznych hacków, aby to zadziałało. Zadania pozwalają nam rozwiązać problem w bardzo naturalny sposób.


2

Jakby nie było tu wystarczającej ilości odpowiedzi: 0), poniższy fragment zawiera w statycznej metodzie rozwiązanie @ kwl powyżej (pierwsze).

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

Stosowanie

    static void Main()
    {
        Console.WriteLine("howdy");
        string result = ConsoleReadLineWithTimeout(TimeSpan.FromSeconds(8.5));
        Console.WriteLine("bye");
    }

1

Prosty przykład gwintowania, aby rozwiązać ten problem

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

lub statyczny sznur do góry, aby uzyskać całą linię.


1

W moim przypadku to działa dobrze:

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

Czy to nie jest ładne i krótkie?

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

    // Handle keyInfo value here...
}

1
Co to do cholery jest SpinWait?
john ktejik

1

To jest pełniejszy przykład rozwiązania Glena Slaydena. Zdarzyło mi się to zrobić podczas tworzenia przypadku testowego dla innego problemu. Wykorzystuje asynchroniczne we / wy i zdarzenie resetowania ręcznego.

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

Mój kod jest w całości oparty na odpowiedzi znajomego @JSQuareD

Ale musiałem użyć Stopwatchtimera, ponieważ kiedy skończyłem program z Console.ReadKey()nim, wciąż czekałemConsole.ReadLine() i generował nieoczekiwane zachowanie.

U mnie DZIAŁAŁ IDEALNIE. Utrzymuje oryginalną 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

Przykładowa implementacja powyższego postu Erica. Ten konkretny przykład został użyty do odczytania informacji, które zostały przesłane do aplikacji konsoli za pośrednictwem potoku:

 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 == "?");

Zwróć uwagę, że jeśli pójdziesz ścieżką „Console.ReadKey”, stracisz niektóre fajne funkcje ReadLine, a mianowicie:

  • Obsługa usuwania, cofania, klawiszy strzałek itp.
  • Możliwość naciśnięcia klawisza „w górę” i powtórzenia ostatniego polecenia (jest to bardzo przydatne, jeśli zaimplementujesz konsolę debugowania w tle, która jest często używana).

Aby dodać limit czasu, zmień pętlę while, aby pasowała.


0

Proszę, nie nienawidź mnie za dodanie kolejnego rozwiązania do wielu istniejących odpowiedzi! Działa to dla Console.ReadKey (), ale można je łatwo zmodyfikować do pracy z ReadLine () itp.

Ponieważ metody „Console.Read” blokują się, konieczne jest „ szturchnięcie ” strumienia stdin do odwołania odczytu.

Składnia wywołania:

ConsoleKeyInfo keyInfo;
bool keyPressed = AsyncConsole.ReadKey(500, out keyInfo);
// where 500 is the timeout

Kod:

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

Oto rozwiązanie, które wykorzystuje Console.KeyAvailable. Są to wywołania blokujące, ale w razie potrzeby wywołanie ich asynchronicznie przez TPL powinno być dość trywialne. Użyłem standardowych mechanizmów anulowania, aby ułatwić połączenie ze wzorcem asynchronicznym zadania i innymi dobrymi rzeczami.

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

Ma to pewne wady.

  • Nie masz standardowych funkcji nawigacyjnych, które ReadLinezapewnia (przewijanie strzałkami w górę / w dół itp.).
  • Powoduje to wstrzyknięcie znaków „\ 0” do wejścia, jeśli zostanie naciśnięty specjalny klawisz (F1, PrtScn itp.). Możesz jednak łatwo je odfiltrować, modyfikując kod.

0

Skończyło się tutaj, ponieważ zadano zduplikowane pytanie. Wymyśliłem następujące rozwiązanie, które wygląda na proste. Jestem pewien, że ma kilka wad, które przegapiłem.

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

Doszedłem do tej odpowiedzi i skończyłem robiąc:

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

Prosty przykład wykorzystujący 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");
}

Co się stanie, jeśli użytkownik naciśnie klawisz i puści w ciągu 2000 ms?
Izzy

0

O wiele bardziej współczesny i oparty na zadaniach kod wyglądałby mniej więcej tak:

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

Miałem wyjątkową sytuację związaną z posiadaniem aplikacji Windows (usługa Windows). Podczas uruchamiania programu w trybie interaktywnym Environment.IsInteractive(VS Debugger lub z cmd.exe) użyłem AttachConsole / AllocConsole, aby uzyskać mój stdin / stdout. Aby proces nie kończył się podczas wykonywania pracy, wywoływany jest wątek interfejsu użytkownika Console.ReadKey(false). Chciałem anulować oczekiwanie, które wątek UI wykonywał z innego wątku, więc wymyśliłem modyfikację rozwiązania przez @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();
  }    
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.