Programmation multithread en C#
- Concepts fondamentaux
1.1 Processus
Un processus (Process) représente une unité d'exécution dans le système Windows. Il contient toutes les ressources nécessaires à l'exécution d'un programme. Chaque application en cours d'exécution est considérée comme un processus dans le système d'exploitation. Un processus peut inclure un ou plusieurs threads. Un thread est l'unité de base de分配tempsprocesseur par le système d'exploitation. À l'intérieur d'un processus, plusieurs threads peuvent exécuter du code simultanément. Les processus sont relativement indépendants les uns des autres.
1.2 Thread
Un thread (Thread) est l'unité d'exécution de base au sein d'un processus. Dans les applications .NET, le point d'entrée est la méthode Main(). Lorsqu'elle est appelée, le système crée automatiquement un thread principal. Un thread est principalement composé de registres CPU, d'une pile d'appels et du stockage local du thread (TLS - Thread Local Storage).
- Aventages du multithreading
Le multithreading offre plusieurs avantages :
- Exécution simultanée de plusieurs tâches
- Réactivité accrue de l'application
- Possibilité de déléguer les tâches chronophages
- Contrôle flexible sur l'arrêt des tâches
- Optimisation des performances via les priorités
Pourquoi le multithreading fonctionne-t-il ?
Deux raisons principales expliquent l'exécution multithread :
- Partage temporel : Le CPU fonctionne si rapidement que le système d'exploitation gère le temps via des tranches (time slices).宏观ment, les threads semblent s'exécuter simultanément.
- Architecture multi-cœurs : Les ordinateurs modernes ont plusieurs cœurs CPU, permettant une vraie exécution parallèle.
Inconvénients potentiels
- Consommation mémoire additionnelle
- Coût de coordination entre threads
- Problèmes de concurrence sur les ressources partagées
- Complexité de gestion
- La classe System.Threading.Thread
La classe Thread est,位于 System.Threading espace de noms. Elle permet de créer et gérer des threads.
3.1 Création de threads
Avec ThreadStart (sans paramètres)
using System;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread thread1 = new Thread(new ThreadStart(MaMethode));
thread1.Start();
Console.ReadKey();
}
static void MaMethode()
{
Console.WriteLine("Thread sans paramètre exécuté");
}
}
}
Avec une méthode d'instance
using System;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
var test = new MaClasse();
Thread thread = new Thread(new ThreadStart(test.MaMethode));
thread.Start();
Console.ReadKey();
}
}
class MaClasse
{
public void MaMethode()
{
Console.WriteLine("Méthode d'instance exécutée");
}
}
}
Avec expressions lambda et délégués anonymes
using System;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread thread1 = new Thread(delegate() {
Console.WriteLine("Thread via délégué anonyme");
});
thread1.Start();
Thread thread2 = new Thread(() =>
Console.WriteLine("Thread via expression lambda"));
thread2.Start();
Console.ReadKey();
}
}
}
Avec ParameterizedThreadStart (avec paramètres)
using System;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(new ParameterizedThreadStart(MethodeAvecParam));
thread.Start("Données transmises au thread");
Console.ReadKey();
}
static void MethodeAvecParam(object parametre)
{
Console.WriteLine("Paramètre reçu : " + parametre);
}
}
}
Important : Le paramètre doit être de type Object. Si on utilise un delegate sans paramètre, on ne peut pas appeler Start avec un argument.
3.2 Propriétés utiles des threads
| Propriété | Description |
|---|---|
| ManagedThreadId | Identifiant unique du thread |
| Name | Nom du thread |
| Priority | Priorité d'exécution |
| ThreadState | État actuel du thread |
| IsBackground | Indique si c'est un thread d'arrière-plan |
| IsAlive | Indique si le thread est en cours d'exécution |
| CurrentThread | Récupère le thread en cours |
3.3 Priorités de thread
La propriété Priority définit l'ordre d'exécution cuando les threads compétitionnent pour le CPU :
| Membre | Description |
|---|---|
| Lowest | Plus basse priorité |
| BelowNormal | Inférieure à la normale |
| Normal | Priorité par défaut |
| AboveNormal | Supérieure à la normale |
| Highest | Plus haute priorité |
3.4 Méthodes principales
| Méthode | Description |
|---|---|
| Start() | Démarre le thread |
| Sleep(int) | Suspend le thread pour une durée |
| Join() | Bloque jusqu'à la fin du thread |
| Abort() | Termine le thread |
| Suspend() | Suspend le thread |
| Resume() | Reprend un thread suspendu |
3.5 Exemple d'utilisation des propriétés
using System;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Thread threadActuel = Thread.CurrentThread;
threadActuel.Nom = "Thread Principal";
int id = threadActuel.ManagedThreadId;
ThreadState etat = threadActuel.ThreadState;
ThreadPriority priorite = threadActuel.Priority;
Console.WriteLine("ID: {0}", id);
Console.WriteLine("Nom: {0}", threadActuel.Nom);
Console.WriteLine("État: {0}", etat);
Console.WriteLine("Priorité: {0}", priorite);
Console.ReadKey();
}
}
}
- Threads de premier plan et d'arrière-plan
Threads de premier plan : L'application ne se termine que lorsque tous les threads de premier plan sont terminés.
Threads d'arrière-plan : L'application se termine dès que tous les threads de premier plan sont terminés, même si les threads d'arrière-plan sont encore en cours.
using System;
using System.Threading;
namespace ThreadDemo
{
class Program
{
static void Main(string[] args)
{
Donnees donnees1 = new Donnees(5);
Thread threadPremier = new Thread(new ThreadStart(donnees1.Executer));
threadPremier.Nom = "Thread Premier";
Donnees donnees2 = new Donnees(10);
Thread threadArriere = new Thread(new ThreadStart(donnees2.Executer));
threadArriere.Nom = "Thread Arrière-plan";
threadArriere.IsBackground = true;
threadPremier.Start();
threadArriere.Start();
Console.ReadKey();
}
}
class Donnees
{
private int _compteur;
public Donnees(int compteur)
{
_compteur = compteur;
}
public void Executer()
{
string nom = Thread.CurrentThread.Nom;
for (int i = 0; i < _compteur; i++)
{
Console.WriteLine("{0} - Compteur: {1}", nom, i);
Thread.Sleep(500);
}
Console.WriteLine("{0} terminé", nom);
}
}
}
- Synchronisation des threads
La synchronisation确保qu'à un instant donné, un seul thread peut accéder à une ressource partagée. Sans synchronisation, des erreurs peuvent survenir.
5.1 Le mot-clé lock
C# fournit le mot-clé lock pour synchroniser l'accès aux ressources :
lock(expression)
{
// Code protégé
}
5.2 Exemple : Gestion d'un stock de livres
Sans synchronisation (résultat incorrect)
using System;
using System.Threading;
namespace LibrairieDemo
{
class Program
{
static void Main(string[] args)
{
StockLivres stock = new StockLivres();
Thread t1 = new Thread(new ThreadStart(stock.Vendre));
Thread t2 = new Thread(new ThreadStart(stock.Vendre));
t1.Start();
t2.Start();
Console.ReadKey();
}
}
class StockLivres
{
public int quantite = 1;
public void Vendre()
{
int temporaire = quantite;
if (temporaire > 0)
{
Thread.Sleep(1000);
quantite -= 1;
Console.WriteLine("Livre vendu. Restant: {0}", quantite);
}
else
{
Console.WriteLine("Stock épuisé");
}
}
}
}
Avec synchronisation (résultat correct)
using System;
using System.Threading;
namespace LibrairieDemo
{
class Program
{
static void Main(string[] args)
{
StockLivres stock = new StockLivres();
Thread t1 = new Thread(new ThreadStart(stock.Vendre));
Thread t2 = new Thread(new ThreadStart(stock.Vendre));
t1.Start();
t2.Start();
Console.ReadKey();
}
}
class StockLivres
{
public int quantite = 1;
public void Vendre()
{
lock(this)
{
int temporaire = quantite;
if (temporaire > 0)
{
Thread.Sleep(1000);
quantite -= 1;
Console.WriteLine("Livre vendu. Restant: {0}", quantite);
}
else
{
Console.WriteLine("Stock épuisé");
}
}
}
}
}
- Accès cross-thread
En Windows Forms, les contrôles ne peuvent être modifiés que par le thread qui les a créés (thread UI). Un accès direct depuis un autre thread provoque une exception.
6.1 Solution 1 : Désactiver la vérification (non recommandé)
private void Form1_Load(object sender, EventArgs e)
{
Control.CheckForIllegalCrossThreadCalls = false;
}
Attention : Cette méthode n'est pas sécurisée et ne doit pas être utilisée en production.
6.2 Solution 2 : Utiliser les rappels (recommandé)
using System;
using System.Threading;
using System.Windows.Forms;
namespace AppWinForms
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private delegate void DelegueMiseAJour(int valeur);
private DelegueMiseAJour _rappel;
private void button1_Click(object sender, EventArgs e)
{
_rappel = new DelegueMiseAJour(MettreAJourTexte);
Thread thread = new Thread(new ThreadStart(TraiterDonnees));
thread.IsBackground = true;
thread.Start();
}
private void TraiterDonnees()
{
for (int i = 0; i < 10000; i++)
{
textBox1.Invoke(_rappel, i);
}
}
private void MettreAJourTexte(int valeur)
{
this.textBox1.Text = valeur.ToString();
}
}
}
- Synchronisation vs Asynchronisation
Synchronisation : Le code attend la fin de l'exécution avant de continuer.
Asynchronisation : Le code continue immédiatement sans attendre (non-bloquant).
Exemple comparatif
using System;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("=== Test Synchrone ===");
MethodeSynchrone();
Console.WriteLine("\n=== Test Asynchrone ===");
MethodeAsynchrone();
Console.ReadKey();
}
static void MethodeSynchrone()
{
Console.WriteLine("Début sync - Thread: " + Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 3; i++)
{
EffectuerTache("Sync_" + i);
}
Console.WriteLine("Fin sync - Thread: " + Thread.CurrentThread.ManagedThreadId);
}
static void MethodeAsynchrone()
{
Console.WriteLine("Début async - Thread: " + Thread.CurrentThread.ManagedThreadId);
Action<string> action = EffectuerTache;
for (int i = 0; i < 3; i++)
{
action.BeginInvoke("Async_" + i, null, null);
}
Console.WriteLine("Fin async - Thread: " + Thread.CurrentThread.ManagedThreadId);
}
static void EffectuerTache(string nom)
{
Console.WriteLine("Début - {0} - Thread: {1}", nom, Thread.CurrentThread.ManagedThreadId);
long resultat = 0;
for (int i = 0; i < 100000000; i++)
{
resultat += i;
}
Console.WriteLine("Fin - {0} - Thread: {1}", nom, Thread.CurrentThread.ManagedThreadId);
}
}
}
Différences clés
- Interface utilisateur : Synchrone bloque l'UI, asynchrone non
- Performance : Asynchrone plus rapide grâce au parallélisme
- Ordre d'exécution : Synchrone séquentiel, asynchrone non déterministe
- Rappels (Callbacks)
Les rappels permettent d'exécuter du code une fois une opération asynchrone terminée, résolvant le problème d'ordre d'exécution.
using System;
using System.Threading;
namespace CallbackDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Démarrage - Thread: " + Thread.CurrentThread.ManagedThreadId);
Action<string> action = MethodeLongue;
AsyncCallback rappel = resultat =>
{
Console.WriteLine("Traitement terminé - Thread: " + Thread.CurrentThread.ManagedThreadId);
};
action.BeginInvoke("Données", rappel, null);
Console.WriteLine("Après BeginInvoke - Thread: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
static void MethodeLongue(string donnees)
{
Thread.Sleep(2000);
Console.WriteLine("Méthode exécutée: " + donnees);
}
}
}
Récupérer le résultat d'un appel asynchrone
using System;
using System.Threading;
namespace AsyncResultat
{
class Program
{
static void Main(string[] args)
{
Func<int> fonction = () =>
{
Thread.Sleep(2000);
return DateTime.Now.Day;
};
// Appel synchrone
Console.WriteLine("Résultat sync: " + fonction.Invoke());
// Appel asynchrone avec résultat
IAsyncResult resultatAsync = fonction.BeginInvoke(null, "mon état");
int valeur = fonction.EndInvoke(resultatAsync);
Console.WriteLine("Résultat async: " + valeur);
Console.ReadKey();
}
}
}
Conclusion
Le multithreading en C# est un outil puissant pour créer des applications performantes et réactives. Il nécessite cependant une bonne compréhension des mécanismes de synchronisation pour éviter les problèmes de concurrence. Les mots-clés lock, les délégués et les rappels constituent les fondamentaux de la programmation multithreadée.