Zawieszanie się programu konsolowego i desktopowego

0

Od pewnego czasu zainteresowała mnie pewna sprawa. Przez całe lata po prostu przyjmowałem do wiadomości, ze tak jest i nie drążyłem tematu, ale teraz nie rozumiem, dlaczego tak jest.

Załóżmy, że napiszę dwa takie same programy, jeden konsolowy, drugi w okienkach, oba programy będą w C# dla .NET. Nie zmienia to faktu, że w tej sprawie nie ma żadnego znaczenia, w jakim języku i w jakiej technologii pisze program.

Pierwszy program to program tekstowy w konsoli. Program czeka na naciśnięcie klawisza Enter, a potem wypisuje liczby od 1 do 10 w odstępach jednej sekundy. Uruchamiam program, naciskam Enter i przez 10 sekund, co sekundę pojawia się kolejna liczba, nie ma żadnej filozofii. Odmierzenie jednej sekundy wygeneruję za pomocą pustej pętli, która porównuje wskazanie zegara ze wskazaniem w chwili rozpoczęcia. Akurat w C# jest klasa Stopwatch, która ułatwia odmierzanie czasu. Oczywiście, że można skorzystać z Thread.Sleep, ale w tym przypadku chciałem zasymulować jakieś obciążające czynności.

Drugi program ma formatkę, która zawiera jedno pole tekstowe i jeden przycisk. W tym programie, do kliknięcia przycisku podłączam funkcję, która czyści pole tekstowe i dopisuje do pola kolejne liczby. Uruchamiam program, klikam przycisk i niespodzianka (ja teraz dobrze wiem, co się zdarzy, ale jak wieki temu zaczynałem przygodę z okienkami, to tego się nie spodziewałem). Program zupełnie zawiesi się, w menedżerze zadań przedstawiony jako "nie odpowiada", a dopiero po 10 sekundach odwiesi się i momentalnie wypisze wszystkie liczby.

Aby uzyskać oczekiwany efekt, to przerabiam program tak, żeby uruchamiał dodatkowy wątek i w tym wątku uruchomił wypisywanie liczb, jednak samo wypisanie też jest utrudnione, bo akurat w C# trzeba to robić poprzez wywołanie Invoke i obiekt delegate (dokładnie nie pamiętam, bo dawno tego nie robiłem).

Pytanie jest proste. Jak to jest, że robię dwa identyczne programy, różniące się tylko interfejsem (tekstowy i okienkowy), a w interfejsie tekstowym nic się nie wiesza, każde wypisanie pokazuje się natychmiast, a w okienkach, to uruchomienie funkcji zawiesza program aż do czasu zakończenia? Z czego wynika, że w przypadku, gdy w okienkach chce się coś wypisać, zmienić z dodatkowego wątku, to trzeba robić obejścia, a w programie tekstowym, niezależnie od tego, skąd wywoła się Console.Write(), to bez żadnego problemu tekst będzie wypisany?

Przerabiałem Lazarus, Winforms, GTK#, Qt, GTK+, Swing i w każdym przypadku, uruchomienie funkcji bez tworzenia dodatkowego wątku zawiesza program. Dlaczego to nie działa tak samo, jak w interfejsie tekstowym?

2

Chat GPT:

To jest dobre pytanie! W systemach operacyjnych, takich jak Windows, Linux czy MacOS, obsługa konsoli (terminala) jest zazwyczaj oddzielona od głównego wątku aplikacji. System operacyjny ma dedykowane procesy i wątki do obsługi interfejsu użytkownika, w tym migającej karetki w konsoli.

Podczas gdy Twój kod jest wykonywany na jednym wątku, system operacyjny równocześnie aktualizuje interfejs użytkownika (w tym migającą karetkę) na innym wątku. Dzięki temu, nawet jeśli Twój kod jest zajęty wykonywaniem długotrwałych operacji, karetka w konsoli nadal będzie migotać.

