Wie füge ich Console.ReadLine () ein Timeout hinzu?

122

Ich habe eine Konsolen-App, in der ich dem Benutzer x Sekunden geben möchte, um auf die Eingabeaufforderung zu antworten. Wenn nach einer bestimmten Zeit keine Eingabe erfolgt, sollte die Programmlogik fortgesetzt werden. Wir gehen davon aus, dass eine Zeitüberschreitung eine leere Antwort bedeutet.

Was ist der einfachste Weg, dies zu erreichen?

Larsenal
quelle

Antworten:

112

Ich bin überrascht zu erfahren, dass nach 5 Jahren alle Antworten immer noch unter einem oder mehreren der folgenden Probleme leiden:

  • Eine andere Funktion als ReadLine wird verwendet, was zu einem Funktionsverlust führt. (Löschen / Rücktaste / Aufwärts-Taste für vorherige Eingabe).
  • Die Funktion verhält sich schlecht, wenn sie mehrmals aufgerufen wird (Laichen mehrerer Threads, viele hängende ReadLines oder anderweitig unerwartetes Verhalten).
  • Die Funktion beruht auf einer Wartezeit. Dies ist eine schreckliche Verschwendung, da die Wartezeit voraussichtlich zwischen einigen Sekunden und dem Zeitlimit liegen kann, das mehrere Minuten betragen kann. Eine Wartezeit, die so lange dauert, ist ein schrecklicher Ressourcenverbrauch, der in einem Multithreading-Szenario besonders schlimm ist. Wenn das Besetzt-Warten mit einem Schlaf geändert wird, wirkt sich dies negativ auf die Reaktionsfähigkeit aus, obwohl ich zugebe, dass dies wahrscheinlich kein großes Problem ist.

Ich glaube, meine Lösung wird das ursprüngliche Problem lösen, ohne an einem der oben genannten Probleme zu leiden:

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

Das Anrufen ist natürlich sehr einfach:

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

Alternativ können Sie die TryXX(out)Konvention verwenden, wie von Shmueli vorgeschlagen:

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

Welches heißt wie folgt:

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

In beiden Fällen können Sie Anrufe nicht Readermit normalen Console.ReadLineAnrufen mischen : Wenn die ReaderZeit abgelaufen ist, wird ein ReadLineAnruf hängen bleiben . Wenn Sie stattdessen einen normalen (nicht zeitgesteuerten) ReadLineAnruf haben möchten , verwenden Sie einfach das Readerund lassen Sie das Zeitlimit weg, sodass standardmäßig ein unendliches Zeitlimit verwendet wird.

Wie wäre es also mit den Problemen der anderen Lösungen, die ich erwähnt habe?

  • Wie Sie sehen können, wird ReadLine verwendet, um das erste Problem zu vermeiden.
  • Die Funktion verhält sich bei mehrmaligem Aufruf ordnungsgemäß. Unabhängig davon, ob eine Zeitüberschreitung auftritt oder nicht, wird immer nur ein Hintergrundthread ausgeführt und höchstens ein Aufruf von ReadLine wird jemals aktiv sein. Das Aufrufen der Funktion führt immer zu der neuesten Eingabe oder zu einer Zeitüberschreitung, und der Benutzer muss nicht mehr als einmal die Eingabetaste drücken, um seine Eingabe zu senden.
  • Und natürlich beruht die Funktion nicht auf einer Wartezeit. Stattdessen werden geeignete Multithreading-Techniken verwendet, um die Verschwendung von Ressourcen zu verhindern.

Das einzige Problem, das ich bei dieser Lösung sehe, ist, dass sie nicht threadsicher ist. Mehrere Threads können den Benutzer jedoch nicht gleichzeitig zur Eingabe Reader.ReadLineauffordern. Daher sollte die Synchronisierung erfolgen, bevor Sie einen Anruf tätigen.

