문제

사용자에게 제공하려는 콘솔 앱이 있습니다. 엑스 프롬프트에 응답하는 데 몇 초가 걸립니다.일정 시간이 지나도 입력이 없으면 프로그램 로직은 계속되어야 합니다.시간 초과는 빈 응답을 의미한다고 가정합니다.

이에 접근하는 가장 간단한 방법은 무엇입니까?

도움이 되었습니까?

해결책

5년이 지난 후에도 모든 답변이 여전히 다음 문제 중 하나 이상으로 인해 어려움을 겪고 있다는 사실에 놀랐습니다.

  • ReadLine 이외의 기능을 사용하여 기능이 손실되었습니다.(이전 입력은 삭제/백스페이스/위쪽 키)
  • 여러 번 호출하면 함수가 제대로 작동하지 않습니다(여러 스레드 생성, 많은 ReadLine 정지 또는 기타 예상치 못한 동작).
  • 기능은 바쁜 대기에 의존합니다.대기는 몇 초에서 몇 분이 걸릴 수 있는 시간 초과까지 어디에서나 실행될 것으로 예상되므로 이는 끔찍한 낭비입니다.이렇게 오랜 시간 동안 실행되는 바쁜 대기는 리소스를 엄청나게 소모하며, 이는 멀티스레딩 시나리오에서 특히 나쁩니다.바쁜 대기가 수면으로 수정되면 이는 응답성에 부정적인 영향을 미치지만 이것이 아마도 큰 문제는 아니라는 점을 인정합니다.

나는 내 솔루션이 위의 문제를 겪지 않고 원래 문제를 해결할 것이라고 믿습니다.

class Reader {
  private static Thread inputThread;
  private static AutoResetEvent getInput, gotInput;
  private static string input;

  static Reader() {
    getInput = new AutoResetEvent(false);
    gotInput = new AutoResetEvent(false);
    inputThread = new Thread(reader);
    inputThread.IsBackground = true;
    inputThread.Start();
  }

  private static void reader() {
    while (true) {
      getInput.WaitOne();
      input = Console.ReadLine();
      gotInput.Set();
    }
  }

  // omit the parameter to read a line without a timeout
  public static string ReadLine(int timeOutMillisecs = Timeout.Infinite) {
    getInput.Set();
    bool success = gotInput.WaitOne(timeOutMillisecs);
    if (success)
      return input;
    else
      throw new TimeoutException("User did not provide input within the timelimit.");
  }
}

물론 전화하는 것은 매우 쉽습니다.

try {
  Console.WriteLine("Please enter your name within the next 5 seconds.");
  string name = Reader.ReadLine(5000);
  Console.WriteLine("Hello, {0}!", name);
} catch (TimeoutException) {
  Console.WriteLine("Sorry, you waited too long.");
}

또는 다음을 사용할 수 있습니다. TryXX(out) 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;
  }

이는 다음과 같이 호출됩니다.

Console.WriteLine("Please enter your name within the next 5 seconds.");
string name;
bool success = Reader.TryReadLine(out name, 5000);
if (!success)
  Console.WriteLine("Sorry, you waited too long.");
else
  Console.WriteLine("Hello, {0}!", name);

두 경우 모두 통화를 혼합할 수 없습니다. Reader 정상으로 Console.ReadLine 통화:만약에 Reader 시간이 초과되면 교수형이 발생합니다. ReadLine 부르다.대신, 일반(시간 제한 없음)을 원하시면 ReadLine 전화하세요. Reader 시간 초과를 생략하여 기본적으로 무한 시간 초과로 설정됩니다.

그렇다면 제가 언급한 다른 솔루션의 문제점은 어떻습니까?

  • 보시다시피 ReadLine이 사용되어 첫 번째 문제를 피합니다.
  • 함수가 여러 번 호출되면 제대로 작동합니다.시간 초과 발생 여부에 관계없이 하나의 백그라운드 스레드만 실행되고 ReadLine에 대한 최대 한 번의 호출만 활성화됩니다.함수를 호출하면 항상 최신 입력이 발생하거나 시간 초과가 발생하며 사용자는 입력을 제출하기 위해 Enter 키를 두 번 이상 누를 필요가 없습니다.
  • 그리고 분명히 이 함수는 바쁜 대기에 의존하지 않습니다.대신 적절한 멀티스레딩 기술을 사용하여 리소스 낭비를 방지합니다.

이 솔루션에서 예상되는 유일한 문제는 스레드로부터 안전하지 않다는 것입니다.그러나 여러 스레드는 사용자에게 동시에 입력을 요청할 수 없으므로 호출하기 전에 동기화가 이루어져야 합니다. Reader.ReadLine 그래도.

다른 팁

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

이 접근 방식은 다음을 사용합니까? Console.Key사용 가능 돕다?

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

어떤 식으로든 두 번째 스레드가 필요합니다.비동기 IO를 사용하면 자신의 선언을 피할 수 있습니다.

  • ManualResetEvent를 선언하고 "evt"라고 부릅니다.
  • System.Console.OpenStandardInput을 호출하여 입력 스트림을 가져옵니다.데이터를 저장하고 evt를 설정할 콜백 메서드를 지정합니다.
  • 해당 스트림의 BeginRead 메서드를 호출하여 비동기 읽기 작업을 시작합니다.
  • 그런 다음 ManualResetEvent에 대한 시간 제한 대기를 입력합니다.
  • 대기 시간이 초과되면 읽기를 취소하세요.

읽기가 데이터를 반환하는 경우 이벤트를 설정하면 메인 스레드가 계속됩니다. 그렇지 않으면 시간 초과 후에도 계속됩니다.

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

이것은 나에게 효과적이었습니다.

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

보조 스레드를 만들고 콘솔에서 키를 폴링해야 할 것 같습니다.나는 이것을 달성하기 위한 내장된 방법이 없다는 것을 알고 있습니다.

저는 기업 환경에서 완벽하게 작동하는 솔루션을 찾기까지 5개월 동안 이 문제로 고생했습니다.

지금까지 대부분의 솔루션의 문제점은 Console.ReadLine() 이외의 다른 솔루션에 의존한다는 점이며 Console.ReadLine()에는 많은 장점이 있습니다.

  • 삭제, 백스페이스, 화살표 키 등 지원
  • "위쪽" 키를 누르고 마지막 명령을 반복하는 기능(이 기능은 많이 사용되는 백그라운드 디버깅 콘솔을 구현하는 경우 매우 유용합니다).

내 솔루션은 다음과 같습니다.

  1. 스폰 별도의 스레드 Console.ReadLine()을 사용하여 사용자 입력을 처리합니다.
  2. 시간 초과 기간이 지나면 다음을 사용하여 현재 콘솔 창에 [enter] 키를 보내 Console.ReadLine() 차단을 해제합니다. http://inputsimulator.codeplex.com/.

샘플 코드:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Console.ReadLine을 사용하는 스레드를 중단하는 올바른 기술을 포함하여 이 기술에 대한 추가 정보:

콘솔 앱인 현재 프로세스에 [enter] 키 입력을 보내는 .NET 호출은 무엇입니까?

해당 스레드가 Console.ReadLine을 실행할 때 .NET에서 다른 스레드를 중단하는 방법은 무엇입니까?

사용자가 'Enter' 키를 누르지 않으면 해당 호출이 절대 반환되지 않기 때문에 대리자에서 Console.ReadLine()을 호출하는 것은 좋지 않습니다.대리자를 실행하는 스레드는 사용자가 'Enter' 키를 누를 때까지 차단되며 이를 취소할 방법이 없습니다.

이러한 호출을 연속해서 실행하면 예상대로 작동하지 않습니다.다음을 고려하십시오(위의 Console 클래스 예제 사용).

System.Console.WriteLine("Enter your first name [John]:");

string firstName = Console.ReadLine(5, "John");

System.Console.WriteLine("Enter your last name [Doe]:");

string lastName = Console.ReadLine(5, "Doe");

사용자는 첫 번째 프롬프트에 대해 시간 초과가 만료되도록 한 다음 두 번째 프롬프트에 값을 입력합니다.firstName과 lastName 모두 기본값을 포함합니다.사용자가 'Enter' 키를 누르면 첫 번째 ReadLine 호출은 완료되지만 코드는 해당 호출을 포기하고 본질적으로 결과를 폐기했습니다.그만큼 두번째 ReadLine 호출은 계속 차단되고 시간 초과는 결국 만료되며 반환된 값은 다시 기본값이 됩니다.

그런데- 위 코드에는 버그가 있습니다.waitHandle.Close()를 호출하면 작업자 스레드 아래에서 이벤트가 닫힙니다.제한 시간이 만료된 후 사용자가 'enter' 키를 누르면 작업자 스레드는 ObjectDisposedException을 발생시키는 이벤트 신호를 보내려고 시도합니다.예외는 작업자 스레드에서 발생하며, 처리되지 않은 예외 처리기를 설정하지 않은 경우 프로세스가 종료됩니다.

질문에 대해 너무 많이 읽었을 수도 있지만 키를 누르지 않는 한 15초 동안 기다리는 부팅 메뉴와 비슷할 것이라고 가정합니다.(1) 차단 기능을 사용하거나 (2) 스레드, 이벤트 및 타이머를 사용할 수 있습니다.이벤트는 '계속' 역할을 하며 타이머가 만료되거나 키를 누를 때까지 차단됩니다.

(1)의 의사 코드는 다음과 같습니다.

// Get configurable wait time
TimeSpan waitTime = TimeSpan.FromSeconds(15.0);
int configWaitTimeSec;
if (int.TryParse(ConfigManager.AppSetting["DefaultWaitTime"], out configWaitTimeSec))
    waitTime = TimeSpan.FromSeconds(configWaitTimeSec);

bool keyPressed = false;
DateTime expireTime = DateTime.Now + waitTime;

// Timer and key processor
ConsoleKeyInfo cki;
// EDIT: adding a missing ! below
while (!keyPressed && (DateTime.Now < expireTime))
{
    if (Console.KeyAvailable)
    {
        cki = Console.ReadKey(true);
        // TODO: Process key
        keyPressed = true;
    }
    Thread.Sleep(10);
}

당신이 Main() 방법을 사용할 수 없습니다. await, 그래서 당신은 사용해야합니다 Task.WaitAny():

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

그러나 C# 7.1에는 비동기식 생성 가능성이 도입되었습니다. Main() 방법을 사용하는 것이 좋습니다. Task.WhenAny() 해당 옵션이 있을 때마다 다음 버전을 사용하세요.

var task = Task.Factory.StartNew(Console.ReadLine);
var completedTask = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(5)));
var result = object.ReferenceEquals(task, completedTask) ? task.Result : string.Empty;

불행하게도 Gulzar의 게시물에 대해서는 언급할 수 없지만 다음은 더 자세한 예입니다.

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

편집하다:실제 작업을 별도의 프로세스에서 수행하고 시간이 초과되면 해당 프로세스를 종료하여 문제를 해결했습니다.자세한 내용은 아래를 참조하세요.아휴!

방금 이것을 실행해 보니 잘 작동하는 것 같았습니다.내 동료는 Thread 개체를 사용하는 버전을 가지고 있었지만 위임 유형의 BeginInvoke() 메서드가 좀 더 우아하다고 생각합니다.

namespace TimedReadLine
{
   public static class Console
   {
      private delegate string ReadLineInvoker();

      public static string ReadLine(int timeout)
      {
         return ReadLine(timeout, null);
      }

      public static string ReadLine(int timeout, string @default)
      {
         using (var process = new System.Diagnostics.Process
         {
            StartInfo =
            {
               FileName = "ReadLine.exe",
               RedirectStandardOutput = true,
               UseShellExecute = false
            }
         })
         {
            process.Start();

            var rli = new ReadLineInvoker(process.StandardOutput.ReadLine);
            var iar = rli.BeginInvoke(null, null);

            if (!iar.AsyncWaitHandle.WaitOne(new System.TimeSpan(0, 0, timeout)))
            {
               process.Kill();
               return @default;
            }

            return rli.EndInvoke(iar);
         }
      }
   }
}

ReadLine.exe 프로젝트는 다음과 같은 클래스가 하나 있는 매우 간단한 프로젝트입니다.

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

.NET 4에서는 작업을 사용하여 이 작업을 매우 간단하게 만듭니다.

먼저 도우미를 빌드합니다.

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

둘째, 작업을 실행하고 기다립니다.

      Dim askTask As Task(Of String) = New TaskFactory().StartNew(Function() AskUser())
      askTask.Wait(TimeSpan.FromSeconds(30))
      If Not askTask.IsCompleted Then
         Console.WriteLine("User failed to respond.")
      Else
         Console.WriteLine(String.Format("You responded, '{0}'.", askTask.Result))
      End If

이 작업을 수행하기 위해 ReadLine 기능을 다시 만들거나 다른 위험한 해킹을 수행하려고 시도할 필요가 없습니다.작업을 사용하면 매우 자연스러운 방식으로 문제를 해결할 수 있습니다.

여기에 충분한 답변이 없는 것처럼 :0) 다음은 위의 @kwl 솔루션(첫 번째 솔루션)의 정적 메서드로 캡슐화됩니다.

    public static string ConsoleReadLineWithTimeout(TimeSpan timeout)
    {
        Task<string> task = Task.Factory.StartNew(Console.ReadLine);

        string result = Task.WaitAny(new Task[] { task }, timeout) == 0
            ? task.Result 
            : string.Empty;
        return result;
    }

용법

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

이 문제를 해결하기 위한 간단한 스레딩 예제

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

또는 전체 줄을 얻기 위한 정적 문자열입니다.

제 경우에는 잘 작동합니다.

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

멋지고 짧지 않나요?

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

    // Handle keyInfo value here...
}

이것은 Glen Slayden 솔루션의 완전한 예입니다.우연히 다른 문제에 대한 테스트 케이스를 만들 때 이렇게 만들었습니다.비동기 I/O와 수동 재설정 이벤트를 사용합니다.

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

두 번째 스레드를 얻는 또 다른 저렴한 방법은 이를 대리자로 래핑하는 것입니다.

위 Eric의 게시물 구현 예입니다.이 특정 예는 파이프를 통해 콘솔 앱에 전달된 정보를 읽는 데 사용되었습니다.

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

"Console.ReadKey" 경로로 가면 다음과 같은 ReadLine의 멋진 기능 중 일부를 잃게 됩니다.

  • 삭제, 백스페이스, 화살표 키 등 지원
  • "위쪽" 키를 누르고 마지막 명령을 반복하는 기능(이 기능은 많이 사용되는 백그라운드 디버깅 콘솔을 구현하는 경우 매우 유용합니다).

시간 초과를 추가하려면 while 루프를 적절하게 변경하세요.

수많은 기존 답변에 또 다른 솔루션을 추가한다고 해서 저를 미워하지 마세요!이는 Console.ReadKey()에서 작동하지만 ReadLine() 등에서 작동하도록 쉽게 수정할 수 있습니다.

"Console.Read" 메서드가 차단되므로 "슬쩍 찌르다" 읽기를 취소하려면 StdIn 스트림을 사용하세요.

호출 구문:

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

암호:

public class AsyncConsole // not thread safe
{
    private static readonly Lazy<AsyncConsole> Instance =
        new Lazy<AsyncConsole>();

    private bool _keyPressed;
    private ConsoleKeyInfo _keyInfo;

    private bool DoReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        _keyPressed = false;
        _keyInfo = new ConsoleKeyInfo();

        Thread readKeyThread = new Thread(ReadKeyThread);
        readKeyThread.IsBackground = false;
        readKeyThread.Start();

        Thread.Sleep(millisecondsTimeout);

        if (readKeyThread.IsAlive)
        {
            try
            {
                IntPtr stdin = GetStdHandle(StdHandle.StdIn);
                CloseHandle(stdin);
                readKeyThread.Join();
            }
            catch { }
        }

        readKeyThread = null;

        keyInfo = _keyInfo;
        return _keyPressed;
    }

    private void ReadKeyThread()
    {
        try
        {
            _keyInfo = Console.ReadKey();
            _keyPressed = true;
        }
        catch (InvalidOperationException) { }
    }

    public static bool ReadKey(
        int millisecondsTimeout,
        out ConsoleKeyInfo keyInfo)
    {
        return Instance.Value.DoReadKey(millisecondsTimeout, out keyInfo);
    }

    private enum StdHandle { StdIn = -10, StdOut = -11, StdErr = -12 };

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetStdHandle(StdHandle std);

    [DllImport("kernel32.dll")]
    private static extern bool CloseHandle(IntPtr hdl);
}

다음은 다음을 사용하는 솔루션입니다. Console.KeyAvailable.이는 차단 호출이지만 원하는 경우 TPL을 통해 비동기적으로 호출하는 것은 매우 간단합니다.저는 작업 비동기 패턴과 그 모든 좋은 기능을 쉽게 연결할 수 있도록 표준 취소 메커니즘을 사용했습니다.

public static class ConsoleEx
{
  public static string ReadLine(TimeSpan timeout)
  {
    var cts = new CancellationTokenSource();
    return ReadLine(timeout, cts.Token);
  }

  public static string ReadLine(TimeSpan timeout, CancellationToken cancellation)
  {
    string line = "";
    DateTime latest = DateTime.UtcNow.Add(timeout);
    do
    {
        cancellation.ThrowIfCancellationRequested();
        if (Console.KeyAvailable)
        {
            ConsoleKeyInfo cki = Console.ReadKey();
            if (cki.Key == ConsoleKey.Enter)
            {
                return line;
            }
            else
            {
                line += cki.KeyChar;
            }
        }
        Thread.Sleep(1);
    }
    while (DateTime.UtcNow < latest);
    return null;
  }
}

여기에는 몇 가지 단점이 있습니다.

  • 표준 탐색 기능을 사용할 수 없습니다. ReadLine (위/아래 화살표 스크롤 등)을 제공합니다.
  • 특수 키(F1, PrtScn 등)를 누르면 입력에 '\0' 문자가 삽입됩니다.하지만 코드를 수정하면 쉽게 필터링할 수 있습니다.

중복된 질문이 있어서 여기까지 왔습니다.나는 간단 해 보이는 다음 솔루션을 생각해 냈습니다.내가 놓친 몇 가지 단점이 있다고 확신합니다.

static void Main(string[] args)
{
    Console.WriteLine("Hit q to continue or wait 10 seconds.");

    Task task = Task.Factory.StartNew(() => loop());

    Console.WriteLine("Started waiting");
    task.Wait(10000);
    Console.WriteLine("Stopped waiting");
}

static void loop()
{
    while (true)
    {
        if ('q' == Console.ReadKey().KeyChar) break;
    }
}

나는 이 답변에 이르렀고 결국 다음을 수행했습니다.

    /// <summary>
    /// Reads Line from console with timeout. 
    /// </summary>
    /// <exception cref="System.TimeoutException">If user does not enter line in the specified time.</exception>
    /// <param name="timeout">Time to wait in milliseconds. Negative value will wait forever.</param>        
    /// <returns></returns>        
    public static string ReadLine(int timeout = -1)
    {
        ConsoleKeyInfo cki = new ConsoleKeyInfo();
        StringBuilder sb = new StringBuilder();

        // if user does not want to spesify a timeout
        if (timeout < 0)
            return Console.ReadLine();

        int counter = 0;

        while (true)
        {
            while (Console.KeyAvailable == false)
            {
                counter++;
                Thread.Sleep(1);
                if (counter > timeout)
                    throw new System.TimeoutException("Line was not entered in timeout specified");
            }

            cki = Console.ReadKey(false);

            if (cki.Key == ConsoleKey.Enter)
            {
                Console.WriteLine();
                return sb.ToString();
            }
            else
                sb.Append(cki.KeyChar);                
        }            
    }

다음을 사용한 간단한 예 Console.KeyAvailable:

Console.WriteLine("Press any key during the next 2 seconds...");
Thread.Sleep(2000);
if (Console.KeyAvailable)
{
    Console.WriteLine("Key pressed");
}
else
{
    Console.WriteLine("You were too slow");
}

훨씬 더 현대적이고 작업 기반 코드는 다음과 같습니다.

public string ReadLine(int timeOutMillisecs)
{
    var inputBuilder = new StringBuilder();

    var task = Task.Factory.StartNew(() =>
    {
        while (true)
        {
            var consoleKey = Console.ReadKey(true);
            if (consoleKey.Key == ConsoleKey.Enter)
            {
                return inputBuilder.ToString();
            }

            inputBuilder.Append(consoleKey.KeyChar);
        }
    });


    var success = task.Wait(timeOutMillisecs);
    if (!success)
    {
        throw new TimeoutException("User did not provide input within the timelimit.");
    }

    return inputBuilder.ToString();
}

Windows 응용 프로그램(Windows 서비스)을 사용하는 독특한 상황이 있었습니다.프로그램을 대화형으로 실행할 때 Environment.IsInteractive (VS Debugger 또는 cmd.exe에서) AttachConsole/AllocConsole을 사용하여 stdin/stdout을 가져왔습니다.작업이 완료되는 동안 프로세스가 종료되지 않도록 하기 위해 UI 스레드는 다음을 호출합니다. Console.ReadKey(false).UI 스레드가 다른 스레드에서 수행하는 대기를 취소하고 싶었기 때문에 @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();
  }    
}

이것은 기본 API를 사용하지 않는 가장 간단하고 작동하는 솔루션인 것 같습니다.

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

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

사용 예:

    static void Main(string[] args)
    {
        AsyncContext.Run(async () =>
        {
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            cancelSource.CancelAfter(1000);
            Console.WriteLine(await ReadLineAsync(cancelSource.Token) ?? "null");
        });
    }
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top