Bagaimana cara menambahkan Timeout ke Console.ReadLine ()?


122

Saya memiliki aplikasi konsol di mana saya ingin memberi pengguna x detik untuk menanggapi permintaan. Jika tidak ada masukan yang dibuat setelah jangka waktu tertentu, logika program harus dilanjutkan. Kami menganggap batas waktu berarti respons kosong.

Apa cara paling mudah untuk mendekati ini?

Jawaban:


112

Saya terkejut mengetahui bahwa setelah 5 tahun, semua jawaban masih mengalami satu atau lebih masalah berikut:

  • Fungsi selain ReadLine digunakan, menyebabkan hilangnya fungsionalitas. (Hapus / spasi mundur / tombol atas untuk masukan sebelumnya).
  • Fungsi berperilaku buruk ketika dipanggil beberapa kali (memunculkan banyak utas, banyak ReadLine yang menggantung, atau perilaku yang tidak terduga).
  • Fungsi bergantung pada sibuk-menunggu. Yang merupakan pemborosan yang mengerikan karena menunggu diperkirakan berjalan di mana saja dari beberapa detik hingga batas waktu, yang mungkin beberapa menit. Penantian yang sibuk yang berlangsung selama jumlah waktu seperti itu adalah menyedot sumber daya yang mengerikan, yang sangat buruk dalam skenario multithreading. Jika busy-wait diubah dengan sleep ini memiliki efek negatif pada responsivitas, meskipun saya akui bahwa ini mungkin bukan masalah besar.

Saya yakin solusi saya akan menyelesaikan masalah asli tanpa menderita salah satu masalah di atas:

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

Menelepon, tentu saja, sangat mudah:

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

Atau, Anda dapat menggunakan TryXX(out)konvensi, seperti yang disarankan 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;
  }

Yang disebut sebagai berikut:

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

Dalam kedua kasus, Anda tidak dapat menggabungkan panggilan ke Readerdengan Console.ReadLinepanggilan biasa : jika Readerwaktu habis, akan ada ReadLinepanggilan gantung . Sebaliknya, jika Anda ingin melakukan panggilan normal (tidak berjangka waktu) ReadLine, cukup gunakan Readerdan hilangkan waktu tunggu, sehingga secara default ke waktu tunggu tak terbatas.

Jadi bagaimana dengan masalah dari solusi lain yang saya sebutkan?

  • Seperti yang Anda lihat, ReadLine digunakan, menghindari masalah pertama.
  • Fungsi ini berperilaku dengan baik saat dipanggil beberapa kali. Terlepas dari apakah waktu tunggu terjadi atau tidak, hanya satu utas latar belakang yang akan berjalan dan paling banyak hanya satu panggilan ke ReadLine yang akan aktif. Memanggil fungsi tersebut akan selalu menghasilkan masukan terbaru, atau waktu tunggu, dan pengguna tidak perlu menekan enter lebih dari sekali untuk mengirimkan masukannya.
  • Dan, jelas, fungsinya tidak bergantung pada sibuk-menunggu. Sebaliknya ia menggunakan teknik multithreading yang tepat untuk mencegah pemborosan sumber daya.

Satu-satunya masalah yang saya perkirakan dengan solusi ini adalah bahwa ini tidak aman untuk thread. Namun, beberapa utas tidak dapat benar-benar meminta input pengguna pada saat yang sama, jadi sinkronisasi harus dilakukan sebelum melakukan panggilan ke sana Reader.ReadLine.


1
Saya mendapat NullReferenceException setelah kode ini. Saya pikir saya bisa memperbaiki memulai utas satu kali saat peristiwa otomatis dibuat.
Augusto Pedraza

1
@ JSQuareD Saya tidak berpikir bahwa kesibukan-menunggu dengan Tidur (200ms) itu banyak horrible waste, tapi tentu saja pensinyalan Anda lebih unggul. Selain itu, menggunakan satu Console.ReadLinepanggilan pemblokiran dalam loop tak terbatas dalam ancaman kedua mencegah masalah dengan banyak panggilan seperti itu yang berkeliaran di latar belakang seperti pada solusi lain yang diberi suara tinggi, di bawah ini. Terima kasih telah membagikan kode Anda. +1
Roland

2
Jika Anda gagal memasukkan tepat waktu, metode ini tampaknya berhenti pada Console.ReadLine()panggilan berikutnya yang Anda lakukan. Anda berakhir dengan "hantu" ReadLineyang harus diselesaikan terlebih dahulu.
Derek

1
@Derek sayangnya, Anda tidak dapat mencampur metode ini dengan panggilan ReadLine normal, semua panggilan harus dilakukan melalui Reader. Solusi untuk masalah ini adalah menambahkan metode ke pembaca yang menunggu gotInput tanpa batas waktu. Saat ini saya menggunakan perangkat seluler, jadi saya tidak dapat menambahkannya ke jawaban dengan sangat mudah.
JSQuareD

1
Saya tidak melihat perlunya 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
Saya tidak tahu mengapa ini belum dipilih - ini bekerja dengan sangat sempurna. Banyak solusi lain yang melibatkan "ReadKey ()", yang tidak berfungsi dengan baik: ini berarti Anda kehilangan semua kekuatan ReadLine (), seperti menekan tombol "atas" untuk mendapatkan perintah yang sebelumnya telah diketik, menggunakan backspace dan tombol panah, dll.
Contango

9
@ Gravitas: Ini tidak berhasil. Yah, itu berhasil sekali. Tetapi setiap ReadLineAnda menelepon duduk di sana menunggu masukan. Jika Anda menyebutnya 100 kali, itu membuat 100 utas yang tidak semuanya hilang sampai Anda menekan Enter 100 kali!
Gabe

2
Awas. Solusi ini tampak rapi tetapi saya berakhir dengan 1000-an panggilan yang belum terselesaikan yang dibiarkan menggantung. Jadi tidak cocok jika dipanggil berulang kali.
Tom Makin

@Gabe, shakinfree: beberapa panggilan tidak dipertimbangkan sebagai solusi tetapi hanya satu panggilan asinkron dengan batas waktu. Saya kira akan membingungkan pengguna untuk memiliki 10 pesan yang dicetak di konsol dan kemudian memasukkan input untuk mereka satu per satu dalam urutan masing-masing. Namun, untuk panggilan gantung, dapatkah Anda mencoba mengomentari baris TimedoutException dan mengembalikan string kosong / null?
gp.

tidak ... masalahnya adalah Console.ReadLine masih memblokir utas threadpool yang menjalankan metode Console.ReadLine dari ReadLineDelegate.
gp.

27

Akankah pendekatan ini menggunakan bantuan 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);
    }
}

Ini benar, OP tampaknya menginginkan panggilan pemblokiran, meskipun saya sedikit ngeri memikirkannya ... Ini mungkin solusi yang lebih baik.
GEOCHET

Saya yakin Anda pernah melihat ini. Mengerti dari google social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/…
Gulzar Nazim

Saya tidak melihat bagaimana "batas waktu" ini jika pengguna tidak melakukan apa pun. Semua ini akan dilakukan mungkin tetap menjalankan logika di latar belakang sampai tombol ditekan dan logika lainnya berlanjut.
mphair

Benar, ini perlu diperbaiki. Tetapi cukup mudah untuk menambahkan batas waktu ke kondisi pengulangan.
Jonathan Allen

KeyAvailablehanya menunjukkan bahwa pengguna telah mulai mengetikkan input ke ReadLine, tetapi kami memerlukan acara saat menekan Enter, yang membuat ReadLine kembali. Solusi ini hanya berfungsi untuk ReadKey, yaitu mendapatkan hanya satu karakter. Karena ini tidak menyelesaikan pertanyaan sebenarnya untuk ReadLine, saya tidak dapat menggunakan solusi Anda. -1 maaf
Roland

13

Ini berhasil untuk saya.

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
Saya pikir ini adalah solusi terbaik dan paling sederhana menggunakan alat yang sudah ada di dalamnya. Kerja bagus!
Vippy

2
Cantik! Kesederhanaan benar-benar merupakan kecanggihan tertinggi. Selamat!
BrunoSalvino

10

Dengan satu atau lain cara Anda memang membutuhkan utas kedua. Anda dapat menggunakan IO asinkron untuk menghindari deklarasi milik Anda sendiri:

  • mendeklarasikan ManualResetEvent, sebut saja "evt"
  • panggil System.Console.OpenStandardInput untuk mendapatkan aliran input. Tentukan metode callback yang akan menyimpan datanya dan menyetel evt.
  • panggil metode BeginRead aliran tersebut untuk memulai operasi baca asinkron
  • lalu masukkan waktu tunggu pada ManualResetEvent
  • jika waktu tunggu habis, maka batalkan pembacaan

Jika pembacaan mengembalikan data, setel acara dan utas utama Anda akan dilanjutkan, jika tidak Anda akan melanjutkan setelah waktu tunggu.


Ini kurang lebih seperti yang dilakukan Solusi yang Diterima.
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

Saya pikir Anda perlu membuat utas sekunder dan polling untuk kunci di konsol. Saya tahu tidak ada cara yang dibangun untuk mencapai ini.


Ya, jika Anda memiliki thread kedua yang melakukan polling untuk kunci, dan aplikasi Anda ditutup saat sedang menunggu, thread polling kunci itu akan duduk di sana, menunggu, selamanya.
Kelly Elton

Sebenarnya: utas kedua, atau delegasi dengan "BeginInvoke" (yang menggunakan utas, di belakang layar - lihat jawaban dari @gp).
Contango

@ kelton52, Akankah utas sekunder berhenti jika Anda mengakhiri proses di Pengelola Tugas?
Arlen Beiler

6

Saya berjuang dengan masalah ini selama 5 bulan sebelum saya menemukan solusi yang bekerja dengan sempurna dalam pengaturan perusahaan.

Masalah dengan sebagian besar solusi sejauh ini adalah mereka mengandalkan sesuatu selain Console.ReadLine (), dan Console.ReadLine () memiliki banyak keuntungan:

  • Dukungan untuk menghapus, spasi mundur, tombol panah, dll.
  • Kemampuan untuk menekan tombol "atas" dan mengulangi perintah terakhir (ini sangat berguna jika Anda menerapkan konsol debugging latar belakang yang sering digunakan).

Solusi saya adalah sebagai berikut:

  1. Buat utas terpisah untuk menangani masukan pengguna menggunakan Console.ReadLine ().
  2. Setelah periode waktu tunggu, buka blokir Console.ReadLine () dengan mengirimkan kunci [enter] ke jendela konsol saat ini, menggunakan http://inputsimulator.codeplex.com/ .

Kode sampel:

 InputSimulator.SimulateKeyPress(VirtualKeyCode.RETURN);

Informasi lebih lanjut tentang teknik ini, termasuk teknik yang benar untuk membatalkan utas yang menggunakan Console. ReadLine:

.NET panggilan untuk mengirim [enter] keystroke ke dalam proses saat ini, yang merupakan aplikasi konsol?

Bagaimana cara membatalkan utas lain di .NET, ketika utas tersebut menjalankan Console.ReadLine?


5

Memanggil Console.ReadLine () dalam delegasi buruk karena jika pengguna tidak menekan 'enter' maka panggilan itu tidak akan pernah kembali. Utas yang mengeksekusi delegasi akan diblokir hingga pengguna menekan 'enter', tanpa ada cara untuk membatalkannya.

Mengeluarkan urutan panggilan ini tidak akan berfungsi seperti yang Anda harapkan. Pertimbangkan hal berikut (menggunakan contoh kelas Console dari atas):

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

Pengguna membiarkan batas waktu kedaluwarsa untuk permintaan pertama, lalu memasukkan nilai untuk permintaan kedua. Baik firstName dan lastName akan berisi nilai default. Ketika pengguna menekan 'enter', panggilan ReadLine pertama akan selesai, tetapi kode telah meninggalkan panggilan itu dan pada dasarnya mengabaikan hasilnya. The kedua panggilan ReadLine akan terus memblokir, timeout akhirnya akan berakhir dan nilai yang dikembalikan lagi akan menjadi default.

BTW- Ada bug pada kode di atas. Dengan memanggil waitHandle.Close () Anda menutup acara dari bawah utas pekerja. Jika pengguna menekan 'enter' setelah waktu tunggu habis, thread pekerja akan mencoba memberi sinyal peristiwa yang menampilkan ObjectDisposedException. Pengecualian dilempar dari utas pekerja, dan jika Anda belum menyiapkan penangan pengecualian yang tidak tertangani, proses Anda akan dihentikan.


1
Istilah "di atas" dalam posting Anda bersifat ambigu dan membingungkan. Jika Anda mengacu pada jawaban lain, Anda harus membuat tautan yang tepat ke jawaban itu.
bzlm

5

Jika Anda dalam Main()metode ini, Anda tidak dapat menggunakan await, jadi Anda harus menggunakan Task.WaitAny():

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

Namun, C # 7.1 memperkenalkan kemungkinan untuk membuat Main()metode asinkron , jadi lebih baik untuk menggunakan Task.WhenAny()versi tersebut setiap kali Anda memiliki opsi itu:

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

Saya mungkin terlalu banyak membaca pertanyaan, tetapi saya berasumsi menunggu akan mirip dengan menu boot di mana ia menunggu 15 detik kecuali Anda menekan tombol. Anda dapat menggunakan (1) fungsi pemblokiran atau (2) Anda dapat menggunakan utas, acara, dan pengatur waktu. Acara tersebut akan bertindak sebagai 'lanjutan' dan akan memblokir hingga pengatur waktu berakhir atau tombol ditekan.

Pseudo-code untuk (1) adalah:

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

Sayangnya saya tidak dapat mengomentari kiriman Gulzar, tetapi berikut adalah contoh yang lebih lengkap:

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

Catatan Anda juga dapat menggunakan Console.In.Peek () jika konsol tidak terlihat (?) Atau input diarahkan dari file.
Jamie Kitson

2

EDIT : perbaiki masalah dengan meminta pekerjaan yang sebenarnya dilakukan dalam proses terpisah dan menghentikan proses itu jika waktu habis. Lihat detailnya di bawah. Wah!

Coba saja ini dan sepertinya bekerja dengan baik. Rekan kerja saya memiliki versi yang menggunakan objek Thread, tetapi saya menemukan metode BeginInvoke () dari tipe delegasi menjadi sedikit lebih elegan.

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

Proyek ReadLine.exe adalah proyek yang sangat sederhana yang memiliki satu kelas yang terlihat seperti ini:

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

2
Memanggil eksekusi terpisah dalam proses baru hanya untuk melakukan ReadLine () berwaktu terdengar seperti berlebihan. Anda pada dasarnya memecahkan masalah karena tidak dapat Membatalkan Thread Pemblokiran ReadLine () dengan menyiapkan dan menghancurkan seluruh proses sebagai gantinya.
bzlm

Kemudian ceritakan ke Microsoft, yang menempatkan kami pada posisi ini.
Jesse C. Slicer

Microsoft tidak menempatkan Anda pada posisi itu. Lihatlah beberapa jawaban lain yang melakukan pekerjaan yang sama dalam beberapa baris. Saya pikir kode di atas harus mendapatkan semacam penghargaan - tetapi bukan yang Anda inginkan :)
Contango

1
Tidak, tidak ada jawaban lain yang sesuai dengan keinginan OP. Semua dari mereka kehilangan fitur dari rutinitas masukan standar atau mendapatkan menutup telepon pada kenyataan bahwa semua permintaan untuk Console.ReadLine() yang menghalangi dan akan tahan masukan atas permintaan berikutnya. Jawaban yang diterima cukup dekat, tetapi masih memiliki keterbatasan.
Jesse C. Slicer

1
Um, tidak, tidak. Buffer input masih memblokir (bahkan jika program tidak). Cobalah sendiri: Masukkan beberapa karakter tetapi jangan tekan enter. Biarkan waktu habis. Tangkap pengecualian di penelepon. Kemudian miliki yang lain ReadLine()dalam program Anda setelah memanggil yang ini. Lihat apa yang terjadi. Anda harus menekan tombol kembali DUA KALI untuk melakukannya karena sifat utas tunggal Console. Itu. Tidak. Kerja.
Jesse C. Slicer

2

.NET 4 membuat ini sangat sederhana menggunakan Tasks.

Pertama, buat helper Anda:

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

Kedua, jalankan dengan tugas dan tunggu:

      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

Tidak ada upaya untuk membuat ulang fungsionalitas ReadLine atau melakukan peretasan berbahaya lainnya untuk membuatnya berfungsi. Tugas memungkinkan kami menyelesaikan pertanyaan dengan cara yang sangat alami.