JSQuareD
quelle
1
Nach diesem Code wurde eine NullReferenceException angezeigt. Ich denke, ich konnte das Starten des Threads einmal korrigieren, wenn die automatischen Ereignisse erstellt wurden.
Augusto Pedraza
1
@JSQuareD Ich denke nicht, dass ein beschäftigtes Warten mit einem Schlaf (200 ms) so viel ist horrible waste, aber natürlich ist Ihre Signalisierung überlegen. Die Verwendung eines blockierenden Console.ReadLineAnrufs in einer Endlosschleife in einer zweiten Bedrohung verhindert außerdem die Probleme mit vielen solchen Anrufen, die im Hintergrund herumhängen, wie in den anderen, stark positiv bewerteten Lösungen unten. Vielen Dank, dass Sie Ihren Code geteilt haben. +1
Roland
2
Wenn Sie nicht rechtzeitig eingeben, scheint diese Methode beim ersten nachfolgenden Console.ReadLine()Aufruf zu brechen . Sie erhalten ein "Phantom" ReadLine, das zuerst ausgefüllt werden muss.
Derek
1
@Derek Sie können diese Methode leider nicht mit normalen ReadLine-Aufrufen mischen. Alle Aufrufe müssen über den Reader erfolgen. Die Lösung für dieses Problem wäre, dem Leser eine Methode hinzuzufügen, die ohne Zeitüberschreitung auf gotInput wartet. Ich bin derzeit auf dem Handy, daher kann ich es der Antwort nicht so einfach hinzufügen.
JSQuareD
1
Ich sehe keine Notwendigkeit dafür 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();
gp.
quelle
2
Ich weiß nicht, warum dies nicht gewählt wurde - es funktioniert absolut einwandfrei. Viele der anderen Lösungen beinhalten "ReadKey ()", was nicht richtig funktioniert: Dies bedeutet, dass Sie die gesamte Leistung von ReadLine () verlieren, z. B. das Drücken der Taste "up", um den zuvor eingegebenen Befehl mit der Rücktaste und zu erhalten Pfeiltasten usw.
Contango
9
@ Gravitas: Das funktioniert nicht. Nun, es funktioniert einmal. Aber jeder, den ReadLineSie anrufen, sitzt dort und wartet auf Eingaben. Wenn Sie es 100 Mal aufrufen, werden 100 Threads erstellt, die nicht alle verschwinden, bis Sie 100 Mal die Eingabetaste drücken!
Gabe
2
In acht nehmen. Diese Lösung scheint ordentlich zu sein, aber am Ende hingen 1000 unvollständige Anrufe hängen. Also nicht geeignet bei wiederholtem Anruf.
Tom Makin
@Gabe, shakinfree: Mehrere Anrufe wurden für die Lösung nicht berücksichtigt, sondern nur ein asynchroner Anruf mit Zeitüberschreitung. Ich denke, es wäre für den Benutzer verwirrend, 10 Nachrichten auf der Konsole zu drucken und dann die Eingaben nacheinander in der jeweiligen Reihenfolge einzugeben. Könnten Sie trotzdem versuchen, bei den hängenden Anrufen die TimedoutException-Zeile zu kommentieren und eine null / leere Zeichenfolge zurückzugeben?
gp.
Nein ... das Problem ist, dass Console.ReadLine immer noch den Threadpool-Thread blockiert, auf dem die Console.ReadLine-Methode von ReadLineDelegate ausgeführt wird.
gp.
27

Hilft dieser Ansatz mit 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);
    }
}
Gulzar Nazim
quelle
Dies ist wahr, das OP scheint einen blockierenden Anruf zu wollen, obwohl ich bei dem Gedanken ein bisschen schaudere ... Dies ist wahrscheinlich eine bessere Lösung.
GEOCHET
Ich bin sicher, Sie haben das gesehen. Ich
Gulzar Nazim
Ich sehe nicht, wie dieses "Timeout", wenn der Benutzer nichts tut. Dies würde möglicherweise dazu führen, dass die Logik im Hintergrund ausgeführt wird, bis eine Taste gedrückt wird und die andere Logik fortgesetzt wird.
mphair
Dies muss zwar behoben werden. Es ist jedoch einfach genug, das Zeitlimit zur Schleifenbedingung hinzuzufügen.
Jonathan Allen
KeyAvailableZeigt nur an, dass der Benutzer mit der Eingabe von ReadLine-Eingaben begonnen hat. Beim Drücken der Eingabetaste ist jedoch ein Ereignis erforderlich, durch das ReadLine zurückkehrt. Diese Lösung funktioniert nur für ReadKey, dh es wird nur ein Zeichen abgerufen. Da dies die eigentliche Frage für ReadLine nicht löst, kann ich Ihre Lösung nicht verwenden. -1 Entschuldigung
Roland
13

