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 |
|---|---|---|
|
Tak | Nie |
|
Tak | Nie |
|
Tak | Nie |
|
Tak | Nie |
|
Tak | Nie |
|
Nie | Tak |
|
Nie | Tak |
|
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.
GO TO FULL VERSION