จะเพิ่ม Timeout ใน Console.ReadLine () ได้อย่างไร?


122

ฉันมีแอปคอนโซลที่ฉันต้องการให้ผู้ใช้xวินาทีในการตอบสนองต่อการแจ้งเตือน หากไม่มีการป้อนข้อมูลหลังจากช่วงเวลาหนึ่งตรรกะของโปรแกรมควรดำเนินต่อไป เราถือว่าการหมดเวลาหมายถึงการตอบสนองที่ว่างเปล่า

วิธีที่ตรงไปตรงมาที่สุดในการเข้าถึงสิ่งนี้คืออะไร?

คำตอบ:


112

ฉันประหลาดใจที่ทราบว่าหลังจากผ่านไป 5 ปีคำตอบทั้งหมดยังคงประสบปัญหาต่อไปนี้อย่างน้อยหนึ่งข้อ:

  • มีการใช้ฟังก์ชันอื่นที่ไม่ใช่ ReadLine ทำให้สูญเสียฟังก์ชันการทำงาน (ลบ / backspace / up-key สำหรับอินพุตก่อนหน้า)
  • ฟังก์ชันจะทำงานไม่ดีเมื่อเรียกใช้หลาย ๆ ครั้ง (การวางไข่หลายเธรด, 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ไป


1
ฉันได้รับ NullReferenceException ตามรหัสนี้ ฉันคิดว่าฉันสามารถแก้ไขการเริ่มเธรดได้ในครั้งเดียวที่มีการสร้างเหตุการณ์อัตโนมัติ
Augusto Pedraza

1
@JSQuareD ฉันไม่คิดว่าการรอคอยที่วุ่นวายด้วยการนอนหลับ (200 มิลลิวินาที) นั้นมากขนาดhorrible wasteนั้น แต่แน่นอนว่าการส่งสัญญาณของคุณนั้นเหนือกว่า นอกจากนี้การใช้การConsole.ReadLineโทรแบบปิดกั้นหนึ่งครั้งในการวนซ้ำแบบไม่สิ้นสุดในภัยคุกคามที่สองจะช่วยป้องกันปัญหาเกี่ยวกับการโทรจำนวนมากที่ห้อยอยู่เบื้องหลังเช่นเดียวกับการแก้ปัญหาด้านล่าง ขอบคุณสำหรับการแบ่งปันรหัสของคุณ +1
Roland

2
หากคุณไม่สามารถป้อนข้อมูลได้ทันเวลาดูเหมือนว่าวิธีนี้จะหยุดชะงักในการConsole.ReadLine()โทรครั้งแรกที่คุณทำ คุณจะจบลงด้วย "ผี" ReadLineที่ต้องทำให้เสร็จก่อน
Derek

1
@Derek ขออภัยคุณไม่สามารถผสมวิธีนี้กับการโทร ReadLine ปกติการโทรทั้งหมดจะต้องทำผ่าน Reader วิธีแก้ปัญหานี้คือการเพิ่ม aethod ให้กับเครื่องอ่านที่รอ gotInput โดยไม่มีการหมดเวลา ขณะนี้ฉันใช้มือถือจึงไม่สามารถเพิ่มคำตอบได้อย่างง่ายดาย
JSQuareD

1
ฉันไม่เห็นความจำเป็นสำหรับ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
ฉันไม่รู้ว่าทำไมถึงไม่ได้รับการโหวต - มันทำงานได้อย่างไม่มีที่ติจริงๆ โซลูชันอื่น ๆ จำนวนมากเกี่ยวข้องกับ "ReadKey ()" ซึ่งทำงานไม่ถูกต้องนั่นหมายความว่าคุณสูญเสียพลังทั้งหมดของ ReadLine () เช่นการกดปุ่ม "ขึ้น" เพื่อรับคำสั่งที่พิมพ์ไว้ก่อนหน้านี้โดยใช้ backspace และ ปุ่มลูกศร ฯลฯ
Contango

9
@Gravitas: ไม่ได้ผล มันใช้ได้ครั้งเดียว แต่ทุกครั้งที่ReadLineคุณโทรมานั่งรอการป้อนข้อมูล หากคุณเรียกมัน 100 ครั้งจะสร้าง 100 เธรดซึ่งจะไม่หายไปทั้งหมดจนกว่าคุณจะกด Enter 100 ครั้ง!
Gabe

2
ระวัง. วิธีแก้ปัญหานี้ดูเรียบร้อย แต่ฉันได้รับสายที่ไม่สำเร็จนับพันครั้ง จึงไม่เหมาะหากเรียกซ้ำ.
ทอมเมฆินทร์

@Gabe, shakinfree: การโทรหลายครั้งไม่ได้รับการพิจารณาสำหรับวิธีแก้ปัญหา แต่เป็นการโทรแบบ async เพียงครั้งเดียวพร้อมการหมดเวลา ฉันเดาว่าผู้ใช้จะสับสนที่จะพิมพ์ข้อความ 10 ข้อความบนคอนโซลแล้วป้อนอินพุตให้ทีละรายการตามลำดับ อย่างไรก็ตามสำหรับสายที่แขวนอยู่คุณสามารถลองแสดงความคิดเห็นในบรรทัด TimedoutException และส่งคืนสตริงว่าง / ว่างได้หรือไม่?
gp.

ไม่ ... ปัญหาคือ Console.ReadLine ยังคงบล็อกเธรดพูลเธรดซึ่งกำลังเรียกใช้เมธอด Console.ReadLine จาก ReadLineDelegate
gp.

27

แนวทางนี้จะใช้Console.KeyAvailableช่วยไหม

class Sample 
{
    public static void Main() 
    {
    ConsoleKeyInfo cki = new ConsoleKeyInfo();

    do {
        Console.WriteLine("\nPress a key to display; press the 'x' key to quit.");

// Your code could perform some useful task in the following loop. However, 
// for the sake of this example we'll merely pause for a quarter second.

        while (Console.KeyAvailable == false)
            Thread.Sleep(250); // Loop until input is entered.
        cki = Console.ReadKey(true);
        Console.WriteLine("You pressed the '{0}' key.", cki.Key);
        } while(cki.Key != ConsoleKey.X);
    }
}

นี่เป็นเรื่องจริง OP ดูเหมือนจะต้องการบล็อกการโทรแม้ว่าฉันจะสั่นเล็กน้อยเมื่อคิดว่า ... นี่น่าจะเป็นทางออกที่ดีกว่า
GEOCHET

ฉันแน่ใจว่าคุณได้เห็นสิ่งนี้ ได้มาจาก google social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…
Gulzar Nazim

ฉันไม่เห็นว่า "หมดเวลา" นี้เป็นอย่างไรหากผู้ใช้ไม่ทำอะไรเลย ทั้งหมดนี้อาจเป็นไปได้ว่าจะดำเนินการตรรกะในพื้นหลังจนกว่าจะกดปุ่มและตรรกะอื่น ๆ จะดำเนินต่อไป
mphair

จริงสิ่งนี้ต้องได้รับการแก้ไข แต่มันง่ายพอที่จะเพิ่มระยะหมดเวลาให้กับเงื่อนไขการวนซ้ำ
Jonathan Allen

KeyAvailableระบุเพียงว่าผู้ใช้เริ่มพิมพ์ input ใน ReadLine แต่เราต้องการเหตุการณ์เมื่อกด Enter ซึ่งจะทำให้ ReadLine กลับมา โซลูชันนี้ใช้ได้กับ ReadKey เท่านั้นกล่าวคือรับอักขระเพียงตัวเดียว เนื่องจากไม่สามารถแก้ปัญหาที่แท้จริงของ ReadLine ได้ฉันจึงใช้วิธีแก้ปัญหาของคุณไม่ได้ -1 ขอโทษ
Roland

13

สิ่งนี้ได้ผลสำหรับฉัน

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
ฉันคิดว่านี่เป็นวิธีที่ดีที่สุดและง่ายที่สุดโดยใช้เครื่องมือที่มีอยู่แล้ว เยี่ยมมาก!
Vippy

2
สวย! ความเรียบง่ายเป็นความซับซ้อนขั้นสูงสุด ยินดีด้วย!
BrunoSalvino

10

ไม่ทางใดก็ทางหนึ่งคุณต้องมีเธรดที่สอง คุณสามารถใช้ IO แบบอะซิงโครนัสเพื่อหลีกเลี่ยงการประกาศของคุณเอง:

  • ประกาศ ManualResetEvent เรียกว่า "evt"
  • เรียก System.Console.OpenStandardInput เพื่อรับอินพุตสตรีม ระบุวิธีการโทรกลับที่จะจัดเก็บข้อมูลและตั้งค่า evt
  • เรียกใช้เมธอด BeginRead ของสตรีมเพื่อเริ่มการดำเนินการอ่านแบบอะซิงโครนัส
  • จากนั้นป้อนการรอตามกำหนดเวลาใน ManualResetEvent
  • หากการรอหมดเวลาให้ยกเลิกการอ่าน

หากการอ่านส่งคืนข้อมูลให้ตั้งค่าเหตุการณ์และเธรดหลักของคุณจะดำเนินการต่อมิฉะนั้นคุณจะดำเนินการต่อหลังจากหมดเวลา


นี่คือสิ่งที่โซลูชันที่ยอมรับทำได้ไม่มากก็น้อย
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

ฉันคิดว่าคุณจะต้องสร้างเธรดรองและสำรวจความคิดเห็นเกี่ยวกับคีย์บนคอนโซล ฉันรู้ว่าไม่มีทางที่จะทำสิ่งนี้ให้สำเร็จได้


ใช่ถ้าคุณมีเธรดที่สองสำหรับการสำรวจคีย์และแอปของคุณปิดในขณะที่กำลังนั่งรออยู่เธรดการสำรวจที่สำคัญนั้นจะนั่งอยู่ที่นั่นรอตลอดไป
Kelly Elton

อันที่จริง: ไม่ว่าจะเป็นเธรดที่สองหรือตัวแทนที่มี "BeginInvoke" (ซึ่งใช้เธรดเบื้องหลัง - ดูคำตอบจาก @gp)
Contango

@ kelton52 เธรดรองจะปิดหรือไม่หากคุณสิ้นสุดกระบวนการในตัวจัดการงาน
Arlen Beiler

6

ฉันต่อสู้กับปัญหานี้เป็นเวลา 5 เดือนก่อนที่จะพบวิธีแก้ปัญหาที่ทำงานได้อย่างสมบูรณ์แบบในสภาพแวดล้อมขององค์กร

ปัญหาของวิธีแก้ปัญหาส่วนใหญ่ที่ผ่านมาคือพวกเขาพึ่งพาสิ่งอื่นที่ไม่ใช่ Console.ReadLine () และ Console.ReadLine () มีข้อดีมากมาย:

  • รองรับการลบย้อนกลับปุ่มลูกศร ฯลฯ
  • ความสามารถในการกดปุ่ม "ขึ้น" และทำซ้ำคำสั่งสุดท้าย (สิ่งนี้มีประโยชน์มากหากคุณใช้คอนโซลการดีบักพื้นหลังที่ใช้งานได้มาก)

วิธีแก้ปัญหาของฉันมีดังนี้:

  1. วางเธรดแยกต่างหากเพื่อจัดการอินพุตของผู้ใช้โดยใช้ Console.ReadLine ()
  2. หลังจากช่วงเวลาที่หมดเวลา Console.ReadLine ยกเลิกการปิดกั้น () โดยการส่งคีย์ [Enter] ลงในหน้าต่างคอนโซลปัจจุบันใช้http://inputsimulator.codeplex.com/

รหัสตัวอย่าง:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

ข้อมูลเพิ่มเติมเกี่ยวกับเทคนิคนี้รวมถึงเทคนิคที่ถูกต้องในการยกเลิกเธรดที่ใช้ Console.ReadLine:

เรียก. NET เพื่อส่ง [Enter] การกดแป้นพิมพ์ในกระบวนการปัจจุบันซึ่งเป็นแอปคอนโซล

วิธีการยกเลิกเธรดอื่นใน. NET เมื่อเธรดดังกล่าวกำลังดำเนินการ Console.ReadLine?


5

การเรียก Console.ReadLine () ใน delegate นั้นไม่ดีเพราะหากผู้ใช้ไม่กด 'enter' การโทรนั้นจะไม่กลับมา เธรดที่ดำเนินการกับตัวแทนจะถูกบล็อกจนกว่าผู้ใช้จะกด 'enter' โดยจะไม่มีทางยกเลิกได้

การออกลำดับของการโทรเหล่านี้จะไม่ทำงานตามที่คุณคาดหวัง พิจารณาสิ่งต่อไปนี้ (โดยใช้คลาสคอนโซลตัวอย่างจากด้านบน):

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 ครั้งที่สองจะยังคงบล็อกการหมดเวลาจะสิ้นสุดลงในที่สุดและค่าที่ส่งกลับจะเป็นค่าเริ่มต้นอีกครั้ง

BTW- มีข้อผิดพลาดในโค้ดด้านบน โดยการเรียก waitHandle.Close () คุณปิดเหตุการณ์จากใต้เธรดผู้ปฏิบัติงาน หากผู้ใช้กด 'Enter' หลังจากหมดเวลาหมดเวลาเธรดผู้ปฏิบัติงานจะพยายามส่งสัญญาณเหตุการณ์ที่พ่น ObjectDisposedException ข้อยกเว้นจะถูกส่งออกจากเธรดของผู้ปฏิบัติงานและหากคุณไม่ได้ตั้งค่าตัวจัดการข้อยกเว้นที่ไม่สามารถจัดการได้กระบวนการของคุณจะสิ้นสุดลง


1
คำว่า "ด้านบน" ในโพสต์ของคุณไม่ชัดเจนและสับสน หากคุณกำลังอ้างถึงคำตอบอื่นคุณควรสร้างลิงก์ที่เหมาะสมไปยังคำตอบนั้น
bzlm

5

หากคุณอยู่ใน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()วิธีการasync ดังนั้นจึงควรใช้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;

4

ฉันอาจจะอ่านคำถามมากเกินไป แต่ฉันคิดว่าการรอจะคล้ายกับเมนูบูตที่รอ 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);
}

2

ฉันไม่สามารถแสดงความคิดเห็นในโพสต์ของ Gulzar ได้ แต่นี่เป็นตัวอย่างที่สมบูรณ์กว่า:

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

โปรดทราบว่าคุณยังสามารถใช้ ConsoleIn.Peek () ได้หากมองไม่เห็นคอนโซล (?) หรืออินพุตถูกนำมาจากไฟล์
Jamie Kitson

2

แก้ไข : แก้ไขปัญหาโดยให้งานจริงทำในกระบวนการแยกต่างหากและฆ่ากระบวนการนั้นหากหมดเวลา ดูรายละเอียดด้านล่าง ต๊าย!

เพียงแค่ให้การทำงานนี้และดูเหมือนว่าจะทำงานได้ดี เพื่อนร่วมงานของฉันมีเวอร์ชันที่ใช้อ็อบเจกต์ 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());
      }
   }
}

2
การเรียกใช้ไฟล์ปฏิบัติการแยกต่างหากในกระบวนการใหม่เท่านั้นเพื่อทำ ReadLine () ที่ตั้งเวลาไว้ฟังดูเหมือน overkill มาก คุณกำลังแก้ปัญหาที่ไม่สามารถยกเลิก ReadLine () - บล็อกเธรดโดยการตั้งค่าและฉีกกระบวนการทั้งหมดแทน
bzlm

จากนั้นแจ้งให้ Microsoft ทราบซึ่งทำให้เราอยู่ในตำแหน่งนี้
Jesse C. Slicer

Microsoft ไม่ได้ทำให้คุณอยู่ในตำแหน่งนั้น ดูคำตอบอื่น ๆ ที่ทำงานเดียวกันในสองสามบรรทัด ฉันคิดว่ารหัสด้านบนควรได้รับรางวัลบางประเภท - แต่ไม่ใช่ประเภทที่คุณต้องการ :)
Contango

1
ไม่คำตอบอื่นใดไม่ตรงตามที่ OP ต้องการ พวกเขาทั้งหมดสูญเสียคุณสมบัติของรูทีนอินพุตมาตรฐานหรือถูกวางสายกับความจริงที่ว่าคำขอทั้งหมดConsole.ReadLine() ถูกบล็อกและจะระงับการป้อนข้อมูลในคำขอถัดไป คำตอบที่ยอมรับนั้นค่อนข้างใกล้เคียง แต่ก็ยังมีข้อ จำกัด
Jesse C. Slicer

1
อืมไม่มันไม่ใช่ บัฟเฟอร์อินพุตยังคงบล็อก (แม้ว่าโปรแกรมจะไม่) ลองด้วยตัวคุณเอง: ป้อนอักขระบางตัว แต่ไม่ต้องกด Enter ปล่อยให้หมดเวลา จับข้อยกเว้นในผู้โทร จากนั้นมีอีกรายการReadLine()ในโปรแกรมของคุณหลังจากเรียกสิ่งนี้ ดูว่าเกิดอะไรขึ้น คุณต้องกดส่งคืน TWICE เพื่อให้ไปได้เนื่องจากลักษณะเธรดเดียวของไฟล์Console. มัน. ไม่ งาน.
Jesse C. Slicer

2

.NET 4 ทำให้สิ่งนี้ง่ายอย่างเหลือเชื่อโดยใช้ Tasks

ขั้นแรกสร้างผู้ช่วยของคุณ:

   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 ขึ้นมาใหม่หรือทำการแฮ็กที่เป็นอันตรายอื่น ๆ เพื่อให้ทำงานได้ Tasks ช่วยให้เราสามารถแก้ปัญหาได้อย่างเป็นธรรมชาติ


2

ราวกับว่ามีคำตอบไม่เพียงพอที่นี่: 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");
    }

1

ตัวอย่างเธรดง่ายๆเพื่อแก้ปัญหานี้

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

หรือสตริงแบบคงที่ขึ้นด้านบนเพื่อรับทั้งบรรทัด


1

กรณีของฉันทำงานได้ดี:

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

นี่คือตัวอย่างที่สมบูรณ์ยิ่งขึ้นของการแก้ปัญหาของ 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!");
        }
    }

1

รหัสของฉันอ้างอิงจากคำตอบของเพื่อน @JSQuareD ทั้งหมด

แต่ฉันจำเป็นต้องใช้Stopwatchเพื่อจับเวลาเพราะเมื่อฉันทำโปรแกรมเสร็จโดยที่Console.ReadKey()มันยังคงรอConsole.ReadLine()และมันสร้างพฤติกรรมที่ไม่คาดคิด

มันทำงานได้อย่างสมบูรณ์แบบสำหรับฉัน ดูแลคอนโซลเดิม 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

ตัวอย่างการใช้งานโพสต์ของ 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
    }
}

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

โปรดทราบว่าหากคุณไปตามเส้นทาง "Console.ReadKey" คุณจะสูญเสียคุณสมบัติเจ๋ง ๆ บางอย่างของ ReadLine ได้แก่ :

  • รองรับการลบย้อนกลับปุ่มลูกศร ฯลฯ
  • ความสามารถในการกดปุ่ม "ขึ้น" และทำซ้ำคำสั่งสุดท้าย (สิ่งนี้มีประโยชน์มากหากคุณใช้คอนโซลการดีบักพื้นหลังที่ใช้งานได้มาก)

หากต้องการเพิ่มระยะหมดเวลาให้ปรับเปลี่ยนลูป while ให้เหมาะสม


0

โปรดอย่าเกลียดฉันที่เพิ่มวิธีแก้ปัญหาอื่นให้กับคำตอบที่มีอยู่มากมาย! ใช้งานได้กับ 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);
}

0

นี่คือวิธีแก้ปัญหาที่ใช้Console.KeyAvailable. สิ่งเหล่านี้กำลังปิดกั้นการโทร แต่การโทรแบบอะซิงโครนัสผ่าน TPL ควรเป็นเรื่องเล็กน้อยหากต้องการ ฉันใช้กลไกการยกเลิกมาตรฐานเพื่อให้ง่ายต่อการเชื่อมต่อกับ Task Asynchronous Pattern และสิ่งดีๆทั้งหมดนั้น

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มีให้ (การเลื่อนลูกศรขึ้น / ลง ฯลฯ )
  • สิ่งนี้จะฉีดอักขระ '\ 0' ลงในอินพุตหากมีการกดแป้นพิเศษ (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;
    }
}

0

ฉันมาถึงคำตอบนี้และจบลงด้วยการทำ:

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

ตัวอย่างง่ายๆโดยใช้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");
}

จะเกิดอะไรขึ้นถ้าผู้ใช้กดปุ่มและปล่อยให้ไปภายใน 2000ms?
Izzy

0

ร่วมสมัยมากขึ้นและโค้ดตามงานจะมีลักษณะดังนี้:

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

ฉันมีสถานการณ์เฉพาะในการมีแอปพลิเคชัน Windows (บริการ Windows) เมื่อรันโปรแกรมแบบโต้ตอบEnvironment.IsInteractive(VS Debugger หรือจาก cmd.exe) ฉันใช้ AttachConsole / AllocConsole เพื่อรับ stdin / stdout เพื่อไม่ให้กระบวนการสิ้นสุดลงในขณะที่กำลังดำเนินการอยู่ UI Thread จะเรียก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();
  }    
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.