Das hat bei mir funktioniert.

ConsoleKeyInfo k = new ConsoleKeyInfo();
Console.WriteLine("Press any key in the next 5 seconds.");
for (int cnt = 5; cnt > 0; cnt--)
  {
    if (Console.KeyAvailable)
      {
        k = Console.ReadKey();
        break;
      }
    else
     {
       Console.WriteLine(cnt.ToString());
       System.Threading.Thread.Sleep(1000);
     }
 }
Console.WriteLine("The key pressed was " + k.Key);
user980750
quelle
4
Ich denke, dies ist die beste und einfachste Lösung mit bereits eingebauten Tools. Gut gemacht!
Vippy
2
Wunderschönen! Einfachheit ist wirklich die ultimative Raffinesse. Glückwunsch!
BrunoSalvino
10

Auf die eine oder andere Weise benötigen Sie einen zweiten Thread. Sie können asynchrone E / A verwenden, um zu vermeiden, dass Sie Ihre eigenen deklarieren:

  • Deklarieren Sie ein ManualResetEvent und nennen Sie es "evt".
  • Rufen Sie System.Console.OpenStandardInput auf, um den Eingabestream abzurufen. Geben Sie eine Rückrufmethode an, die ihre Daten speichert und evt festlegt.
  • Rufen Sie die BeginRead-Methode dieses Streams auf, um eine asynchrone Leseoperation zu starten
  • Geben Sie dann eine zeitgesteuerte Wartezeit für ein ManualResetEvent ein
  • Wenn die Wartezeit abgelaufen ist, brechen Sie den Lesevorgang ab

Wenn der Lesevorgang Daten zurückgibt, legen Sie das Ereignis fest und Ihr Hauptthread wird fortgesetzt, andernfalls fahren Sie nach dem Timeout fort.

Eric
quelle
Dies ist mehr oder weniger das, was die akzeptierte Lösung tut.
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.
}
Glenn Slayden
quelle
8

Ich denke, Sie müssen einen sekundären Thread erstellen und nach einem Schlüssel auf der Konsole suchen. Ich kenne keinen eingebauten Weg, um dies zu erreichen.

GEOCHET
quelle
Ja, wenn Sie einen zweiten Thread haben, der nach Schlüsseln abfragt, und Ihre App geschlossen wird, während sie dort sitzt und wartet, wird dieser Thread zum Abrufen von Schlüsseln einfach dort sitzen und für immer warten.
Kelly Elton
Eigentlich: entweder ein zweiter Thread oder ein Delegat mit "BeginInvoke" (der einen Thread hinter den Kulissen verwendet - siehe Antwort von @gp).
Contango
@ kelton52, Wird der sekundäre Thread beendet, wenn Sie den Prozess im Task-Manager beenden?
Arlen Beiler
6

Ich hatte 5 Monate lang mit diesem Problem zu kämpfen, bevor ich eine Lösung fand, die in einem Unternehmen perfekt funktioniert.

Das Problem bei den meisten bisherigen Lösungen besteht darin, dass sie sich auf etwas anderes als Console.ReadLine () stützen, und Console.ReadLine () bietet viele Vorteile:

  • Unterstützung für Löschen, Rücktaste, Pfeiltasten usw.
  • Die Möglichkeit, die Auf-Taste zu drücken und den letzten Befehl zu wiederholen (dies ist sehr praktisch, wenn Sie eine Hintergrund-Debugging-Konsole implementieren, die häufig verwendet wird).

