Come aggiungere un timeout a Console.ReadLine ()?


122

Ho un'app console in cui desidero dare all'utente x secondi per rispondere al prompt. Se non viene effettuato alcun input dopo un certo periodo di tempo, la logica del programma dovrebbe continuare. Supponiamo che un timeout significhi una risposta vuota.

Qual è il modo più diretto per affrontare questo problema?

Risposte:


112

Sono sorpreso di apprendere che dopo 5 anni tutte le risposte soffrono ancora di uno o più dei seguenti problemi:

  • Viene utilizzata una funzione diversa da ReadLine, che causa la perdita di funzionalità. (Canc / backspace / tasto su per l'immissione precedente).
  • La funzione si comporta male quando viene richiamata più volte (generazione di più thread, molti ReadLine sospesi o comportamento imprevisto).
  • La funzione si basa su un'attesa impegnata. Il che è uno spreco orribile poiché l'attesa dovrebbe durare da un numero di secondi fino al timeout, che potrebbe essere di più minuti. Un'attesa impegnativa che dura per una tale quantità di tempo è un orribile risucchio di risorse, il che è particolarmente negativo in uno scenario di multithreading. Se l'attesa occupata viene modificata con uno sleep, ciò ha un effetto negativo sulla reattività, anche se ammetto che questo probabilmente non è un grosso problema.

Credo che la mia soluzione risolverà il problema originale senza soffrire di nessuno dei problemi di cui sopra:

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

La chiamata è, ovviamente, molto semplice:

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

In alternativa, puoi usare la TryXX(out)convenzione, come suggerito da 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;
  }

Che si chiama come segue:

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 entrambi i casi, non è possibile combinare chiamate Readercon Console.ReadLinechiamate normali : se Readerscade, ci sarà una ReadLinechiamata in sospeso . Invece, se vuoi avere una ReadLinechiamata normale (non a tempo) , usa semplicemente Readere ometti il ​​timeout, in modo che il valore predefinito sia un timeout infinito.

Allora che ne dici di quei problemi delle altre soluzioni che ho menzionato?

  • Come puoi vedere, viene utilizzato ReadLine, evitando il primo problema.
  • La funzione si comporta correttamente se richiamata più volte. Indipendentemente dal fatto che si verifichi o meno un timeout, solo un thread in background sarà sempre in esecuzione e solo una chiamata a ReadLine sarà sempre attiva. La chiamata alla funzione risulterà sempre nell'ultimo input o in un timeout e l'utente non dovrà premere invio più di una volta per inviare il suo input.
  • E, ovviamente, la funzione non si basa su un'attesa impegnativa. Utilizza invece tecniche di multithreading adeguate per evitare sprechi di risorse.

L'unico problema che prevedo con questa soluzione è che non è thread-safe. Tuttavia, più thread non possono realmente chiedere l'input all'utente contemporaneamente, quindi la sincronizzazione dovrebbe avvenire comunque prima di effettuare una chiamata a Reader.ReadLine.


1
Ho ricevuto una NullReferenceException seguendo questo codice. Penso che avrei potuto correggere l'avvio del thread una volta che vengono creati gli eventi automatici.
Augusto Pedraza

1
@JSQuareD Non penso che un'attesa impegnata con uno Sleep (200 ms) sia così tanto horrible waste, ma ovviamente la tua segnalazione è superiore. Inoltre, l'utilizzo di una Console.ReadLinechiamata di blocco in un ciclo infinito in una seconda minaccia previene i problemi con molte di tali chiamate in background come nelle altre soluzioni, fortemente votate, di seguito. Grazie per aver condiviso il tuo codice. +1
Roland

2
Se non si immette in tempo, questo metodo sembra interrompersi alla prima Console.ReadLine()chiamata successiva che si effettua. Finisci con un "fantasma" ReadLineche deve essere completato prima.
Derek

1
@Derek sfortunatamente, non puoi combinare questo metodo con le normali chiamate ReadLine, tutte le chiamate devono essere effettuate tramite Reader. La soluzione a questo problema sarebbe aggiungere un metodo al lettore che attende gotInput senza timeout. Al momento sono su un dispositivo mobile, quindi non posso aggiungerlo alla risposta molto facilmente.
JSQuareD

1
Non vedo la necessità di 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
Non so perché questo non sia stato votato - funziona in modo assolutamente impeccabile. Molte delle altre soluzioni coinvolgono "ReadKey ()", che non funziona correttamente: significa che si perde tutta la potenza di ReadLine (), come premere il tasto "su" per ottenere il comando digitato in precedenza, usando backspace e tasti freccia, ecc.
Contango

