Làm cách nào để thêm Timeout vào Console.ReadLine ()?


122

Tôi có một ứng dụng bảng điều khiển mà tôi muốn cho người dùng x giây để trả lời lời nhắc. Nếu không có đầu vào nào được thực hiện sau một khoảng thời gian nhất định, logic chương trình sẽ tiếp tục. Chúng tôi giả định thời gian chờ có nghĩa là phản hồi trống.

Cách đơn giản nhất để tiếp cận vấn đề này là gì?

Câu trả lời:


112

Tôi ngạc nhiên khi biết rằng sau 5 năm, tất cả các câu trả lời vẫn mắc phải một hoặc nhiều vấn đề sau:

  • Một chức năng khác ngoài ReadLine được sử dụng, gây ra mất chức năng. (Xóa / xóa lùi / phím lên cho lần nhập trước).
  • Hàm hoạt động không tốt khi được gọi nhiều lần (sinh ra nhiều luồng, nhiều ReadLine bị treo hoặc hành vi không mong muốn khác).
  • Chức năng dựa trên thời gian chờ đợi. Đó là một sự lãng phí khủng khiếp vì thời gian chờ dự kiến ​​sẽ kéo dài từ vài giây cho đến khi hết thời gian, có thể là nhiều phút. Một sự chờ đợi bận rộn kéo dài trong một khoảng thời gian như vậy là một sự ngốn tài nguyên khủng khiếp, điều này đặc biệt tồi tệ trong kịch bản đa luồng. Nếu thời gian chờ đợi được sửa đổi bằng chế độ ngủ, điều này có ảnh hưởng tiêu cực đến khả năng phản hồi, mặc dù tôi thừa nhận rằng đây có lẽ không phải là một vấn đề lớn.

Tôi tin rằng giải pháp của tôi sẽ giải quyết được vấn đề ban đầu mà không bị bất kỳ vấn đề nào ở trên:

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

Tất nhiên, việc gọi điện rất dễ dàng:

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

Ngoài ra, bạn có thể sử dụng TryXX(out)quy ước, như shmueli đã đề xuất:

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

Nó được gọi như sau:

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

Trong cả hai trường hợp, bạn không thể kết hợp các cuộc gọi đến Readervới các Console.ReadLinecuộc gọi thông thường : nếu hết Readerthời gian, sẽ có một ReadLinecuộc gọi treo . Thay vào đó, nếu bạn muốn có một ReadLinecuộc gọi bình thường (không tính thời gian) , chỉ cần sử dụng Readervà bỏ qua thời gian chờ, để mặc định nó là thời gian chờ vô hạn.

Vì vậy, làm thế nào về những vấn đề của các giải pháp khác mà tôi đã đề cập?

  • Như bạn có thể thấy, ReadLine đã được sử dụng, tránh được vấn đề đầu tiên.
  • Hàm hoạt động đúng khi được gọi nhiều lần. Bất kể thời gian chờ có xảy ra hay không, chỉ một chuỗi nền sẽ chạy và chỉ có nhiều nhất một lệnh gọi tới ReadLine sẽ hoạt động. Việc gọi hàm sẽ luôn dẫn đến đầu vào mới nhất hoặc trong khoảng thời gian chờ và người dùng sẽ không phải nhấn enter nhiều lần để gửi đầu vào của mình.
  • Và, rõ ràng, chức năng không dựa vào thời gian chờ đợi. Thay vào đó, nó sử dụng các kỹ thuật đa luồng thích hợp để tránh lãng phí tài nguyên.

Vấn đề duy nhất mà tôi thấy trước với giải pháp này là nó không an toàn về đường chỉ. Tuy nhiên, nhiều luồng thực sự không thể yêu cầu người dùng nhập cùng một lúc, vì vậy, nên đồng bộ hóa trước khi thực hiện cuộc gọi Reader.ReadLine.


1
Tôi nhận được một NullReferenceException sau mã này. Tôi nghĩ rằng tôi có thể sửa lỗi bắt đầu chuỗi một lần khi các sự kiện tự động được tạo.
Augusto Pedraza,