Meine Lösung lautet wie folgt:

  1. Erstellen Sie einen separaten Thread , um die Benutzereingaben mit Console.ReadLine () zu verarbeiten.
  2. Entsperren Sie nach Ablauf des Zeitlimits Console.ReadLine (), indem Sie unter http://inputsimulator.codeplex.com/ eine [Eingabetaste] in das aktuelle Konsolenfenster senden .

Beispielcode:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Weitere Informationen zu dieser Technik, einschließlich der richtigen Technik zum Abbrechen eines Threads, der Console.ReadLine verwendet:

.NET-Aufruf zum Senden eines Tastendrucks [Enter] in den aktuellen Prozess. Welche Konsolen-App ist das?

Wie kann ich einen anderen Thread in .NET abbrechen, wenn dieser Thread Console.ReadLine ausführt?

Contango
quelle
5

Das Aufrufen von Console.ReadLine () im Delegaten ist fehlerhaft, da dieser Aufruf niemals zurückgegeben wird, wenn der Benutzer nicht die Eingabetaste drückt. Der Thread, der den Delegaten ausführt, wird blockiert, bis der Benutzer die Eingabetaste drückt, ohne dass er abgebrochen werden kann.

Das Ausgeben einer Folge dieser Aufrufe verhält sich nicht wie erwartet. Beachten Sie Folgendes (anhand des Beispiels Konsolenklasse von oben):

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

Der Benutzer lässt das Zeitlimit für die erste Eingabeaufforderung ablaufen und gibt dann einen Wert für die zweite Eingabeaufforderung ein. Sowohl Vorname als auch Nachname enthalten die Standardwerte. Wenn der Benutzer die Eingabetaste drückt, wird der erste ReadLine-Aufruf abgeschlossen, aber der Code hat diesen Aufruf abgebrochen und das Ergebnis im Wesentlichen verworfen. Der zweite ReadLine-Aufruf wird weiterhin blockiert, das Zeitlimit läuft schließlich ab und der zurückgegebene Wert ist wieder der Standardwert.

Übrigens: Der obige Code enthält einen Fehler. Durch Aufrufen von waitHandle.Close () schließen Sie das Ereignis unter dem Arbeitsthread aus. Wenn der Benutzer nach Ablauf des Zeitlimits die Eingabetaste drückt, versucht der Arbeitsthread, das Ereignis zu signalisieren, das eine ObjectDisposedException auslöst. Die Ausnahme wird vom Arbeitsthread ausgelöst. Wenn Sie keinen nicht behandelten Ausnahmebehandler eingerichtet haben, wird der Prozess beendet.

Brannon
quelle
1
Der Begriff "oben" in Ihrem Beitrag ist mehrdeutig und verwirrend. Wenn Sie sich auf eine andere Antwort beziehen, sollten Sie einen geeigneten Link zu dieser Antwort erstellen.
Bzlm
5

Wenn Sie in der Main()Methode sind, können Sie nicht verwenden await, also müssen Sie verwenden 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 bietet jedoch die Möglichkeit, eine asynchrone Main()Methode zu erstellen. Daher ist es besser, die Task.WhenAny()Version zu verwenden, wenn Sie diese Option haben:

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;
kwl
quelle
4

Ich lese möglicherweise zu viel in die Frage, aber ich gehe davon aus, dass das Warten dem Startmenü ähnelt, in dem es 15 Sekunden wartet, es sei denn, Sie drücken eine Taste. Sie können entweder (1) eine Blockierungsfunktion oder (2) einen Thread, ein Ereignis und einen Timer verwenden. Das Ereignis würde als "Weiter" fungieren und blockieren, bis entweder der Timer abgelaufen ist oder eine Taste gedrückt wurde.

Pseudocode für (1) wäre:

// 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);
}
Ryan
quelle
2

Ich kann Gulzars Beitrag leider nicht kommentieren, aber hier ist ein ausführlicheres Beispiel:

            while (Console.KeyAvailable == false)
            {
                Thread.Sleep(250);
                i++;
                if (i > 3)
                    throw new Exception("Timedout waiting for input.");
            }
            input = Console.ReadLine();
Jamie Kitson
quelle
Beachten Sie, dass Sie Console.In.Peek () auch verwenden können, wenn die Konsole nicht sichtbar ist (?) Oder die Eingabe aus einer Datei stammt.
Jamie Kitson
2

BEARBEITEN : Das Problem wurde behoben, indem die eigentliche Arbeit in einem separaten Prozess ausgeführt wurde und dieser Prozess abgebrochen wurde, wenn das Zeitlimit überschritten wurde. Siehe unten für Details. Wütend!

Ich habe es nur ausprobiert und es schien gut zu funktionieren. Mein Kollege hatte eine Version, die ein Thread-Objekt verwendete, aber ich finde die BeginInvoke () -Methode von Delegatentypen etwas eleganter.

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

Das ReadLine.exe-Projekt ist sehr einfach und hat eine Klasse, die so aussieht:

namespace ReadLine
{
   internal static class Program
   {
      private static void Main()
      {
         System.Console.WriteLine(System.Console.ReadLine());
      }
   }
}
Jesse C. Slicer
quelle
2
Das Aufrufen einer separaten ausführbaren Datei in einem neuen Prozess, nur um eine zeitgesteuerte ReadLine () auszuführen, klingt nach einem massiven Overkill. Sie lösen im Wesentlichen das Problem, dass Sie einen ReadLine () - blockierenden Thread nicht abbrechen können, indem Sie stattdessen einen gesamten Prozess einrichten und abreißen.
Bzlm
Dann teilen Sie es Microsoft mit, das uns in diese Position gebracht hat.
Jesse C. Slicer
Microsoft hat Sie nicht in diese Position gebracht. Schauen Sie sich in einigen Zeilen einige der anderen Antworten an, die den gleichen Job machen. Ich denke, der obige Code sollte eine Art Auszeichnung bekommen - aber nicht die Art, die Sie wollen :)
Contango
1
Nein, keine der anderen Antworten hat genau das getan, was das OP wollte. Alle von ihnen verlieren Funktionen der Standard - Eingaberoutinen oder Fahren Sie auf die Tatsache aufgehängt , dass alle Anfragen an Console.ReadLine() sind und auf die nächste Anforderung wird halten Eingang blockiert. Die akzeptierte Antwort ist ziemlich nah, hat aber immer noch Einschränkungen.
Jesse C. Slicer
1
Ähm, nein, ist es nicht. Der Eingabepuffer blockiert weiterhin (auch wenn das Programm dies nicht tut). Probieren Sie es aus: Geben Sie einige Zeichen ein, aber drücken Sie nicht die Eingabetaste. Lass es Timeout. Erfassen Sie die Ausnahme im Anrufer. Haben ReadLine()Sie dann einen anderen in Ihrem Programm, nachdem Sie diesen aufgerufen haben. Schau was passiert. Sie müssen ZWEIMAL die Eingabetaste drücken, um es zum Laufen zu bringen, da die Single-Thread-Funktion des Console. Es. Tut nicht. Arbeit.
Jesse C. Slicer
2

.NET 4 macht dies mithilfe von Aufgaben unglaublich einfach.

Bauen Sie zuerst Ihren Helfer:

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

Zweitens mit einer Aufgabe ausführen und warten:

      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

Es gibt keinen Versuch, die ReadLine-Funktionalität neu zu erstellen oder andere gefährliche Hacks durchzuführen, um dies zum Laufen zu bringen. Mit Aufgaben können wir die Frage auf ganz natürliche Weise lösen.

StevoInco
quelle
2

Als ob es hier nicht schon genug Antworten gäbe: 0), kapselt das Folgende in eine statische Methode @ kwls Lösung oben (die erste).

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

Verwendung

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