9
@Gravitas: questo non funziona. Bene, funziona una volta. Ma ogni volta ReadLineche chiami resta in attesa di input. Se lo chiami 100 volte, crea 100 thread che non scompaiono finché non premi Invio 100 volte!
Gabe

2
Attenzione. Questa soluzione sembra pulita, ma ho finito con migliaia di chiamate non completate lasciate in sospeso. Quindi non adatto se chiamato ripetutamente.
Tom Makin

@Gabe, shakinfree: più chiamate non sono state prese in considerazione per la soluzione, ma solo una chiamata asincrona con timeout. Immagino che sarebbe fonte di confusione per l'utente avere 10 messaggi stampati sulla console e quindi inserire gli input per loro uno per uno nel rispettivo ordine. Tuttavia, per le chiamate in sospeso, potresti provare a commentare la riga TimedoutException e restituire una stringa nulla / vuota?
gp.

no ... il problema è che Console.ReadLine sta ancora bloccando il thread del pool di thread che esegue il metodo Console.ReadLine da ReadLineDelegate.
gp.

27

Questo approccio utilizza Console.KeyAvailable aiuto?

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

Questo è vero, l'OP sembra volere una chiamata di blocco, anche se rabbrividisco un po 'al pensiero ... Questa è probabilmente una soluzione migliore.
GEOCHET

Sono sicuro che l'hai visto. Ottenuto da un rapido google social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…
Gulzar Nazim,

Non vedo come questo "timeout" se l'utente non fa nulla. Tutto ciò che farebbe è possibilmente continuare a eseguire la logica in background finché non viene premuto un tasto e l'altra logica continua.
mphair

È vero, questo deve essere risolto. Ma è abbastanza facile aggiungere il timeout alla condizione del ciclo.
Jonathan Allen

KeyAvailableindica solo che l'utente ha iniziato a digitare l'input in ReadLine, ma abbiamo bisogno di un evento alla pressione di Invio, che fa tornare ReadLine. Questa soluzione funziona solo per ReadKey, ovvero per ottenere un solo carattere. Poiché questo non risolve la domanda effettiva per ReadLine, non posso utilizzare la tua soluzione. -1 scusa
Roland

13

Questo ha funzionato per me.

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
Penso che questa sia la soluzione migliore e più semplice utilizzando strumenti già integrati. Ottimo lavoro!
Vippy

2
Bellissimo! La semplicità è davvero la massima raffinatezza. Congratulazioni!
BrunoSalvino

10

In un modo o nell'altro hai bisogno di un secondo thread. È possibile utilizzare IO asincrono per evitare di dichiarare il proprio:

  • dichiarare un ManualResetEvent, chiamalo "evt"
  • chiama System.Console.OpenStandardInput per ottenere il flusso di input. Specificare un metodo di callback che memorizzerà i propri dati e imposterà evt.
  • chiamare il metodo BeginRead di quel flusso per avviare un'operazione di lettura asincrona
  • quindi immettere un'attesa a tempo su un ManualResetEvent
  • se l'attesa scade, annulla la lettura

Se la lettura restituisce dati, imposta l'evento e il thread principale continuerà, altrimenti continuerai dopo il timeout.


Questo è più o meno ciò che fa la soluzione accettata.
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

Penso che dovrai creare un thread secondario ed eseguire il polling per una chiave sulla console. Non conosco alcun modo integrato per ottenere questo risultato.


Sì, se hai un secondo thread che esegue il polling delle chiavi e la tua app si chiude mentre è seduta lì in attesa, quel thread di polling delle chiavi rimarrà lì, in attesa, per sempre.
Kelly Elton

In realtà: un secondo thread o un delegato con "BeginInvoke" (che utilizza un thread, dietro le quinte - vedere la risposta di @gp).
Contango

@ kelton52, Il thread secondario verrà chiuso se si termina il processo in Task Manager?
Arlen Beiler

6

Ho lottato con questo problema per 5 mesi prima di trovare una soluzione che funzionasse perfettamente in un ambiente aziendale.

Il problema con la maggior parte delle soluzioni finora è che si basano su qualcosa di diverso da Console.ReadLine () e Console.ReadLine () ha molti vantaggi:

  • Supporto per cancellazione, backspace, tasti freccia, ecc.
  • La possibilità di premere il tasto "su" e ripetere l'ultimo comando (questo è molto utile se si implementa una console di debug in background che viene utilizzata molto).

La mia soluzione è la seguente:

  1. Genera un thread separato per gestire l'input dell'utente utilizzando Console.ReadLine ().
  2. Dopo il periodo di timeout, sbloccare Console.ReadLine () inviando un tasto [invio] nella finestra della console corrente, utilizzando http://inputsimulator.codeplex.com/ .

Codice di esempio:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Ulteriori informazioni su questa tecnica, inclusa la tecnica corretta per interrompere un thread che utilizza Console.ReadLine:

Chiamata .NET per inviare la sequenza di tasti [invio] nel processo corrente, che è un'app console?

Come interrompere un altro thread in .NET, quando detto thread sta eseguendo Console.ReadLine?


5

Chiamare Console.ReadLine () nel delegato non è corretto perché se l'utente non preme "invio", la chiamata non verrà mai restituita. Il thread che esegue il delegato verrà bloccato finché l'utente non preme "invio", senza alcun modo per annullarlo.

L'emissione di una sequenza di queste chiamate non si comporterà come ci si aspetterebbe. Considera quanto segue (utilizzando la classe Console di esempio dall'alto):

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

L'utente lascia scadere il timeout per il primo prompt, quindi immette un valore per il secondo prompt. Sia firstName che lastName conterranno i valori predefiniti. Quando l'utente preme "invio", la prima chiamata ReadLine verrà completata, ma il codice ha abbandonato quella chiamata e sostanzialmente ha scartato il risultato. La seconda chiamata ReadLine continuerà a bloccarsi, il timeout scadrà e il valore restituito sarà nuovamente quello predefinito.

BTW- C'è un bug nel codice sopra. Chiamando waitHandle.Close () si chiude l'evento da sotto il thread di lavoro. Se l'utente preme "invio" dopo la scadenza del timeout, il thread di lavoro tenterà di segnalare l'evento che genera un'eccezione ObjectDisposedException. L'eccezione viene generata dal thread di lavoro e se non hai configurato un gestore di eccezioni non gestito il tuo processo verrà terminato.


1
Il termine "sopra" nel tuo post è ambiguo e confuso. Se ti riferisci a un'altra risposta, dovresti creare un collegamento appropriato a quella risposta.
bzlm

5

Se sei nel Main()metodo, non puoi usare await, quindi dovrai usare Task.WaitAny():

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

Tuttavia, C # 7.1 introduce la possibilità di creare un Main()metodo asincrono , quindi è meglio usare la Task.WhenAny()versione ogni volta che hai questa opzione:

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

Potrei leggere troppo nella domanda, ma presumo che l'attesa sarebbe simile al menu di avvio in cui attende 15 secondi a meno che non si prema un tasto. Puoi usare (1) una funzione di blocco o (2) puoi usare un thread, un evento e un timer. L'evento avrebbe agito come un "continua" e si sarebbe bloccato fino alla scadenza del timer o alla pressione di un tasto.

Lo pseudo codice per (1) sarebbe:

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

Sfortunatamente non posso commentare il post di Gulzar, ma ecco un esempio più completo:

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

Nota che puoi anche usare Console.In.Peek () se la console non è visibile (?) O l'input è diretto da un file.
Jamie Kitson

2

MODIFICA : risolto il problema facendo eseguire il lavoro effettivo in un processo separato e uccidendo quel processo se scade. Vedi sotto per i dettagli. Meno male!

Ho appena provato e sembrava funzionare bene. Il mio collega aveva una versione che utilizzava un oggetto Thread, ma trovo che il metodo BeginInvoke () dei tipi delegati sia un po 'più elegante.

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

Il progetto ReadLine.exe è molto semplice che ha una classe che assomiglia a questo:

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

2
Invocare un eseguibile separato in un nuovo processo solo per eseguire una ReadLine () temporizzata suona come un enorme sovraccarico. Stai essenzialmente risolvendo il problema di non essere in grado di interrompere un thread di blocco ReadLine () impostando e abbattendo invece un intero processo.
bzlm

Quindi dillo a Microsoft, che ci ha messo in questa posizione.
Jesse C. Slicer

Microsoft non ti ha messo in quella posizione. Guarda alcune delle altre risposte che fanno lo stesso lavoro in poche righe. Penso che il codice sopra dovrebbe ottenere una sorta di premio, ma non il tipo che desideri :)
Contango

1
No, nessuna delle altre risposte ha fatto esattamente quello che voleva l'OP. Tutti perdono le funzionalità delle routine di input standard o Console.ReadLine() restano bloccati dal fatto che tutte le richieste sono bloccate e manterranno l'input alla richiesta successiva. La risposta accettata è abbastanza vicina, ma ha ancora dei limiti.
Jesse C. Slicer

1
Ehm, no, non lo è. Il buffer di input si blocca ancora (anche se il programma non lo fa). Provalo tu stesso: inserisci alcuni caratteri ma non premere invio. Lascia che scada. Cattura l'eccezione nel chiamante. Quindi averne un altro ReadLine()nel tuo programma dopo aver chiamato questo. Guarda cosa succede. Devi premere Invio DUE VOLTE per farlo andare a causa della natura a thread singolo di Console. Si. Non lo fa. Lavoro.
Jesse C. Slicer

2

.NET 4 lo rende incredibilmente semplice usando Tasks.

Per prima cosa, costruisci il tuo aiutante:

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

Secondo, esegui con un'attività e attendi:

      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

Non si cerca di ricreare la funzionalità ReadLine o di eseguire altri pericolosi hack per farlo funzionare. I compiti ci consentono di risolvere la domanda in modo molto naturale.


2

Come se non ci fossero già abbastanza risposte qui: 0), quanto segue incapsula in un metodo statico la soluzione di @ kwl sopra (la prima).

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

uso

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

1

Semplice esempio di threading per risolvere questo problema

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

o una stringa statica in alto per ottenere un'intera linea.


1

Nel mio caso questo funziona bene:

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

Non è carino e breve?

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

    // Handle keyInfo value here...
}

1
Che diavolo è SpinWait?
john ktejik

1

Questo è un esempio più completo della soluzione di Glen Slayden. Sono riuscito a farlo quando ho creato un test case per un altro problema. Utilizza l'I / O asincrono e un evento di ripristino manuale.

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

Il mio codice si basa interamente sulla risposta dell'amico @JSQuareD

Ma avevo bisogno di usare il Stopwatchtimer perché quando ho finito il programma con Console.ReadKey()esso era ancora in attesa Console.ReadLine()e ha generato un comportamento inaspettato.

Ha funzionato perfettamente per me. Mantiene la Console originale.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

Un altro modo economico per ottenere un secondo thread è avvolgerlo in un delegato.


0

Esempio di implementazione del post di Eric sopra. Questo particolare esempio è stato utilizzato per leggere le informazioni passate a un'app console tramite pipe:

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

Nota che se segui il percorso "Console.ReadKey", perdi alcune delle fantastiche funzionalità di ReadLine, vale a dire:

  • Supporto per cancellazione, backspace, tasti freccia, ecc.
  • La possibilità di premere il tasto "su" e ripetere l'ultimo comando (questo è molto utile se si implementa una console di debug in background che viene utilizzata molto).

Per aggiungere un timeout, modifica il ciclo while per adattarlo.


0

Per favore, non odiarmi per aver aggiunto un'altra soluzione alla pletora di risposte esistenti! Funziona per Console.ReadKey (), ma potrebbe essere facilmente modificato per funzionare con ReadLine (), ecc.

Poiché i metodi "Console.Read" si bloccano, è necessario " spingere " il flusso StdIn per annullare la lettura.

Richiamo della sintassi:

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

Codice:

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

Ecco una soluzione che utilizza Console.KeyAvailable. Si tratta di chiamate di blocco, ma dovrebbe essere abbastanza banale chiamarle in modo asincrono tramite TPL, se lo si desidera. Ho usato i meccanismi di cancellazione standard per semplificare il collegamento con il Task Asynchronous Pattern e tutte quelle cose buone.

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

Ci sono alcuni svantaggi con questo.

  • Non ottieni le funzionalità di navigazione standard che ReadLine fornite (scorrimento con freccia su / giù, ecc.).
  • Questo inserisce i caratteri "\ 0" nell'input se viene premuto un tasto speciale (F1, PrtScn, ecc.). Puoi filtrarli facilmente modificando il codice.

0

Sono finito qui perché è stata posta una domanda duplicata. Ho trovato la seguente soluzione che sembra semplice. Sono sicuro che abbia alcuni inconvenienti che mi sono perso.

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

Sono arrivato a questa risposta e finisco per fare:

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

Un semplice esempio che utilizza 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");
}

Cosa succede se l'utente preme il tasto e lo lascia andare entro 2000 ms?
Izzy

0

Il codice molto più contemporaneo e basato su attività sarebbe simile a questo:

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

Ho avuto una situazione unica di avere un'applicazione Windows (servizio Windows). Durante l'esecuzione del programma in modo interattivo Environment.IsInteractive(VS Debugger o da cmd.exe), ho utilizzato AttachConsole / AllocConsole per ottenere il mio stdin / stdout. Per evitare che il processo termini durante il lavoro, il thread dell'interfaccia utente chiama Console.ReadKey(false). Volevo annullare l'attesa che il thread dell'interfaccia utente stava facendo da un altro thread, quindi ho apportato una modifica alla soluzione di @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();
  }    
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.