Programmation multithread en C#

Programmation multithread en C#

  1. 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).

  1. 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 :

  1. 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.
  2. 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
  1. 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();
        }
    }
}

  1. 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);
        }
    }
}

  1. 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é");
                }
            }
        }
    }
}

  1. 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();
        }
    }
}

  1. 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
  1. 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.

Étiquettes: CSharp multithreading threading synchronisation async

Publié le 31 mai à 02h39