1
@JSQuareD Tôi không nghĩ rằng thời gian chờ đợi với chế độ Sleep (200ms) là quá nhiều horrible waste, nhưng tất nhiên tín hiệu của bạn tốt hơn. Ngoài ra, việc sử dụng một Console.ReadLinecuộc gọi chặn trong một vòng lặp vô hạn trong mối đe dọa thứ hai ngăn chặn các vấn đề với nhiều cuộc gọi như vậy ở chế độ nền như trong cuộc gọi khác, được ủng hộ nhiều, các giải pháp bên dưới. Cảm ơn bạn đã chia sẻ mã của bạn. +1
Roland

2
Nếu bạn không nhập kịp thời, phương thức này dường như bị hỏng trong lần Console.ReadLine()gọi tiếp theo đầu tiên bạn thực hiện. Bạn kết thúc với một "bóng ma" ReadLinecần được hoàn thành trước.
Derek

1
@Derek rất tiếc, bạn không thể kết hợp phương thức này với các cuộc gọi ReadLine thông thường, tất cả các cuộc gọi phải được thực hiện thông qua Reader. Giải pháp cho vấn đề này sẽ là thêm aethod vào trình đọc chờ gotInput mà không có thời gian chờ. Tôi hiện đang sử dụng thiết bị di động, vì vậy tôi không thể thêm nó vào câu trả lời một cách dễ dàng.
JSQuareD

1
Tôi không thấy cần thiết 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
Tôi không biết tại sao cái này không được bình chọn - nó hoạt động hoàn hảo. Rất nhiều giải pháp khác liên quan đến "ReadKey ()", không hoạt động bình thường: nó có nghĩa là bạn mất tất cả sức mạnh của ReadLine (), chẳng hạn như nhấn phím "lên" để nhận lệnh đã nhập trước đó, sử dụng phím xóa lùi và các phím mũi tên, v.v.
Contango

9
@Gravitas: Điều này không hoạt động. Vâng, nó hoạt động một lần. Nhưng mỗi khi ReadLinebạn gọi đều ngồi đó để chờ đợi thông tin đầu vào. Nếu bạn gọi nó 100 lần, nó sẽ tạo ra 100 chuỗi không biến mất hết cho đến khi bạn nhấn Enter 100 lần!
Gabe

2
Hãy coi chừng. Giải pháp này có vẻ gọn gàng nhưng tôi đã kết thúc với hàng nghìn cuộc gọi chưa hoàn thành bị treo. Vì vậy, không phù hợp nếu gọi nhiều lần.
Tom Makin

@Gabe, shakinfree: nhiều cuộc gọi không được xem xét cho giải pháp mà chỉ là một cuộc gọi không đồng bộ với thời gian chờ. Tôi đoán sẽ khiến người dùng bối rối khi có 10 thông báo được in trên bảng điều khiển và sau đó nhập đầu vào cho từng cái một theo thứ tự tương ứng. Tuy nhiên, đối với các cuộc gọi treo, bạn có thể thử nhận xét dòng TimedoutException và trả về chuỗi null / rỗng không?
gp.

không ... vấn đề là Console.ReadLine vẫn đang chặn luồng threadpool đang chạy phương thức Console.ReadLine từ ReadLineDelegate.
gp.

27

Cách tiếp cận này sử dụng Console.KeyAvailable có trợ giúp không?

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

Điều này là đúng, OP dường như muốn một cuộc gọi chặn, mặc dù tôi hơi rùng mình khi nghĩ đến ... Đây có lẽ là một giải pháp tốt hơn.
GEOCHET

Tôi chắc chắn bạn đã thấy điều này. Lấy
Gulzar Nazim

Tôi không hiểu "thời gian chờ" này như thế nào nếu người dùng không làm gì cả. Tất cả điều này có thể làm là tiếp tục thực thi logic trong nền cho đến khi một phím được nhấn và logic khác tiếp tục.
mphair 17/02/10

Đúng, điều này cần được sửa chữa. Nhưng nó đủ dễ dàng để thêm thời gian chờ vào điều kiện vòng lặp.
Jonathan Allen

KeyAvailablechỉ cho biết rằng người dùng đã bắt đầu nhập đầu vào cho ReadLine, nhưng chúng tôi cần một sự kiện khi nhấn Enter, điều đó làm cho ReadLine quay trở lại. Giải pháp này chỉ hoạt động cho ReadKey, tức là chỉ nhận một ký tự. Vì điều này không giải quyết được câu hỏi thực tế cho ReadLine, tôi không thể sử dụng giải pháp của bạn. -1 xin lỗi
Roland

13

Điều này đã làm việc cho tôi.

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
Tôi nghĩ đây là giải pháp tốt nhất và đơn giản nhất sử dụng các công cụ đã được tích hợp sẵn. Bạn đã làm rất tốt!
Vippy

2
Xinh đẹp! Sự đơn giản thực sự là sự tinh tế cuối cùng. Chúc mừng!
BrunoSalvino

10

Bằng cách này hay cách khác, bạn cần một chuỗi thứ hai. Bạn có thể sử dụng IO không đồng bộ để tránh khai báo của riêng bạn:

  • khai báo một ManualResetEvent, gọi nó là "evt"
  • gọi System.Console.OpenStandardInput để lấy luồng đầu vào. Chỉ định một phương thức gọi lại sẽ lưu trữ dữ liệu của nó và thiết lập evt.
  • gọi phương thức BeginRead của luồng đó để bắt đầu thao tác đọc không đồng bộ
  • sau đó nhập thời gian chờ trên ManualResetEvent
  • nếu hết thời gian chờ, hãy hủy đọc

Nếu quá trình đọc trả về dữ liệu, hãy đặt sự kiện và chuỗi chính của bạn sẽ tiếp tục, nếu không bạn sẽ tiếp tục sau thời gian chờ.


Đây ít nhiều là những gì Giải pháp được Chấp nhận thực hiện.
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

Tôi nghĩ bạn sẽ cần tạo một chuỗi phụ và thăm dò ý kiến ​​cho một khóa trên bảng điều khiển. Tôi biết không có cách nào được xây dựng để thực hiện điều này.


Vâng nếu bạn có một chuỗi thăm dò ý kiến ​​thứ hai cho các phím và ứng dụng của bạn đóng lại trong khi nó đang ngồi ở đó chờ đợi, chuỗi thăm dò chính đó sẽ chỉ ngồi đó, chờ đợi, mãi mãi.
Kelly Elton

Trên thực tế: một chuỗi thứ hai hoặc một đại biểu với "BeginInvoke" (sử dụng một chuỗi, đằng sau hậu trường - xem câu trả lời từ @gp).
Contango, 17/11/11

@ kelton52, Chuỗi phụ có thoát ra nếu bạn kết thúc quy trình trong Trình quản lý tác vụ không?
Arlen Beiler

6

Tôi đã vật lộn với vấn đề này trong 5 tháng trước khi tìm ra giải pháp hoạt động hoàn hảo trong môi trường doanh nghiệp.

Vấn đề với hầu hết các giải pháp cho đến nay là chúng dựa vào thứ gì đó khác ngoài Console.ReadLine () và Console.ReadLine () có rất nhiều lợi thế:

  • Hỗ trợ xóa, xóa lùi, phím mũi tên, v.v.
  • Khả năng nhấn phím "lên" và lặp lại lệnh cuối cùng (điều này rất hữu ích nếu bạn triển khai bảng điều khiển gỡ lỗi nền được sử dụng nhiều).

Giải pháp của tôi như sau:

  1. Tạo một chuỗi riêng để xử lý đầu vào của người dùng bằng Console.ReadLine ().
  2. Sau khoảng thời gian chờ, hãy bỏ chặn Console.ReadLine () bằng cách gửi khóa [enter] vào cửa sổ bảng điều khiển hiện tại, sử dụng http://inputsimulator.codeplex.com/ .