Einfaches Threading-Beispiel, um dies zu lösen

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

oder eine statische Zeichenfolge oben, um eine ganze Zeile zu erhalten.

mphair
quelle
1

Ich bin mein Fall, das funktioniert gut:

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;
}
Sasha
quelle
1

Ist das nicht schön kurz?

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

    // Handle keyInfo value here...
}
John Atac
quelle
1
Was zum Teufel ist SpinWait?
John Ktejik
1

Dies ist ein umfassenderes Beispiel für die Lösung von Glen Slayden. Ich habe dies gerne gemacht, als ich einen Testfall für ein anderes Problem erstellt habe. Es verwendet asynchrone E / A und ein manuelles Rücksetzereignis.

public static void Main() {
    bool readInProgress = false;
    System.IAsyncResult result = null;
    var stop_waiting = new System.Threading.ManualResetEvent(false);
    byte[] buffer = new byte[256];
    var s = System.Console.OpenStandardInput();
    while (true) {
        if (!readInProgress) {
            readInProgress = true;
            result = s.BeginRead(buffer, 0, buffer.Length
              , ar => stop_waiting.Set(), null);

        }
        bool signaled = true;
        if (!result.IsCompleted) {
            stop_waiting.Reset();
            signaled = stop_waiting.WaitOne(5000);
        }
        else {
            signaled = true;
        }
        if (signaled) {
            readInProgress = false;
            int numBytes = s.EndRead(result);
            string text = System.Text.Encoding.UTF8.GetString(buffer
              , 0, numBytes);
            System.Console.Out.Write(string.Format(
              "Thank you for typing: {0}", text));
        }
        else {
            System.Console.Out.WriteLine("oy, type something!");
        }
    }
mikemay
quelle
1

Mein Code basiert vollständig auf der Antwort des Freundes @JSQuareD

Aber ich musste den StopwatchTimer verwenden, denn als ich das Programm Console.ReadKey()damit beendet hatte, wartete es immer noch Console.ReadLine()und es erzeugte unerwartetes Verhalten.

Es hat perfekt für mich funktioniert. Behält die ursprüngliche Console.ReadLine () bei

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.");
            }
        }
    }
}
Sergio Cabral
quelle
0

Ein anderer billiger Weg, um einen zweiten Thread zu erhalten, besteht darin, ihn in einen Delegaten zu packen.

Joel Coehoorn
quelle
0

Beispielimplementierung von Erics Beitrag oben. In diesem Beispiel wurden Informationen gelesen, die über Pipe an eine Konsolen-App übergeben wurden:

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

quelle
0
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

Beachten Sie, dass Sie einige der coolen Funktionen von ReadLine verlieren, wenn Sie die Route "Console.ReadKey" wählen, nämlich:

  • Unterstützung für Löschen, Rücktaste, Pfeiltasten usw.
  • Die Möglichkeit, die Auf-Taste zu drücken und den letzten Befehl zu wiederholen (dies ist sehr praktisch, wenn Sie eine Hintergrund-Debugging-Konsole implementieren, die häufig verwendet wird).

Ändern Sie die while-Schleife entsprechend, um eine Zeitüberschreitung hinzuzufügen.

Contango
quelle
0

Bitte hasse mich nicht dafür, dass ich der Fülle der vorhandenen Antworten eine weitere Lösung hinzugefügt habe! Dies funktioniert für Console.ReadKey (), kann jedoch leicht geändert werden, um mit ReadLine () usw. zu arbeiten.

Da die „Console.Read“ Methoden blockiert werden, ist es notwendig, „ Schub des StdIn Stromes“ die Lese abzubrechen.

Aufrufsyntax:

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

Code:

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);
}
David Kirkland
quelle
0

Hier ist eine Lösung, die verwendet Console.KeyAvailable. Diese blockieren Anrufe, aber es sollte ziemlich trivial sein, sie auf Wunsch asynchron über die TPL aufzurufen. Ich habe die Standard-Stornierungsmechanismen verwendet, um die Verbindung mit dem Task Asynchronous Pattern und all den guten Dingen zu vereinfachen.

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

