Programmation Multithread en C# : Concepts et Implémentations

Concepts fondamentaux du multithreading

Pour maîtriser la programmation multithread, il faut d'abord distinguer processus et thread. Un processus représente une instance d'application avec son propre espace mémoire virtuel, isolant les ressources entre instances. Les pools de processus optimisent la création et la réutilisation des processus pour des tâches concurrentes.

Un thread est un flux d'exécution au sein d'un processus, partageant le code mais possédant ses propres registres (compteur de programme, pointeur de pile). Les pools de threads évitent la surcharge liée à la création/destruction fréquente des threads, améliorant la performance et la localité du cache.

Les coroutines, dites "légers threads", offrent une gestion de concurrence hors du système d'exploitation, permettant une exécution concurrente sur un seul thread.

Avantages du multithreading : utilisation accrue du processeur. Inconvénients : consommaiton mémoire élevée, risques de compétition pour les ressources partagées nécessitant des synchronisations.

Gestion des threads avec Thread et ThreadPool

La classe Thread permet de créer et contrôler des threads. Le ThreadPool réutilise des threads pour réduire les frais de gestion. Exemple d'utilisation :

private void LancerTacheThread()
{
    Thread threadSecondaire = new Thread(EffectuerCuisson);
    threadSecondaire.IsBackground = true;
    threadSecondaire.Priority = ThreadPriority.Lowest;
    threadSecondaire.Name = "Cuisinier";
    threadSecondaire.Start();
    threadSecondaire.Join(); // Attend la fin du thread secondaire
}

static void TelechargerFichier(object etat)
{
    Trace.WriteLine("Début du téléchargement... ID thread: " + Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1500);
    Trace.WriteLine("Téléchargement terminé.");
}

private void EffectuerCuisson()
{
    Thread.Sleep(2000);
    Trace.WriteLine("Légumes prêts.");
    Thread.Sleep(4000);
    Trace.WriteLine("Plat principal terminé.");
}

private void DemarrerDepuisPool()
{
    ThreadPool.QueueUserWorkItem(TelechargerFichier);
    ThreadPool.QueueUserWorkItem(etat =>
    {
        Trace.WriteLine("Exécuté dans un thread du pool: " + Thread.CurrentThread.IsThreadPoolThread);
        Trace.WriteLine("ID thread: " + Thread.CurrentThread.ManagedThreadId);
    });
    Thread.Sleep(5000);
    Trace.WriteLine("Tâche principale terminée.");
}

Programmation avec Task et TaskFactory

Task simplifie la gestion des tâches parallèles, offrant un contrôle intuitif sur l'exécution asynchrone. Le modèle TAP (Task-based Asynchronous Pattern) est recommandé pour de nouvelles applications. Exemple avec annulation :

private void ExecuterAvecTask()
{
    var sourceAnnulation = new CancellationTokenSource();
    var tacheCalcul = new Task<int>(() => CalculerValeur("CalculA", 8, sourceAnnulation.Token));
    tacheCalcul.Start();
    Thread.Sleep(2000);

    if (MessageBox.Show("Annuler la tâche ?", "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes)
    {
        sourceAnnulation.Cancel();
    }

    Trace.WriteLine("Résultat: " + tacheCalcul.Result);
    Trace.WriteLine("Statut: " + tacheCalcul.Status.ToString());
}

static int CalculerValeur(string nom, int iterations, CancellationToken jetonAnnulation)
{
    Trace.WriteLine(string.Format("Tâche {0} en cours, thread ID: {1}", nom, Thread.CurrentThread.ManagedThreadId));
    for (int i = 0; i < iterations; i++)
    {
        Thread.Sleep(1000);
        if (jetonAnnulation.IsCancellationRequested)
        {
            Trace.WriteLine("Annulation demandée.");
            return -1;
        }
    }
    return 42 * iterations;
}

private void CollecterResultatsTaches()
{
    var listeTaches = new List<Task<string>>
    {
        Task.Factory.StartNew(() => "RésultatA"),
        Task.Factory.StartNew(() => "RésultatB"),
        Task.Factory.StartNew(() => "RésultatC")
    };

    foreach (var tache in listeTaches)
    {
        Trace.WriteLine(tache.Result);
    }
}</int>

Gestion des exceptions dans les threads et tâches

Pour les threads créés via Thread, les exceptions doivent être capturées dans la méthode cible. Avec Task, on peut attraper AggregateException lors de l'appel à Wait ou Result :

private void GererExceptionsThread()
{
    Thread filExecution = new Thread(TravaillerAvecRisque);
    filExecution.Start();
}

private void TravaillerAvecRisque()
{
    try
    {
        throw new InvalidOperationException("Erreur dans le thread secondaire");
    }
    catch (Exception ex)
    {
        Trace.Assert(false, ex.Message);
    }
}

private void CapturerExceptionTask()
{
    try
    {
        Task tache = Task.Run(() =>
        {
            throw new TimeoutException("Délai dépassé");
        });
        tache.Wait();
    }
    catch (AggregateException exAgg)
    {
        foreach (var ex in exAgg.InnerExceptions)
        {
            Trace.WriteLine("Exception capturée: " + ex.Message);
        }
    }
}

Synchronicité versus Asynchronicité

Du point de vue client, la synchronicité implique d'attendre une réponse avant de continuer, tandis que l'asynchronicité permet de soumettre d'autres requêtes. Côté serveur, le blocage signifie qu'un seul traitement à la fois est possible, contrairement au non-blocage qui accepte plusieurs requêtes simultanées.

En pratique, les méthodes synchrones bloquent l'exécution jusqu'à complétion, tandis que les méthodes asynchrones retournent immédiatement, notifiant l'appelant via des callbacks ou des états.

Modèles asynchrones dans .NET

Les modèles APM (Asynchronous Programming Model) et EAP (Event-based Asynchronous Pattern) sont historiques. Le TAP, basé sur Task, est privilégié dans .NET 4.0+ pour sa simplicité. Exemple d'implémentation TAP :

public delegate string MethodeAsync(int duree, out int idThread);

internal class ApplicationDemo
{
    static void Main(string[] arguments)
    {
        MethodeAsync appelant = new MethodeAsync(EffectuerOperationAsync);
        int idThread = 0;
        Task<string> travail = Task.Run(() => appelant.Invoke(1200, out idThread));

        for (int i = 0; i < 5; i++)
        {
            Thread.Sleep(800);
            Trace.WriteLine("Traitement annexe " + i.ToString());
        }

        string resultat = travail.Result;
        Trace.WriteLine("Opération terminée: " + resultat);
    }

    static string EffectuerOperationAsync(int duree, out int idThread)
    {
        Stopwatch chrono = new Stopwatch();
        chrono.Start();
        Trace.WriteLine("Début de l'opération asynchrone");
        for (int i = 0; i < 3; i++)
        {
            Thread.Sleep(duree);
            Trace.WriteLine("Étape " + i.ToString());
        }
        chrono.Stop();
        idThread = Thread.CurrentThread.ManagedThreadId;
        return string.Format("Durée: {0}ms", chrono.ElapsedMilliseconds);
    }
}

Étiquettes: CSharp multithreading Task Thread Asynchronous

Publié le 10 juin à 23h37