2

Seolah-olah belum ada cukup jawaban di sini: 0), berikut ini merangkum menjadi solusi metode statis @ kwl di atas (yang pertama).

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

Pemakaian

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

1

Contoh threading sederhana untuk mengatasi ini

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

atau string statis di bagian atas untuk mendapatkan seluruh baris.


1

Saya kasus saya ini berfungsi dengan baik:

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

Bukankah ini bagus dan pendek?

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

    // Handle keyInfo value here...
}

1
Apa sih SpinWait itu?
john ktejik

1

Ini adalah contoh lengkap dari solusi Glen Slayden. Saya dengan senang hati membuat ini saat membuat kasus uji untuk masalah lain. Ini menggunakan asynchronous I / O dan acara reset manual.

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

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

1

Kode saya sepenuhnya didasarkan pada jawaban teman @JSQuareD

Tetapi saya perlu menggunakan Stopwatchto timer karena ketika saya menyelesaikan program Console.ReadKey()itu masih menunggu Console.ReadLine()dan menghasilkan perilaku yang tidak terduga.

Ini BERFUNGSI SEMPURNA bagi saya. Mempertahankan Konsol asli.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

Cara murah lainnya untuk mendapatkan utas kedua adalah membungkusnya dengan seorang delegasi.


0

Contoh implementasi postingan Eric di atas. Contoh khusus ini digunakan untuk membaca informasi yang diteruskan ke aplikasi konsol melalui pipa:

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

Perhatikan bahwa jika Anda menggunakan rute "Console.ReadKey", Anda kehilangan beberapa fitur keren ReadLine, yaitu:

  • Dukungan untuk menghapus, spasi mundur, tombol panah, dll.
  • Kemampuan untuk menekan tombol "atas" dan mengulangi perintah terakhir (ini sangat berguna jika Anda menerapkan konsol debugging latar belakang yang sering digunakan).

Untuk menambahkan batas waktu, ubah loop sementara agar sesuai.


0

Tolong jangan membenci saya karena menambahkan solusi lain ke kebanyakan jawaban yang ada! Ini berfungsi untuk Console.ReadKey (), tetapi dapat dengan mudah dimodifikasi untuk bekerja dengan ReadLine (), dll.

Karena metode "Console.Read" memblokir, aliran StdIn perlu " mendorong " aliran untuk membatalkan pembacaan.

Memanggil sintaks:

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

Kode:

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

Berikut adalah solusi yang digunakan Console.KeyAvailable. Ini adalah panggilan pemblokiran, tetapi seharusnya cukup sepele untuk memanggilnya secara asinkron melalui TPL jika diinginkan. Saya menggunakan mekanisme pembatalan standar untuk membuatnya mudah disambungkan dengan Pola Asinkron Tugas dan semua hal bagus itu.

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

Ada beberapa kekurangannya.

  • Anda tidak mendapatkan fitur navigasi standar yang ReadLinemenyediakan (scrolling panah atas / bawah, dll.).
  • Ini memasukkan karakter '\ 0' ke dalam input jika tombol khusus ditekan (F1, PrtScn, dll.). Anda dapat dengan mudah memfilternya dengan memodifikasi kodenya.

0

Berakhir di sini karena pertanyaan duplikat telah diajukan. Saya datang dengan solusi berikut yang terlihat langsung. Saya yakin ada beberapa kekurangan yang saya lewatkan.

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

Saya sampai pada jawaban ini dan akhirnya melakukan:

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

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

Bagaimana jika pengguna menekan tombol dan melepaskannya dalam 2000ms?
Izzy

0

Kode yang jauh lebih kontemporer dan berbasis tugas akan terlihat seperti ini:

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

Saya memiliki situasi unik karena memiliki Aplikasi Windows (Layanan Windows). Saat menjalankan program secara interaktif Environment.IsInteractive(VS Debugger atau dari cmd.exe), saya menggunakan AttachConsole / AllocConsole untuk mendapatkan stdin / stdout saya. Agar proses tidak berakhir saat pekerjaan sedang dilakukan, UI Thread memanggil Console.ReadKey(false). Saya ingin membatalkan penantian yang dilakukan utas UI dari utas lain, jadi saya membuat modifikasi pada solusi oleh @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();
  }    
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.