Pregunta

Tengo una aplicación de consola en la que quiero darle al usuario X segundos para responder al mensaje.Si no se realiza ninguna entrada después de un cierto período de tiempo, la lógica del programa debe continuar.Asumimos que un tiempo de espera significa una respuesta vacía.

¿Cuál es la forma más sencilla de abordar esto?

¿Fue útil?

Solución

Me sorprende saber que después de 5 años, todas las respuestas todavía sufren uno o más de los siguientes problemas:

  • Se utiliza una función distinta a ReadLine, lo que provoca pérdida de funcionalidad.(Eliminar/retroceder/tecla arriba para la entrada anterior).
  • La función se comporta mal cuando se invoca varias veces (genera múltiples subprocesos, muchos ReadLine cuelgan o comportamiento inesperado).
  • La función se basa en una espera ocupada.Lo cual es un desperdicio horrible, ya que se espera que la espera dure desde varios segundos hasta el tiempo de espera, que puede ser de varios minutos.Una espera ocupada que dura tanto tiempo es una terrible pérdida de recursos, lo cual es especialmente malo en un escenario de subprocesos múltiples.Si la espera ocupada se modifica con un sueño, esto tiene un efecto negativo en la capacidad de respuesta, aunque admito que probablemente esto no sea un gran problema.

Creo que mi solución resolverá el problema original sin sufrir ninguno de los problemas anteriores:

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

Por supuesto, llamar es muy sencillo:

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

Alternativamente, puede utilizar el TryXX(out) convención, como sugirió 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;
  }

Que se llama de la siguiente manera:

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

En ambos casos, no puedes mezclar llamadas a Reader con normalidad Console.ReadLine llamadas:Si el Reader Se agota el tiempo, habrá un ahorcamiento. ReadLine llamar.En cambio, si quieres tener una sesión normal (no cronometrada) ReadLine llama, solo usa el Reader y omita el tiempo de espera, de modo que el valor predeterminado sea un tiempo de espera infinito.

Entonces, ¿qué pasa con los problemas de las otras soluciones que mencioné?

  • Como puede ver, se utiliza ReadLine, evitando el primer problema.
  • La función se comporta correctamente cuando se invoca varias veces.Independientemente de si se produce un tiempo de espera o no, solo se ejecutará un subproceso en segundo plano y solo estará activa como máximo una llamada a ReadLine.Llamar a la función siempre dará como resultado la última entrada, o un tiempo de espera, y el usuario no tendrá que presionar Enter más de una vez para enviar su entrada.
  • Y, obviamente, la función no depende de una espera ocupada.En su lugar, utiliza técnicas adecuadas de subprocesos múltiples para evitar el desperdicio de recursos.

El único problema que preveo con esta solución es que no es segura para subprocesos.Sin embargo, varios subprocesos realmente no pueden solicitar información al usuario al mismo tiempo, por lo que la sincronización debería realizarse antes de realizar una llamada a Reader.ReadLine de todos modos.

Otros consejos

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

¿Este enfoque utilizará Consola.ClaveDisponible ¿ayuda?

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

De una forma u otra necesitas un segundo hilo.Podrías usar IO asíncrono para evitar declarar el tuyo propio:

  • declare un ManualResetEvent, llámelo "evt"
  • llame a System.Console.OpenStandardInput para obtener el flujo de entrada.Especifique un método de devolución de llamada que almacenará sus datos y configurará evt.
  • llame al método BeginRead de esa secuencia para iniciar una operación de lectura asincrónica
  • luego ingrese una espera cronometrada en un ManualResetEvent
  • Si se agota el tiempo de espera, cancele la lectura.

Si la lectura devuelve datos, configure el evento y su hilo principal continuará; de lo contrario, continuará después del tiempo de espera.

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

Esto funcionó para mí.

ConsoleKeyInfo k = new ConsoleKeyInfo();
Console.WriteLine("Press any key in the next 5 seconds.");
for (int cnt = 5; cnt > 0; cnt--)
  {
    if (Console.KeyAvailable == true)
      {
        k = Console.ReadKey();
        break;
      }
    else
     {
       Console.WriteLine(cnt.ToString());
       System.Threading.Thread.Sleep(1000);
     }
 }
Console.WriteLine("The key pressed was " + k.Key);

Creo que necesitarás crear un hilo secundario y buscar una clave en la consola.No conozco ninguna forma integrada de lograr esto.

Luché con este problema durante 5 meses antes de encontrar una solución que funcione perfectamente en un entorno empresarial.

El problema con la mayoría de las soluciones hasta ahora es que dependen de algo más que Console.ReadLine(), y Console.ReadLine() tiene muchas ventajas:

  • Soporte para eliminar, retroceder, teclas de flecha, etc.
  • La capacidad de presionar la tecla "arriba" y repetir el último comando (esto resulta muy útil si implementa una consola de depuración en segundo plano que se usa mucho).

Mi solución es la siguiente:

  1. Generar un hilo separado para manejar la entrada del usuario usando Console.ReadLine().
  2. Después del período de tiempo de espera, desbloquee Console.ReadLine() enviando una tecla [enter] a la ventana actual de la consola, usando http://inputsimulator.codeplex.com/.

Código de muestra:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Más información sobre esta técnica, incluida la técnica correcta para cancelar un hilo que usa Console.ReadLine:

Llamada .NET para enviar la pulsación de tecla [ingresar] al proceso actual, ¿cuál es una aplicación de consola?

¿Cómo abortar otro hilo en .NET, cuando dicho hilo está ejecutando Console.ReadLine?

Llamar a Console.ReadLine() en el delegado es malo porque si el usuario no presiona 'enter', esa llamada nunca volverá.El hilo que ejecuta el delegado se bloqueará hasta que el usuario presione 'enter', sin forma de cancelarlo.

Emitir una secuencia de estas llamadas no se comportará como cabría esperar.Considere lo siguiente (usando el ejemplo de clase Consola de arriba):

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

El usuario deja que expire el tiempo de espera para el primer mensaje y luego ingresa un valor para el segundo mensaje.Tanto el nombre como el apellido contendrán los valores predeterminados.Cuando el usuario presiona 'enter', el primero La llamada ReadLine se completará, pero el código abandonó esa llamada y esencialmente descartó el resultado.El segundo La llamada ReadLine continuará bloqueándose, el tiempo de espera eventualmente expirará y el valor devuelto volverá a ser el predeterminado.

Por cierto, hay un error en el código anterior.Al llamar a waitHandle.Close(), cierra el evento desde debajo del hilo de trabajo.Si el usuario presiona 'enter' después de que expire el tiempo de espera, el subproceso de trabajo intentará señalar el evento que genera una excepción ObjectDisposedException.La excepción se genera desde el subproceso de trabajo y, si no ha configurado un controlador de excepciones no controlado, su proceso finalizará.

Puede que esté leyendo demasiado sobre la pregunta, pero supongo que la espera sería similar al menú de inicio donde espera 15 segundos a menos que presione una tecla.Podrías usar (1) una función de bloqueo o (2) podrías usar un hilo, un evento y un temporizador.El evento actuaría como una "continuación" y se bloquearía hasta que expirara el cronómetro o se presionara una tecla.

El pseudocódigo para (1) sería:

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

Si estás en el Main() método, no puedes usar await, entonces tendrás que usar Task.WaitAny():

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

Sin embargo, C# 7.1 introduce la posibilidad de crear un archivo asíncrono. Main() método, por lo que es mejor utilizar el Task.WhenAny() versión siempre que tengas esa opción:

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;

Lamentablemente no puedo comentar sobre la publicación de Gulzar, pero aquí hay un ejemplo más completo:

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

EDITAR:Se solucionó el problema haciendo que el trabajo real se realizara en un proceso separado y eliminando ese proceso si se agotaba el tiempo de espera.Consulte a continuación para obtener más detalles.¡Uf!

Simplemente probé esto y pareció funcionar bien.Mi compañero de trabajo tenía una versión que usaba un objeto Thread, pero encuentro que el método BeginInvoke() de tipos delegados es un poco más 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);
         }
      }
   }
}

El proyecto ReadLine.exe es muy simple y tiene una clase que se ve así:

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

.NET 4 hace que esto sea increíblemente sencillo usando Tareas.

Primero, construye tu ayudante:

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

Segundo, ejecuta con una tarea y espera:

      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

No es necesario intentar recrear la funcionalidad ReadLine ni realizar otros trucos peligrosos para que esto funcione.Las tareas nos permiten resolver la cuestión de una forma muy natural.

Como si no hubiera suficientes respuestas aquí :0), lo siguiente resume en un método estático la solución anterior de @kwl (la primera).

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

Ejemplo de subprocesamiento simple para resolver esto

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 cuerda estática en la parte superior para obtener una línea completa.

En mi caso, este funciona bien:

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

¿No es bonito y breve?

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

    // Handle keyInfo value here...
}

Este es un ejemplo más completo de la solución de Glen Slayden.Hice esto cuando construí un caso de prueba para otro problema.Utiliza E/S asincrónicas y un evento de reinicio manual.

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

Otra forma económica de obtener un segundo hilo es envolverlo en un delegado.

Ejemplo de implementación de la publicación de Eric anterior.Este ejemplo en particular se usó para leer información que se pasó a una aplicación de consola a través de una canalización:

 using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace PipedInfo
{
    class Program
    {
        static void Main(string[] args)
        {
            StreamReader buffer = ReadPipedInfo();

            Console.WriteLine(buffer.ReadToEnd());
        }

        #region ReadPipedInfo
        public static StreamReader ReadPipedInfo()
        {
            //call with a default value of 5 milliseconds
            return ReadPipedInfo(5);
        }

        public static StreamReader ReadPipedInfo(int waitTimeInMilliseconds)
        {
            //allocate the class we're going to callback to
            ReadPipedInfoCallback callbackClass = new ReadPipedInfoCallback();

            //to indicate read complete or timeout
            AutoResetEvent readCompleteEvent = new AutoResetEvent(false);

            //open the StdIn so that we can read against it asynchronously
            Stream stdIn = Console.OpenStandardInput();

            //allocate a one-byte buffer, we're going to read off the stream one byte at a time
            byte[] singleByteBuffer = new byte[1];

            //allocate a list of an arbitary size to store the read bytes
            List<byte> byteStorage = new List<byte>(4096);

            IAsyncResult asyncRead = null;
            int readLength = 0; //the bytes we have successfully read

            do
            {
                //perform the read and wait until it finishes, unless it's already finished
                asyncRead = stdIn.BeginRead(singleByteBuffer, 0, singleByteBuffer.Length, new AsyncCallback(callbackClass.ReadCallback), readCompleteEvent);
                if (!asyncRead.CompletedSynchronously)
                    readCompleteEvent.WaitOne(waitTimeInMilliseconds);

                //end the async call, one way or another

                //if our read succeeded we store the byte we read
                if (asyncRead.IsCompleted)
                {
                    readLength = stdIn.EndRead(asyncRead);
                    if (readLength > 0)
                        byteStorage.Add(singleByteBuffer[0]);
                }

            } while (asyncRead.IsCompleted && readLength > 0);
            //we keep reading until we fail or read nothing

            //return results, if we read zero bytes the buffer will return empty
            return new StreamReader(new MemoryStream(byteStorage.ToArray(), 0, byteStorage.Count));
        }

        private class ReadPipedInfoCallback
        {
            public void ReadCallback(IAsyncResult asyncResult)
            {
                //pull the user-defined variable and strobe the event, the read finished successfully
                AutoResetEvent readCompleteEvent = asyncResult.AsyncState as AutoResetEvent;
                readCompleteEvent.Set();
            }
        }
        #endregion ReadPipedInfo
    }
}
string readline = "?";
ThreadPool.QueueUserWorkItem(
    delegate
    {
        readline = Console.ReadLine();
    }
);
do
{
    Thread.Sleep(100);
} while (readline == "?");

Tenga en cuenta que si sigue la ruta "Console.ReadKey", perderá algunas de las características interesantes de ReadLine, a saber:

  • Soporte para eliminar, retroceder, teclas de flecha, etc.
  • La capacidad de presionar la tecla "arriba" y repetir el último comando (esto resulta muy útil si implementa una consola de depuración en segundo plano que se usa mucho).

Para agregar un tiempo de espera, modifique el ciclo while para adaptarlo.

¡No me odien por agregar otra solución a la gran cantidad de respuestas existentes!Esto funciona para Console.ReadKey(), pero podría modificarse fácilmente para que funcione con ReadLine(), etc.

Como los métodos "Console.Read" se están bloqueando, es necesario "empujar" la secuencia StdIn para cancelar la lectura.

Sintaxis de llamada:

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

Código:

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

Aquí hay una solución que utiliza Console.KeyAvailable.Estas son llamadas de bloqueo, pero debería ser bastante trivial llamarlas de forma asincrónica a través de TPL si se desea.Utilicé los mecanismos de cancelación estándar para facilitar la conexión con el patrón asincrónico de tareas y todas esas cosas buenas.

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

Hay algunas desventajas con esto.

  • No obtienes las funciones de navegación estándar que ReadLine proporciona (desplazamiento de flecha arriba/abajo, etc.).
  • Esto inyecta caracteres '\0' en la entrada si se presiona una tecla especial (F1, PrtScn, etc.).Sin embargo, puedes filtrarlos fácilmente modificando el código.

Terminé aquí porque se hizo una pregunta duplicada.Se me ocurrió la siguiente solución que parece sencilla.Estoy seguro de que tiene algunos inconvenientes que se me escaparon.

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

Llegué a esta respuesta y terminé haciendo:

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

Un ejemplo sencillo usando 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");
}

Un código mucho más contemporáneo y basado en tareas se vería así:

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

Tuve una situación única al tener una aplicación de Windows (servicio de Windows).Al ejecutar el programa de forma interactiva Environment.IsInteractive (VS Debugger o desde cmd.exe), utilicé AttachConsole/AllocConsole para obtener mi stdin/stdout.Para evitar que el proceso finalice mientras se realiza el trabajo, UI Thread llama Console.ReadKey(false).Quería cancelar la espera que estaba haciendo el hilo de la interfaz de usuario desde otro hilo, así que se me ocurrió una modificación a la solución de @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();
  }    
}

Esta parece ser la solución funcional más simple, que no utiliza ninguna API nativa:

    static Task<string> ReadLineAsync(CancellationToken cancellation)
    {
        return Task.Run(() =>
        {
            while (!Console.KeyAvailable)
            {
                if (cancellation.IsCancellationRequested)
                    return null;

                Thread.Sleep(100);
            }
            return Console.ReadLine();
        });
    }

Uso de ejemplo:

    static void Main(string[] args)
    {
        AsyncContext.Run(async () =>
        {
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            cancelSource.CancelAfter(1000);
            Console.WriteLine(await ReadLineAsync(cancelSource.Token) ?? "null");
        });
    }
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top