To jest jeden z przykładów, jak systemy operacyjne wykorzystują wielowątkowość do zapewnienia płynnej i responsywnej interakcji z użytkownikiem, nawet gdy aplikacje są zajęte. Wielowątkowość pozwala na równoczesne wykonywanie wielu zadań, co jest kluczowe dla efektywnego zarządzania zasobami komputera.

Stworzyłem aplikacje konsolową i faktycznie jest kilka procesów uruchamianych podczas jej wykonywania.
screenshot-20240131102117.png

2

Jest inna architektura, bo event driven w gui programach, do tego windows ma coś typu watchdog i jeśli aplikacja nie zapyta się systemu przez jakiś czas czy są nowe eventy to uznaje za zawieszony.

Przychodzą eventy do odświeżenia gui, resize, wyjścia z programu, kliknięcia buttona i tu pewnie masz jakiś onClick event, który twoją funkcję wywoła, a ten śpi sleepem i nie wraca do głównej pętli to system uznaje za zawieszony.

Sama konsola to jest terminal, w nim bash odpalasz i potem twoją apkę, chodź można też otworzyć to inaczej, bezpośrednio w terminalu apkę bez basha, dodatkowo są też różne sposoby otworzenia, fork i bez czyli zastąpienie procesu innym.
Jest trochę inaczej bo terminal to jakby kolejna aplikacja, która przesyła wejście stdin do aplikacji i stdout wypisuje ci na ekran z niej, więc tak jakby masz dwie aplikacje, jedna która przesyła strumień danych i odbiera, a druga co je przetwarza więc jest asynchroniczne przetworzenie.

W aplikacjach sterowanych eventowo nie można od tak zawiesić sobie sleepem pętli głównej bo tam się dzieją rzeczy typu odrysowanie okna, przychodzą do tej pętli zdarzenia np. naciśnięcie przycisku na klawiaturze, ruch myszką czy naciśnięcie i po prostu nie zostaną dostarczone messages głównej aplikacji zarządzającej gui całego systemu tym CSRSS.exe.

Ja w debugerze wielokrotnie przechodziłem od eventa przez całą kolejkę wiadomości do danej funkcji, która ją wykonywała.
Można w ten sposób i się nauczyć jak coś działa i zrozumieć funkcję, której się poszukuje tym samym ją znajdując.

1
andrzejlisek napisał(a):

jednak samo wypisanie też jest utrudnione, bo akurat w C# trzeba to robić poprzez wywołanie Invoke i obiekt delegate (dokładnie nie pamiętam, bo dawno tego nie robiłem).

Co?

W tak prostym przypadku przebiega to raczej tak samo w tej kwestii.

private async void Button_Click(object sender, EventArgs e)
{
    for (int i = 1; i <= 10; i++)
    {
        await Task.Delay(1_000);

        RichTextBox.AppendText($"{i}\n");        
    }
}
0
Ferdynand Lipski napisał(a):
andrzejlisek napisał(a):

jednak samo wypisanie też jest utrudnione, bo akurat w C# trzeba to robić poprzez wywołanie Invoke i obiekt delegate (dokładnie nie pamiętam, bo dawno tego nie robiłem).

Co?

W tak prostym przypadku przebiega to raczej tak samo w tej kwestii.

private async void Button_Click(object sender, EventArgs e)
{
    for (int i = 1; i <= 10; i++)
    {
        await Task.Delay(1_000);

        RichTextBox.AppendText($"{i}\n");        
    }
}

To jest WinForms, WPF, MUI, czy coś jeszcze innego?

W Winforms nie wiem, czy może być słówko async, nie próbowałem.

Natomiast bez słowa async to się skompiluje, ale wysypie się przy uruchamianiu. Własnie wyszukałem, że to musiało wtedy wygądać tak:

private delegate void D();

private void Button_Click(object sender, EventArgs e)
{
    for (int i = 1; i <= 10; i++)
    {
        System.Threading.Thread.Sleep(1000);

        Invoke((D)delegate
        {
            RichTextBox.AppendText($"{i}\n");        
        });
    }
}

Swego czasu, jak kiedyś tego potrzebowałem, to taki sposób znalazłem w internecie jako pierwszy lepszy i taki stosowałem. Jednakże starałem się unikać prób bezpośredniego odwołania do interfejsu z wątku innego niż interfejs.

0
Autysta napisał(a):

Jest inna architektura, bo event driven w gui programach, do tego windows ma coś typu watchdog i jeśli aplikacja nie zapyta się systemu przez jakiś czas czy są nowe eventy to uznaje za zawieszony.

Przychodzą eventy do odświeżenia gui, resize, wyjścia z programu, kliknięcia buttona i tu pewnie masz jakiś onClick event, który twoją funkcję wywoła, a ten śpi sleepem i nie wraca do głównej pętli to system uznaje za zawieszony.

Sama konsola to jest terminal, w nim bash odpalasz i potem twoją apkę, chodź można też otworzyć to inaczej, bezpośrednio w terminalu apkę bez basha, dodatkowo są też różne sposoby otworzenia, fork i bez czyli zastąpienie procesu innym.
Jest trochę inaczej bo terminal to jakby kolejna aplikacja, która przesyła wejście stdin do aplikacji i stdout wypisuje ci na ekran z niej, więc tak jakby masz dwie aplikacje, jedna która przesyła strumień danych i odbiera, a druga co je przetwarza więc jest asynchroniczne przetworzenie.

W aplikacjach sterowanych eventowo nie można od tak zawiesić sobie sleepem pętli głównej bo tam się dzieją rzeczy typu odrysowanie okna, przychodzą do tej pętli zdarzenia np. naciśnięcie przycisku na klawiaturze, ruch myszką czy naciśnięcie i po prostu nie zostaną dostarczone messages głównej aplikacji zarządzającej gui całego systemu tym CSRSS.exe.

Ja w debugerze wielokrotnie przechodziłem od eventa przez całą kolejkę wiadomości do danej funkcji, która ją wykonywała.
Można w ten sposób i się nauczyć jak coś działa i zrozumieć funkcję, której się poszukuje tym samym ją znajdując.

Teraz rozumiem, choć terminal działa identycznie, jak działał niegdyś DOS z tą różnica, ze w DOS mógł być uruchomiony tylko jeden program naraz i nie było żadnych wątków, a sprawę rozwiązywało się poprzez przerwania. Aplikacja wpadała w nieskończoną pustą pętlę, czyli była zawieszona. ale i tak kursor (zwany karetką) migotał, a komputer przyjmował klawisz np Ctrl+Alt+Del w celu restartu lub klawisz Print screen w celu druku ekranu.

Czy dobrze rozumiem, ze w przypadku GUI, to aplikacja w stanie niby bezczynności, tak naprawdę pracuje w nieskończonej pętli, w której co chwilę odpytuje WinAPI (kilkadziesiąt razy na sekundę?), czy dostała jakieś zdarzenie typu przemieścić okno, odmalowanie okna, przejazd myszką nad oknem, a wykonywanie funkji OnClick jest w ramach tej pętli, dlatego nie odpytuje i się zawiesza?

Nawet, jeżeli aplikacja wykonuje jedno zdarzenie typu onClick i nie może przyjmować kolejnych zdarzeń, to czy i tak system co rusz wyzwala zdarzenia naet, jak nie są podpięte funkcje, a niewywołanie zdarzenia powoduje zawieszenie?

1

No tak, a niby jak miała by aplikacja wykryć, że kliknąłeś w button?
Jakby nie było pętli to program by się w pewnym momencie skończył.
W konsoli robi się to tak, że wczytujesz z stdin tekst, a że jest to blokująca operacja to dopóki terminal nie wyśle na stdin aplikacji tekstu zakończonego znakiem nowej linii bo do tego jest wczytywane.
To po prostu stoi aplikacja zawieszona, ale terminal sobie działa dalej.

Najpierw eventami przychodzi x,y event z kliknięcia myszką, jest on procesowany i przychodzi potem drugi po tym event, który już odpowiada za konkretny button.

W surowym winapi jest tak, rejestrujesz sobie okno.
Tam ustawiasz wndProc jako procedurę obsługi wiadomości.

Teraz tak masz GetMessage i PeekMessage, ustawiasz jedno albo drugie w przy budowie event loop.
W piewszym przypadku jest blokujące aplikację do czasu przyjścia eventu np. wciśnięcia klawisza, ale okno musi być aktywne inaczej nie będzie do niego tego typu wiadomość przychodziła.
PeekMessage za to nie blokuje jeśli nie ma wiadomości, można też dać parametr żeby usuwał wiadomości bo tylko zerka na Eventy nie usuwając ich z kolejki.

No i DispatchMessage to wysyłasz daną wiadomość do procedury obsługi eventów czyli tego wndProc zarejestrowanego przy tworzeniu okna, gdzie pod koniec można wysłać jeszcze dalej tą wiadomośc czy jakieś filtry wiadomości dodawać.
Coś jak w springu dostajesz requesta i robisz jakieś filtry na nim za nim trafią do danego miejsca.

To tak niskopoziomowo na winapi, ale C# ma obudowaną tą pętlę i w frameworkach do GUI gdzie wyklikujesz to ten framework za ciebie buduje tą pętlę, ale jak weźmiesz debugger to dalej to tam będzie wewnątrz binarki.
I C# ma async gdzie takiego braknie w C.
Javascript ma podobny event loop.

function wait(ms){
   var start = new Date().getTime();
   var end = start;
   while(end < start + ms) {
     end = new Date().getTime();
  }
  alert('end');
}

Teraz dodaj

<a href="javascript:wait(5000)">tests</a>

I dopóki nie minie 5 sekund nie będziesz mógł nic robić na stronie, bo funkcja nie jest asynchroniczna i zablokuje cały event loop.

Ogólnie działanie event driven jest bardzo popularne w wielu środowiskach.

0

W Visual Basic 6 działało to tak jak w konsoli :)

0

No tak, a niby jak miała by aplikacja wykryć, że kliknąłeś w button?
Jakby nie było pętli to program by się w pewnym momencie skończył.

To nie aplikacja miałaby wykrywać klikniecie poprzez odpytywanie lub czekanie na zdarzenie, tylko OS miałby zlecać uruchomienie funkcji podpiętej jako "onclick", o ile takowa jest podpięta, dokładnie tak samo, jak to OS maluje formatkę i przyciski na zlecenie aplikacji. Teoretycznie, moim zdaniem jest to możliwe, że OS maluje formatkę, przemalowuje ją po każdym jej ruchu i zarządza kliknięciami, a działałoby w dwóch wątkach, gdzie aplikacja, która zleciła namalowanie formatki jest w jednym wątku, a sama formatka w innym. Jak się kliknie przycisk podczas "zajętości" apki, to w zależności od implementacji i ustawień, zdarzenie byłoby zignorowane lub wykonane niezwłocznie, jak apka skończy obsługiwać bieżące zdarzenie. W konsoli tak właśnie się dzieje.

Z tego wszystkiego wygląda na to, że aplikacja konsolowa i okienkowa działa dokładnie tak samo, ale z pewną różnicą:

Aplikacja konsola ma polecenie "readKey" lub "getChar" (nazwa zależy od języka ale sens ten sam, a i tak każdy odczyt naciśnięcia klawisza jest tak naprawdę odczytem ze strumienia wejścia z ewentualną zmianą odczytanej informacji na "przyjazną" informację identyfikująca naciśnięty klawisz), który żąda jednego znaku z OS i czeka aż dostanie (polecenie "readLine" to jest tak naprawdę "readKey" w pętli, z której wychodzi jak dostanie bajt 0x0D lub 0x0A), taką pętlę zazwyczaj trzeba wyrazić wprost, a jeżeli aplikacja nie czeka na klawisz, to każdy naciśnięty klawisz będzie zachowany w buforze stdin i odczytany przy najbliższym "readKey", a tak naprawdę przy najbliższym odczycie ze strumienia wejścia. Oczywiście, może też być funkcja, która sprawdza, czy strumień wejścia jest pusty i odczytuje bajt tylko w przypadku, gdy nie jest pusty.

API okienkowe ma polecenie w stylu "getEvent", które żąda uzyskanie jednego zdarzenia od OS, a jak dostanie, to funkcja zwraca to zdarzenie z niezbędnymi parametrami (m. in. id formatki, rodzaj zdarzenia) i aplikacja wywołująca tą funkcję uruchamia metodę zależną od zdarzenia o ile ma ją w ogóle uruchomić. Jak apka czeka na zdarzenie od OS, to okno jest żywe, a jak apka coś robi i nie czeka na zdarzenie od OS, to okno jest zawieszone. Tego nigdy nie implementuje się wprost, ale kompilator sam wytworzy taki kod. Ewentualnie np. w czystym WinAPI należy to zaimplementować wprost. A jak aplikacja ma kilka formatek, to pętla i tak byłaby jedna i obsługiwała zdarzenia do wszystkich formatek.

Czy dobrze rozumiem? Czy można powiedzieć, ze faktycznie tak jest?

Teraz dodaj

<a href="javascript:wait(5000)">tests</a>

I dopóki nie minie 5 sekund nie będziesz mógł nic robić na stronie, bo funkcja nie jest asynchroniczna i zablokuje cały event loop.

Tak, to strona będzie zablokowana, a jak zrobię jeden Worker, który będzie mieć całą logikę, a każdy element w HTML będzie wysyłać wiadomość do Workera (czas samego wysłania wiadomości jest pomijalnie krótki), to uruchomienie długotrwałych zdarzeń nie będzie zawieszać przeglądarki. Będzie to identycznie, jak konsola, gdzie HTML i przeglądarka jest jak sam program obslugujący konsolę, a Worker jest jak właściwa aplikacja wykonująca logikę biznesową i to bez tworzenia wątku przy każdym zdarzeniu.

0

Jak ustawisz czytanie w innym wątku to inny wątek będzie zablokowany, a ten drugi będzie coś innego wykonywał, więc kernel zrobi task switching i przeskoczy sobie do innego zadania.

konsola jest zawieszona jak wczytujesz stdin, a nic tam nie ma, trzeba specjalnie ustawić parametry, żeby nie blokowała operacja czytania z stdin jeśli nie ma danych, wtedy wraca od razu funkcja i masz w return 0 znaków odczytanych, w ten sposób możesz stwierdzić, że nikt nie wprowadził żadnych znaków i robisz inne zadania, a później po jakimś czasie znowu sprawdzasz czy coś się pojawiło.

np. non blocking i/o z stdin możesz pod linuxem ustawić tak

fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);

Normalnie jak zrobisz read na stdin czyli 0, to zablokujesz program jak nie będzie danych do czasu aż te dane się pojawią, ale tak program będzie leciał sobie dalej jak nie będzie żadnych danych.

Co do implementacji tego w winform czy innych to zależy, bo to można różnie zrobić, thready, async, ale co do tego

andrzejlisek napisał(a):

Czy dobrze rozumiem? Czy można powiedzieć, ze faktycznie tak jest?

Jak chcesz się przekonać jak jest to musisz debuggerem sprawdzić jak kto ktoś zaprojektował, ewentualnie poczytać, np. C#, JS i Python często i gęsto z async korzystają, a pętla jest jedna i czasem jakieś osobne thready się zrobi, albo procesy.
Też zależy czy bardziej czekasz na dane czy chcesz przeprowadzić jakieś długie i kosztowne obliczenia.
Co technologia to może być inne rozwiązanie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1