CodeGym /Cours /C# SELF /Problème des appels bloquants

Problème des appels bloquants

C# SELF
Niveau 59, Leçon 0
Disponible

1. Introduction

Appel bloquant — c'est tout appel qui «bloque» l'exécution du thread tant qu'une opération n'est pas terminée. En général, il s'agit de :

  • Lecture ou écriture de données (par exemple, sur le disque).
  • Requête longue à la base de données ou au réseau.
  • Appel à une bibliothèque tierce qui est «lente».

Dans ces moments le thread reste planté, piétine et attend : «Alors, quoi de neuf, on a une réponse ?»

Exemple de la vie courante


// Votre logique principale
Console.WriteLine("J'attends la réponse du serveur...");
string response = CallServer(); // ici tout est bloqué !
Console.WriteLine("Le serveur a répondu : " + response);

Tant que le serveur ne répond pas, votre thread est «planté» — il ne peut rien faire d'autre !

À quoi ça ressemble dans les applications

Dans différents types d'applications ça se manifeste différemment. Dans une application console on dirait qu'elle est juste «gelée» — le curseur arrête de clignoter et il n'y a aucune réaction. Dans une interface graphique, par exemple WinForms ou WPF, la fenêtre cesse de répondre, le fameux «not responding» apparaît, et l'utilisateur est prêt à maudire votre logiciel et votre nom de famille. Et dans les applications serveurs la situation est encore pire : un thread bloqué signifie un utilisateur de moins servi.

2. Appels bloquants en C# en pratique

Explorons la situation manuellement avec un projet didactique. Supposons que nous avons une application de boule magique. On peut avoir, par exemple, ce fragment :


Console.WriteLine("Entrez votre question pour la boule magique :");
string question = Console.ReadLine(); // appel bloquant : on attend la saisie !
Console.WriteLine("Je réfléchis...");
Thread.Sleep(5000); // émulation d'un travail long
Console.WriteLine("Réponse : essayez de nouveau demain !");
  • Console.ReadLine() — attend la saisie de l'utilisateur autant qu'il le faudra (bloque le thread).
  • Thread.Sleep(5000) — met artificiellement en pause, gèle le thread.

Question : Qu'y a-t-il de mal si on travaille juste avec une application console ?

Réponse : Dans une application console ce n'est pas si critique — votre utilisateur attend. Mais si l'utilisateur n'est pas seul ? Et si c'est un serveur qui reçoit immédiatement 1000 requêtes ? Ou une appli GUI où l'utilisateur attend une réaction de l'interface, pas vos considérations philosophiques ?

Exemple : blocage de l'UI


// Dans WinForms, gestionnaire du bouton :
private void button1_Click(object sender, EventArgs e)
{
    label1.Text = "Chargement...";
    DoHeavyWork(); // opération lourde (par exemple, requête DB)
    label1.Text = "Prêt !";
}

Problème : Tant que DoHeavyWork s'exécute, la fenêtre n'actualise pas l'interface, ne réagit pas au clavier ni à la souris, ne se redessine pas. Si l'utilisateur tente de fermer la fenêtre — Windows affichera «ne répond pas» et proposera de terminer la tâche.

3. Le principal problème du monde multithread

Problèmes des appels bloquants

  • Gaspillage de ressources. Chaque thread (Thread) en .NET est un objet assez «lourd». Le système d'exploitation lui alloue de la mémoire pour la pile (souvent 1 MB), des descripteurs, des mécanismes de synchronisation, etc. Si un thread est «inactif» (attend un fichier ou le réseau), il fait un travail inutile («rien ne faisant, il mange de la mémoire»).
  • Limitation du nombre de threads. Dans les applications serveur il y a généralement un thread pool. Si tous les threads sont bloqués — les nouvelles requêtes ne sont pas traitées. Par exemple, en ASP.NET : si tous les threads disponibles attendent la base, le site «gèle» pour tous les nouveaux utilisateurs.
  • Mauvaise réactivité de l'interface. Dans les applis GUI la fenêtre cesse de répondre même à la commande «fermer» quand le thread UI est bloqué.
  • Baisse des performances. Plus de threads sont bloqués, plus de changements de contexte, plus de charge sur le système, plus lent devient l'ordinateur entier.

Sources typiques d'appels bloquants

Regardons quels appels peuvent «sans raison» bloquer un thread :

  • Opérations réseau : appel à une API, téléchargement de fichiers, récupération de mises à jour.
  • Disque/système de fichiers : lecture ou écriture de gros fichiers.
  • Bases de données : requête lourde à SQL Server ou même à une base locale.
  • Entrée utilisateur : un long ReadLine — petit détail mais bloque aussi.
  • Thread.Sleep, Task.Delay : «gèlent» artificiellement le thread.