Mã mẫu:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Thông tin thêm về kỹ thuật này, bao gồm cả kỹ thuật chính xác để hủy bỏ một chuỗi sử dụng Console.ReadLine:

Cuộc gọi .NET để gửi tổ hợp phím [enter] vào quy trình hiện tại, đó là ứng dụng bảng điều khiển?

Làm thế nào để hủy một luồng khác trong .NET, khi luồng cho biết đang thực thi Console.ReadLine?


5

Việc gọi Console.ReadLine () trong ủy quyền là không tốt vì nếu người dùng không nhấn 'enter' thì lệnh gọi đó sẽ không bao giờ trở lại. Chuỗi thực thi ủy quyền sẽ bị chặn cho đến khi người dùng nhấn 'enter', không có cách nào để hủy nó.

Việc đưa ra một chuỗi các cuộc gọi này sẽ không hoạt động như bạn mong đợi. Hãy xem xét những điều sau (sử dụng lớp Console ví dụ từ trên):

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

Người dùng để thời gian chờ hết hạn cho lời nhắc đầu tiên, sau đó nhập giá trị cho lời nhắc thứ hai. Cả firstName và lastName sẽ chứa các giá trị mặc định. Khi người dùng nhấn 'enter', lệnh gọi ReadLine đầu tiên sẽ hoàn tất, nhưng mã đã từ bỏ lệnh gọi đó và về cơ bản loại bỏ kết quả. Cuộc gọi ReadLine thứ hai sẽ tiếp tục bị chặn, thời gian chờ cuối cùng sẽ hết hạn và giá trị trả về sẽ lại là giá trị mặc định.

BTW- Có một lỗi trong đoạn mã trên. Bằng cách gọi waitHandle.Close (), bạn đóng sự kiện từ bên dưới chuỗi worker. Nếu người dùng nhấn 'enter' sau khi hết thời gian chờ, chuỗi công nhân sẽ cố gắng báo hiệu sự kiện ném ra một ObjectDisposedException. Ngoại lệ được ném khỏi chuỗi công nhân và nếu bạn chưa thiết lập trình xử lý ngoại lệ chưa xử lý thì quy trình của bạn sẽ kết thúc.


1
Thuật ngữ "ở trên" trong bài đăng của bạn là mơ hồ và khó hiểu. Nếu bạn đang đề cập đến một câu trả lời khác, bạn nên tạo một liên kết thích hợp đến câu trả lời đó.
bzlm 06-08

5

Nếu bạn đang ở trong Main()phương pháp này, bạn không thể sử dụng await, vì vậy bạn sẽ phải sử dụng Task.WaitAny():

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

Tuy nhiên, C # 7.1 giới thiệu khả năng có thể tạo Main()phương thức không đồng bộ, vì vậy tốt hơn nên sử dụng Task.WhenAny()phiên bản bất cứ khi nào bạn có tùy chọ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;

4

Tôi có thể đang đọc quá nhiều vào câu hỏi, nhưng tôi cho rằng quá trình chờ sẽ tương tự như menu khởi động, nơi nó đợi 15 giây trừ khi bạn nhấn một phím. Bạn có thể sử dụng (1) chức năng chặn hoặc (2) bạn có thể sử dụng một chuỗi, một sự kiện và một bộ đếm thời gian. Sự kiện sẽ hoạt động như một 'tiếp tục' và sẽ chặn cho đến khi bộ hẹn giờ hết hạn hoặc một phím được nhấn.

Mã giả cho (1) sẽ là:

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

Rất tiếc, tôi không thể bình luận về bài đăng của Gulzar, nhưng đây là một ví dụ đầy đủ hơn:

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

Lưu ý rằng bạn cũng có thể sử dụng Console.In.Peek () nếu giao diện điều khiển không hiển thị (?) Hoặc đầu vào được chuyển hướng từ một tệp.
Jamie Kitson

2

CHỈNH SỬA : khắc phục sự cố bằng cách yêu cầu công việc thực tế được thực hiện trong một quy trình riêng biệt và hủy bỏ quy trình đó nếu nó hết thời gian. Xem chi tiết bên dưới. Chà!

Chỉ cần chạy thử và nó có vẻ hoạt động tốt. Đồng nghiệp của tôi có một phiên bản sử dụng đối tượng Thread, nhưng tôi thấy phương thức BeginInvoke () của các kiểu đại biểu sẽ thanh lịch hơn một chút.

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

Dự án ReadLine.exe là một dự án rất đơn giản có một lớp trông giống như sau:

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

2
Việc gọi một tệp thực thi riêng biệt trong một quy trình mới chỉ để thực hiện ReadLine () được định thời gian nghe có vẻ như quá mức cần thiết. Về cơ bản, bạn đang giải quyết vấn đề không thể hủy bỏ một chuỗi ReadLine () - chặn bằng cách thiết lập và hủy bỏ toàn bộ quy trình.
bzlm

Sau đó, hãy nói điều đó với Microsoft, người đã đưa chúng tôi vào vị trí này.
Jesse C. Slicer

Microsoft đã không đặt bạn vào vị trí đó. Hãy xem một số câu trả lời khác thực hiện công việc tương tự trong một vài dòng. Tôi nghĩ rằng các mã trên sẽ nhận được một số loại giải thưởng - nhưng không phải là loại mà bạn muốn :)
contango

1
Không, không có câu trả lời nào khác làm được chính xác những gì OP muốn. Tất cả chúng đều mất các tính năng của quy trình nhập liệu tiêu chuẩn hoặc bị treo trên thực tế là tất cả các yêu cầu Console.ReadLine() đang chặn và sẽ giữ lại đầu vào cho yêu cầu tiếp theo. Câu trả lời được chấp nhận là khá gần, nhưng vẫn có những hạn chế.
Jesse C. Slicer

1
Ừm, không, không phải vậy. Bộ đệm đầu vào vẫn chặn (ngay cả khi chương trình không). Hãy thử nó cho chính mình: Nhập một số ký tự nhưng không nhấn enter. Hãy để nó hết thời gian. Chụp ngoại lệ trong trình gọi. Sau đó, có một cái khác ReadLine()trong chương trình của bạn sau khi gọi cái này. Hãy xem điều gì xảy ra. Bạn phải nhấn trở lại TWICE để có được nó đi do tính chất đơn luồng của Console. Nó. Không. Công việc.
Jesse C. Slicer,

2

.NET 4 làm cho việc này trở nên cực kỳ đơn giản bằng Tasks.

Đầu tiên, hãy xây dựng trình trợ giúp của bạn:

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

Thứ hai, thực hiện một nhiệm vụ và đợi:

      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

Không có cố gắng tạo lại chức năng ReadLine hoặc thực hiện các thủ thuật nguy hiểm khác để làm cho điều này hoạt động. Nhiệm vụ hãy để chúng tôi giải quyết câu hỏi một cách rất tự nhiên.


2

Như thể chưa có đủ câu trả lời ở đây: 0), phần sau sẽ đóng gói thành giải pháp của static method @ kwl ở trên (giải pháp đầu tiên).

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

Sử dụng

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

1

Ví dụ về luồng đơn giản để giải quyết vấn đề này

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

hoặc một chuỗi tĩnh lên trên để lấy toàn bộ dòng.


1

Tôi trường hợp này hoạt động tốt:

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

Điều này không phải là tốt đẹp và ngắn?

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

    // Handle keyInfo value here...
}

1
SpinWait là cái quái gì vậy?
john ktejik

1

Đây là một ví dụ đầy đủ hơn về giải pháp của Glen Slayden. Tôi đã cố tình thực hiện điều này khi xây dựng trường hợp thử nghiệm cho một vấn đề khác. Nó sử dụng I / O không đồng bộ và sự kiện đặt lại thủ công.

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

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

1

Mã của tôi hoàn toàn dựa trên câu trả lời của người bạn @JSQuareD

Nhưng tôi cần sử dụng Stopwatchđể hẹn giờ vì khi tôi hoàn thành chương trình, Console.ReadKey()nó vẫn đang chờ Console.ReadLine()và nó tạo ra hành vi không mong muốn.

Nó làm việc hoàn hảo cho tôi. Duy trì Console ban đầu.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

Một cách rẻ tiền khác để có được luồng thứ 2 là quấn nó trong một đại biểu.


0

Ví dụ thực hiện bài đăng của Eric ở trên. Ví dụ cụ thể này được sử dụng để đọc thông tin được chuyển đến ứng dụng bảng điều khiển qua đường dẫ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
    }
}

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

Lưu ý rằng nếu bạn đi xuống tuyến đường "Console.ReadKey", bạn sẽ mất một số tính năng thú vị của ReadLine, cụ thể là:

  • Hỗ trợ xóa, xóa lùi, phím mũi tên, v.v.
  • Khả năng nhấn phím "lên" và lặp lại lệnh cuối cùng (điều này rất hữu ích nếu bạn triển khai bảng điều khiển gỡ lỗi nền được sử dụng nhiều).

Để thêm thời gian chờ, hãy thay đổi vòng lặp while cho phù hợp.


0

Xin đừng ghét tôi vì đã thêm một giải pháp khác cho vô số câu trả lời hiện có! Điều này hoạt động cho Console.ReadKey (), nhưng có thể dễ dàng sửa đổi để hoạt động với ReadLine (), v.v.

Vì các phương thức "Console.Read" đang chặn, cần phải " di chuyển " luồng StdIn để hủy quá trình đọc.

Cú pháp gọi:

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

Mã:

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

Đây là một giải pháp sử dụng Console.KeyAvailable. Đây là những cuộc gọi đang chặn, nhưng sẽ khá nhỏ nếu gọi chúng không đồng bộ qua TPL nếu muốn. Tôi đã sử dụng các cơ chế hủy tiêu chuẩn để giúp dễ dàng kết nối với Task Asynchronous Pattern và tất cả những thứ tốt đó.

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

Có một số bất lợi với điều này.

  • Bạn không nhận được các tính năng điều hướng tiêu chuẩn ReadLinecung cấp (cuộn mũi tên lên / xuống, v.v.).
  • Thao tác này sẽ đưa các ký tự '\ 0' vào đầu vào nếu một phím đặc biệt được nhấn (F1, PrtScn, v.v.). Tuy nhiên, bạn có thể dễ dàng lọc chúng ra bằng cách sửa đổi mã.

0

Đã kết thúc ở đây vì một câu hỏi trùng lặp đã được hỏi. Tôi đã đưa ra giải pháp sau đây có vẻ đơn giản. Tôi chắc chắn rằng nó có một số nhược điểm mà tôi đã bỏ qua.

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

Tôi đã đi đến câu trả lời này và kết thúc:

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

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

        int counter = 0;

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

            cki = Console.ReadKey(false);

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

0

Một ví dụ đơn giản bằng cách sử dụng 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");
}

Điều gì sẽ xảy ra nếu người dùng nhấn phím và thả ra trong vòng 2000ms?
Izzy

0

Mã dựa trên Tác vụ và hiện đại hơn nhiều sẽ trông giống như sau:

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

Tôi gặp một tình huống duy nhất là có Ứng dụng Windows (Dịch vụ Windows). Khi chạy chương trình tương tác Environment.IsInteractive(VS Debugger hoặc từ cmd.exe), tôi đã sử dụng AttachConsole / AllocConsole để lấy stdin / stdout của mình. Để ngăn quá trình kết thúc trong khi công việc đang được thực hiện, chuỗi giao diện người dùng sẽ gọi Console.ReadKey(false). Tôi muốn hủy việc chờ đợi mà chuỗi giao diện người dùng đang thực hiện từ một chuỗi khác, vì vậy tôi đã đưa ra một sửa đổi cho giải pháp của @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();
  }    
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.