CodeGym /Kursy /C# SELF /Problem blokujących wywołań

Problem blokujących wywołań

C# SELF
Poziom 59 , Lekcja 0
Dostępny

1. Wprowadzenie

Wywołanie blokujące — to każde wywołanie, które „blokuje” wykonanie wątku dopóki jakaś operacja się nie zakończy. Zazwyczaj to:

  • Odczyt lub zapis danych (np. z dysku).
  • Długie zapytanie do bazy danych lub sieci.
  • Wywołanie biblioteki zewnętrznej, która działa „wolno”.

W takich momentach wątek po prostu stoi, przestępuje z nogi na nogę i czeka: „No co tam, kiedy wreszcie odpowiedź?”

Przykład z życia


// Twoja główna logika
Console.WriteLine("Czekam na odpowiedź od serwera...");
string response = CallServer(); // tu wszystko stanęło!
Console.WriteLine("Serwer odpowiedział: " + response);

Dopóki serwer nie odpowie, Twój wątek „zawisł” — i nie może robić nic więcej!

Jak to wygląda w aplikacjach

W różnych typach aplikacji objawia się to inaczej. W programie konsolowym wygląda to tak, jakby po prostu „zamarł” — kursor przestaje migać i nie widać reakcji. W GUI, np. w WinForms lub WPF, okno nagle przestaje odpowiadać, pojawia się znajome „not responding”, i użytkownik jest gotów przekląć zarówno Twój soft, jak i Twoje nazwisko. A w aplikacjach serwerowych sytuacja jest jeszcze gorsza: jeden zawieszony wątek to jeden mniej obsłużony użytkownik.

2. Blokujące wywołania w C# w praktyce

Zróbmy „na żywo” mały eksperyment, używając projektu edukacyjnego. Załóżmy, że mamy aplikację magicznej kuli. Może być taki fragment:


Console.WriteLine("Wpisz swoje pytanie do magicznej kuli:");
string question = Console.ReadLine(); // wywołanie blokujące: czekamy na wejście!
Console.WriteLine("Myślę nad odpowiedzią...");
Thread.Sleep(5000); // emulacja długiej pracy
Console.WriteLine("Odpowiedź: spróbuj jutro ponownie!");
  • Console.ReadLine() — czeka na wejście użytkownika tyle, ile potrzeba (blokując wątek).
  • Thread.Sleep(5000) — sztucznie robi pauzę, zamraża wątek.

Pytanie: Co złego, jeśli po prostu pracujemy z aplikacją konsolową?

Odpowiedź: W konsolowym programie to nie takie krytyczne — użytkownik sam czeka. Ale co jeśli użytkowników jest wielu? Jeśli to serwer, który dostaje od razu 1000 zapytań? Albo aplikacja GUI, gdzie użytkownik oczekuje reakcji interfejsu, a nie Twoich filozoficznych rozważań?

Przykład: Blokowanie UI


// W WinForms handler przycisku:
private void button1_Click(object sender, EventArgs e)
{
    label1.Text = "Ładuję...";
    DoHeavyWork(); // ciężka operacja (np. zapytanie do DB)
    label1.Text = "Gotowe!";
}

Problem: Dopóki działa DoHeavyWork, okno nie odświeża interfejsu, nie reaguje na klawiaturę i mysz, nie przerysowuje się. Jeśli użytkownik spróbuje zamknąć okno — Windows pokaże "nie odpowiada" i zaproponuje zakończenie zadania.

3. Główny ból wielowątkowego świata

Problemy wywołań blokujących

  • Zmarnowane zasoby. Każdy wątek (Thread) w .NET to dość „ciężki” obiekt. OS przydziela dla niego pamięć na stos (zwykle 1 MB), deskryptory, synchronizację itd. Jeśli wątek „bezczynnie” czeka (na plik lub sieć), to zajmuje zasoby bez sensu („nic nie robiąc, je pamięć”).
  • Ograniczona liczba wątków. W aplikacjach serwerowych zwykle jest pool (pul) wątków. Jeśli wszystkie wątki się zablokują — nowe zapytania nie będą obsłużone. Na przykład w ASP.NET: jeśli wszystkie dostępne wątki czekają na bazę, to serwis „zamiera” dla nowych użytkowników.
  • Słaba responsywność interfejsu. W GUI okno przestaje reagować nawet na „zamknij”, kiedy UI-wątek jest zablokowany.
  • Spadek wydajności. Im więcej wątków się blokuje, tym więcej przełączeń kontekstu, obciążenie OS, i tym wolniej działa cały komputer.

Typowe źródła wywołań blokujących

Sprawdźmy, które wywołania mogą „na płaskim miejscu” zablokować wątek:

  • Operacje sieciowe: odwołania do API, pobieranie plików, aktualizacji.
  • Dysk/system plików: odczyt lub zapis dużych plików.
  • Bazy danych: długie zapytanie do SQL Server lub nawet lokalnej bazy.
  • Wejście użytkownika: długie ReadLine — drobiazg, ale też blokuje.
  • Thread.Sleep, Task.Delay: sztucznie „zamrażają” wątek.
Przykład (pobieranie danych ze strony)

using System.Net.Http;

HttpClient client = new HttpClient();
string result = client.GetStringAsync("https://google.com").Result; // blokujące wywołanie synchroniczne!
Console.WriteLine(result);

Co tu się dzieje? Metoda .Result blokuje wątek do otrzymania odpowiedzi!

4. Jak rozpoznać, że Twoje wywołanie blokuje wątek?

To proste: jeśli metoda nie zwraca sterowania przed zakończeniem swojej pracy (oczekiwanie, wejście, sieć, dysk) — to wywołanie blokujące.

  • Metody z Thread.Sleep, Task.Wait, .Result, .Wait() — blokują.
  • Metody, w których pojawia się „odczyt/zapis danych” bez asynchroniczności — też.
  • „Zawieszone” okno lub zamroczony serwer — częsty znak.

Wizualny element: Schemat blokującego wywołania


+-------------------+
| Uruchomienie metody|
+-------------------+
         |
         v
+-------------------+
| Wywołanie metody  |
| blokującej (np.   |
| odczyt pliku)     |
+-------------------+
         |
         v
+-------------------+
| Czekamy na zakończenie |
| operacji             |
+-------------------+
         |
         v
+-------------------+
| Zwracamy się z     |
| metody i idziemy   |
| dalej              |
+-------------------+

5. Dlaczego blokujące wywołania są złe na serwerach

Większość nowoczesnych aplikacji to aplikacje sieciowe lub serwerowe. Nawet jeśli piszesz grę, to są tam ładowania z serwera. Jeśli robisz stronę WWW, na pewno będzie praca z siecią i dyskiem.

Wyobraź sobie, że Twój serwer obsługuje 1000 zapytań na sekundę. Każde zapytanie musi pogadać z bazą. Piszesz:


// Web-handler
string data = db.ReadDataSync(); // synchroniczne blokowanie!
return new Response(data);

Dopóki trwa zapytanie do bazy, wątek po prostu nudzi się i czeka. Jedno zapytanie — okej. Ale gdy robi się ich masa, wszystkie wątki zostają zablokowane. Nowe już nie wchodzą i stoją w kolejce. W efekcie serwer przestaje być maszyną, a staje się długą kolejką, gdzie wszyscy stoją i czekają, aż coś się ruszy.

Ilustracja: "Synchroniczna obsługa zapytań"


Zapytanie 1: zajęło wątek -> czeka na bazę -> zwolniło się
Zapytanie 2: zajęło wątek -> czeka na bazę -> zwolniło się
...
Zapytanie 100: brak wolnych wątków, czeka w kolejce...

6. Asynchroniczność — lekarstwo na wywołania blokujące

Aby pokonać blokadę, używa się wywołań asynchronicznych. W C# to magiczne słowa: async i await.

Pozwalają one:

  • Nie blokować wątku (szczególnie cennego wątku UI lub serwerowego).
  • Nie zajmować zasobów bez sensu.
  • Poprawić responsywność GUI.
  • Nie „trzymać” wątku podczas trwania powolnej operacji.

Zwróć uwagę: nie zaczynamy od razu wszystkiego „asynchronicznie”, bo to wymaga zrozumienia wątków i blokad. Teraz, kiedy zrozumieliście mękę od wywołań blokujących, asynchroniczne podejście wyda się prawdziwą radością dla mózgu i użytkownika.

Tabela: Co blokuje, a co nie?

Metoda/Wywołanie Blokuje wątek Nadaje się do UI/Server
Console.ReadLine()
Tak Nie
Thread.Sleep(1000)
Tak Nie
File.ReadAllText()
Tak Nie
Task.Delay(1000).Wait()
Tak Nie
HttpClient.GetStringAsync().Result
Tak Nie
await File.ReadAllTextAsync()
Nie Tak
await Task.Delay(1000)
Nie Tak
await HttpClient.GetStringAsync()
Nie Tak

NB: await działa tylko w funkcji asynchronicznej (async Task ...), którą omówimy szczegółowo w kolejnych wykładach.

7. Legendarne błędy przy pracy z wywołaniami blokującymi

„Blokuję UI — użytkownik przeklina wszystko”


private void btnLoad_Click(object sender, EventArgs e)
{
    var data = BigFileReader.Read(@"C:\huge.dat"); // wywołanie blokujące!
    textBox1.Text = data;
}

Dopóki nie odczyta się ogromnego pliku — okno „zawisło”.

„Zawieszenie serwera — klienci uciekają”


public IActionResult Download()
{
    var content = File.ReadAllBytes("bigfile.zip"); // synchronicznie, długo, blokuje wątek ASP.NET!
    return File(content, "application/zip");
}

Małe serwery (np. na darmowym hostingu) mogą po prostu „upaść” przy takim podejściu.

„Metoda asynchroniczna + .Wait()/.Result = synchroniczna śmierć”


public void LoadData()
{
    var result = DoAsyncWork().Result; // Blokuje wątek!
}

.Result i .Wait() zmieniają nawet ładny asynchroniczny kod w zwykłe wywołanie blokujące.

2
Zadanie
C# SELF, poziom 59, lekcja 0
Niedostępne
Emulacja blokującego wywołania przez pauzę
Emulacja blokującego wywołania przez pauzę
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION