Jak utrzymać uruchomioną aplikację konsoli .NET?

105

Rozważ aplikację konsolową, która uruchamia niektóre usługi w osobnym wątku. Wszystko, co musi zrobić, to poczekać, aż użytkownik naciśnie Ctrl + C, aby go wyłączyć.

Która z poniższych opcji jest lepszym sposobem na zrobienie tego?

static ManualResetEvent _quitEvent = new ManualResetEvent(false);

static void Main() {
    Console.CancelKeyPress += (sender, eArgs) => {
        _quitEvent.Set();
        eArgs.Cancel = true;
    };

    // kick off asynchronous stuff 

    _quitEvent.WaitOne();

    // cleanup/shutdown and quit
}

Lub to, używając Thread.Sleep (1):

static bool _quitFlag = false;

static void Main() {
    Console.CancelKeyPress += delegate {
        _quitFlag = true;
    };

    // kick off asynchronous stuff 

    while (!_quitFlag) {
        Thread.Sleep(1);
    }

    // cleanup/shutdown and quit
}
intoOrbit
źródło

Odpowiedzi:

64

zawsze chcesz zapobiec używaniu pętli while, zwłaszcza gdy wymuszasz na kodzie ponowne sprawdzanie zmiennych. Marnuje zasoby procesora i spowalnia program.

Zdecydowanie powiedziałbym o pierwszym.

Mikrofon
źródło
2
+1. Ponadto, ponieważ boolnie jest zadeklarowane jako volatile, istnieje wyraźna możliwość, że kolejne odczyty _quitFlagw whilepętli zostaną zoptymalizowane, co prowadzi do nieskończonej pętli.
Adam Robinson
2
Brakuje zalecanego sposobu, aby to zrobić. Spodziewałem się tego jako odpowiedzi.
Iúri dos Anjos
30

Alternatywnie prostszym rozwiązaniem jest po prostu:

Console.ReadLine();
Cocowalla
źródło
Już miałem to zasugerować, ale nie skończy się to tylko na Ctrl-C
Thomas Levesque
Odniosłem wrażenie, że CTRL-C to tylko przykład - dowolne wejście użytkownika, aby je zamknąć
Cocowalla
Pamiętaj, że „Console.ReadLine ()” blokuje wątki. Tak więc aplikacja nadal działałaby, ale nie robiła nic poza czekaniem, aż użytkownik wejdzie do linii
fabriciorissetto
2
@fabriciorissetto Pytanie OP brzmi: „rozpocznij asynchroniczne rzeczy”, więc aplikacja będzie pracować nad innym wątkiem
Cocowalla
1
@Cocowalla Brakowało mi tego. Mój błąd!
fabriciorissetto
13

Możesz to zrobić (i usunąć CancelKeyPressmoduł obsługi zdarzeń):

while(!_quitFlag)
{
    var keyInfo = Console.ReadKey();
    _quitFlag = keyInfo.Key == ConsoleKey.C
             && keyInfo.Modifiers == ConsoleModifiers.Control;
}

Nie jestem pewien, czy to jest lepsze, ale nie podoba mi się pomysł wywoływania Thread.Sleeppętli. Myślę, że blokowanie danych wejściowych użytkownika jest czystsze.

Thomas Levesque
źródło
Nie podoba mi się, że sprawdzasz klawisze Ctrl + C, zamiast sygnału wyzwalanego przez Ctrl + C.
CodesInChaos
9

Wolę korzystać z Application.Run

static void Main(string[] args) {

   //Do your stuff here

   System.Windows.Forms.Application.Run();

   //Cleanup/Before Quit
}

z dokumentów:

Rozpoczyna uruchamianie standardowej pętli komunikatów aplikacji w bieżącym wątku, bez formularza.

ygaradon
źródło
10
Ale wtedy bierzesz zależność od formularzy Windows tylko w tym celu. Nie stanowi to większego problemu w przypadku tradycyjnego środowiska .NET, ale obecna tendencja polega na wdrażaniu modułowym obejmującym tylko potrzebne części.
CodesInChaos
4

Wygląda na to, że utrudniasz to, niż potrzebujesz. Dlaczego nie tylko Joinwątek po zasygnalizowaniu jego zatrzymania?

class Program
{
    static void Main(string[] args)
    {
        Worker worker = new Worker();
        Thread t = new Thread(worker.DoWork);
        t.IsBackground = true;
        t.Start();

        while (true)
        {
            var keyInfo = Console.ReadKey();
            if (keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers == ConsoleModifiers.Control)
            {
                worker.KeepGoing = false;
                break;
            }
        }
        t.Join();
    }
}

class Worker
{
    public bool KeepGoing { get; set; }

    public Worker()
    {
        KeepGoing = true;
    }

    public void DoWork()
    {
        while (KeepGoing)
        {
            Console.WriteLine("Ding");
            Thread.Sleep(200);
        }
    }
}
Ed Power
źródło
2
W moim przypadku nie kontroluję wątków, na których działają elementy asynchroniczne.
Orbit
1) Nie podoba mi się, że sprawdzasz klawisze Ctrl + C, zamiast sygnału wyzwalanego przez Ctrl + C. 2) Twoje podejście nie działa, jeśli aplikacja używa zadań zamiast pojedynczego wątku roboczego.
CodesInChaos
3

Możliwe jest również zablokowanie wątku / programu na podstawie tokenu anulowania.

token.WaitHandle.WaitOne();

WaitHandle jest sygnalizowany, gdy token jest anulowany.

Widziałem tę technikę używaną przez Microsoft.Azure.WebJobs.JobHost, gdzie token pochodzi ze źródła tokenu anulowania WebJobsShutdownWatcher (obserwator plików, który kończy zadanie).

Daje to pewną kontrolę nad tym, kiedy program może się zakończyć.

CRice
źródło
1
Jest to doskonała odpowiedź dla każdej rzeczywistej aplikacji konsolowej, która musi nasłuchiwać, CTL+Cponieważ wykonuje długotrwałą operację lub jest demonem, który powinien również z wdziękiem zamknąć swoje wątki robocze. Zrobiłbyś to za pomocą CancelToken, więc ta odpowiedź korzysta z WaitHandletego, który już istniałby, zamiast tworzyć nowy.
mdisibio
1

Z dwóch pierwszy jest lepszy

_quitEvent.WaitOne();

ponieważ w drugim wątek budzi się co jedną milisekundę i zostaje zamieniony na przerwanie systemu operacyjnego, co jest drogie

Naveen
źródło
To dobra alternatywa dla Consolemetod, jeśli nie masz podłączonej konsoli (bo np. Program jest uruchamiany przez usługę)
Marco Sulla
0

Powinieneś to zrobić tak, jak gdybyś programował usługę Windows. Nigdy nie użyłbyś instrukcji while zamiast tego użyłbyś delegata. WaitOne () jest zwykle używana podczas oczekiwania na usunięcie wątków - Thread.Sleep () - nie jest zalecane - Czy myślałeś o użyciu System.Timers.Timer przy użyciu tego zdarzenia do sprawdzenia zdarzenia zamknięcia?

KenL
źródło