Dies hat einige Nachteile.

  • Sie erhalten nicht die Standardnavigationsfunktionen ReadLine(Bildlauf nach oben / unten usw.).
  • Dadurch werden '\ 0'-Zeichen in die Eingabe eingefügt, wenn eine Sondertaste gedrückt wird (F1, PrtScn usw.). Sie können sie jedoch leicht herausfiltern, indem Sie den Code ändern.
Brian Gideon
quelle
0

Bin hier gelandet, weil eine doppelte Frage gestellt wurde. Ich habe die folgende Lösung gefunden, die einfach aussieht. Ich bin sicher, es hat einige Nachteile, die ich verpasst habe.

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;
    }
}
Frank Rem
quelle
0

Ich bin zu dieser Antwort gekommen und habe am Ende Folgendes getan:

    /// <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);                
        }            
    }
Tono Nam
quelle
0

Ein einfaches Beispiel mit Console.KeyAvailable:

Console.WriteLine("Press any key during the next 2 seconds...");
Thread.Sleep(2000);
if (Console.KeyAvailable)
{
    Console.WriteLine("Key pressed");
}
else
{
    Console.WriteLine("You were too slow");
}
cprcrack
quelle
Was ist, wenn der Benutzer die Taste drückt und innerhalb von 2000 ms loslässt?
Izzy
0

Viel zeitgemäßerer und aufgabenbasierter Code würde ungefähr so ​​aussehen:

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();
}
Shonn Lyga
quelle
0

Ich hatte eine einzigartige Situation mit einer Windows-Anwendung (Windows-Dienst). Bei der interaktiven Ausführung des Programms Environment.IsInteractive(VS Debugger oder von cmd.exe) habe ich AttachConsole / AllocConsole verwendet, um mein stdin / stdout abzurufen. Um zu verhindern, dass der Prozess während der Arbeit beendet wird, ruft der UI-Thread auf Console.ReadKey(false). Ich wollte das Warten auf den UI-Thread von einem anderen Thread abbrechen, daher habe ich eine Änderung der Lösung von @JSquaredD gefunden.

using System;
using System.Diagnostics;

internal class PressAnyKey
{
  private static Thread inputThread;
  private static AutoResetEvent getInput;
  private static AutoResetEvent gotInput;
  private static CancellationTokenSource cancellationtoken;

  static PressAnyKey()
  {
    // Static Constructor called when WaitOne is called (technically Cancel too, but who cares)
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(ReaderThread);
    inputThread.IsBackground = true;
    inputThread.Name = "PressAnyKey";
    inputThread.Start();
  }

  private static void ReaderThread()
  {
    while (true)
    {
      // ReaderThread waits until PressAnyKey is called
      getInput.WaitOne();
      // Get here 
      // Inner loop used when a caller uses PressAnyKey
      while (!Console.KeyAvailable && !cancellationtoken.IsCancellationRequested)
      {
        Thread.Sleep(50);
      }
      // Release the thread that called PressAnyKey
      gotInput.Set();
    }
  }

  /// <summary>
  /// Signals the thread that called WaitOne should be allowed to continue
  /// </summary>
  public static void Cancel()
  {
    // Trigger the alternate ending condition to the inner loop in ReaderThread
    if(cancellationtoken== null) throw new InvalidOperationException("Must call WaitOne before Cancelling");
    cancellationtoken.Cancel();
  }

  /// <summary>
  /// Wait until a key is pressed or <see cref="Cancel"/> is called by another thread
  /// </summary>
  public static void WaitOne()
  {
    if(cancellationtoken==null || cancellationtoken.IsCancellationRequested) throw new InvalidOperationException("Must cancel a pending wait");
    cancellationtoken = new CancellationTokenSource();
    // Release the reader thread
    getInput.Set();
    // Calling thread will wait here indefiniately 
    // until a key is pressed, or Cancel is called
    gotInput.WaitOne();
  }    
}
JJS
quelle