Exemple (télécharger des données d'un site)

using System.Net.Http;

HttpClient client = new HttpClient();
string result = client.GetStringAsync("https://google.com").Result; // appel synchrone bloquant !
Console.WriteLine(result);

Que se passe-t-il ici ? La méthode .Result bloque le thread jusqu'à la réception de la réponse !

4. Comment savoir si votre appel bloque le thread ?

C'est simple : si la méthode ne rend pas la main avant la fin de son travail (attente, saisie, réseau, disque) — c'est un appel bloquant.

  • Méthodes avec Thread.Sleep, Task.Wait, .Result, .Wait() — bloquent.
  • Méthodes qui effectuent «lecture/écriture» sans asynchronisme — aussi.
  • Une fenêtre figée ou un serveur qui rame — signes courants.

Élément visuel : Organigramme d'un appel bloquant


+-------------------+
| Démarrage méthode |
+-------------------+
         |
         v
+-------------------+
| Appel d'une méthode|
| bloquante (par ex. |
| lecture de fichier)|
+-------------------+
         |
         v
+-------------------+
| On attend la fin  |
| de l'opération    |
+-------------------+
         |
         v
+-------------------+
| On retourne et on  |
| continue           |
+-------------------+

5. Pourquoi les appels bloquants sont mauvais sur les serveurs

La plupart des applications modernes sont réseau ou serveur. Même dans un jeu il y a du chargement depuis un serveur. Si vous faites un site web, il y aura forcément du réseau et du disque.

Imaginez que votre serveur traite 1000 requêtes par seconde. Chaque requête doit parler à la base. Vous écrivez :


// Handler web
string data = db.ReadDataSync(); // Blocage synchrone !
return new Response(data);

Pendant l'appel à la base, le thread s'ennuie et attend. Une requête — ok. Mais quand elles s'accumulent, tous les threads sont pris. Les nouvelles requêtes ne passent plus et font la queue. Le serveur devient une longue file d'attente où tout le monde attend.

Illustration : "Traitement synchrone des requêtes"


Requête 1 : prend un thread -> attend la base -> libéré
Requête 2 : prend un thread -> attend la base -> libéré
...
Requête 100 : pas de thread libre, attend en file...

6. L'asynchronisme — remède aux appels bloquants

Pour combattre le blocage on utilise les appels asynchrones. En C# ce sont les mots magiques : async et await.

Ils permettent de :

  • Ne pas bloquer le thread (surtout le thread UI ou serveur).
  • Ne pas occuper inutilement les ressources.
  • Améliorer la réactivité du GUI.
  • Ne pas «retenir» le thread pendant une opération lente.

Remarque : on ne saute pas directement sur l'asynchrone sans comprendre, parce que ça demande de bien saisir threads et blocages. Maintenant que vous avez vu les tourments causés par les appels bloquants, l'approche asynchrone vous paraîtra un vrai soulagement pour l'esprit et pour l'utilisateur.

Tableau : Ce qui bloque et ce qui ne bloque pas ?

Méthode/Appel Bloque le thread Convient pour UI/Server
Console.ReadLine()
Oui Non
Thread.Sleep(1000)
Oui Non
File.ReadAllText()
Oui Non
Task.Delay(1000).Wait()
Oui Non
HttpClient.GetStringAsync().Result
Oui Non
await File.ReadAllTextAsync()
Non Oui
await Task.Delay(1000)
Non Oui
await HttpClient.GetStringAsync()
Non Oui

NB : await ne fonctionne que dans une fonction asynchrone (async Task ...), qu'on détaillera dans les prochaines leçons.

7. Erreurs légendaires en travaillant avec des appels bloquants

«Je bloque l'UI — l'utilisateur me maudit»


private void btnLoad_Click(object sender, EventArgs e)
{
    var data = BigFileReader.Read(@"C:\huge.dat"); // appel bloquant !
    textBox1.Text = data;
}

Tant que le gros fichier n'est pas lu — la fenêtre «reste pendue».

«Serveur gelé — les clients partent»


public IActionResult Download()
{
    var content = File.ReadAllBytes("bigfile.zip"); // synchroniquement, long, bloque le thread ASP.NET !
    return File(content, "application/zip");
}

Les petits serveurs (par exemple sur un hébergement gratuit) peuvent simplement «tomber» avec cette approche.

«Méthode async + .Wait()/.Result = mort synchrone»


public void LoadData()
{
    var result = DoAsyncWork().Result; // Bloque le thread !
}

.Result et .Wait() transforment même un joli code asynchrone en un simple appel bloquant.

2
Mission
C# SELF, niveau 59, leçon 0
Bloqué
Émulation d'un appel bloquant via une pause
Émulation d'un appel bloquant via une pause
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION