L’accès concurrent aux ressources partagées est un soucis que rencontre les développeurs au quotidien.
C# propose de nombreux outils pour gérer ce genre de problème. Le plus utilisé est la primitive lock que l’on retrouve souvent dans de nombreuses sources. Mais il en existe d’autres, par exemple, les sémaphores et les mutex. Dans cet article nous parlerons de ces derniers.

Introduction

Le mutex empêche l’accès simultanné à une ressource par plusieurs threads ou processus. Pour pouvoir accéder à la ressource partagée, chaque thread doit d’abord l’acquérir. Un seul thread pouvant l’acquérir à la fois, lui seul pourra accéder à la ressource. Si un autre thread tente de l’acquérir, il devra attendre que celui ci soit libéré par son actuel propriétaire.

Création d’un Mutex

var myMutex = new Mutex();

Lorsqu’un thread veut en devenir propriétaire, il appelle:

myMutex.WaitOne();

Le code qui suit cette ligne ne s’exécutera que lorsque notre thread pourra être propriétaire du mutex. Si celui-ci est acquis par un autre thread, l’exécution de notre thread sera bloquée à cette ligne tant que le mutex ne sera pas libéré.

Lorsque notre exécution continue, nous sommes sûr que notre mutex est libre. Après cette ligne, nous pouvons écrire le code que nous souhaitons protéger de l’accès concurrent.

Par exemple, incrémenter le solde d’un compte.

solde+=montant;

A la fin de cette section de code, il faut libérer le mutex pour qu’il puisse être utilisé par d’autres threads, avec:

myMutex.ReleaseMutex();

Voyons l’exemple suivant, nous souhaitons lancer des threads qui écrivent dans le même fichier mais éviter que les threads accèdent au fichier en même temps.

On crée notre mutex.

static Mutex myMutex = new Mutex();

Dans notre main, on crée des threads qui écriront dans le fichier:

static void Main()
{
    string filePath = "log.txt";

    // On nettoie le fichier pour l'exemple
    if (File.Exists(filePath))
        File.Delete(filePath);

    for (int i = 1; i <= 5; i++)
    {
        int threadId = i;
        Thread t = new Thread(() => WriteToFile(threadId));
        t.Start();
    }
}

La méthode WriteToFile écrit dans le fichier:

static void WriteToFile(int threadId)
{
        for (int i = 0; i < 3; i++)
        {
            myMutex.WaitOne(); // Accès exclusif au fichier
            try
            {
                using (StreamWriter sw = new StreamWriter(filePath, append: true))
                {
                    string line = $"Thread {threadId} écrit la ligne {i + 1} à {DateTime.Now:dd/MM/yyyy HH:mm:ss.fff}";
                    sw.WriteLine(line);
                    Console.WriteLine(line);
                }
            }
            finally
            {
                myMutex.ReleaseMutex(); // on le libère, les autres threads peuvent l'utiliser
            }

            Thread.Sleep(500);
        }
    }

Notre code ci-dessus crée 5 threads qui écrivent 3 lignes dans un fichier. Avant d’écrire dans le fichier chaque thread tente d’acquérir le mutex. Une fois qu’il l’a, il va écrire une ligne dans le fichier puis le libérer. Un autre thread peut alors le récupérer.

Si on supprime l’utilisation du mutex, on commentant les lignes

myMutex.WaitOne();

et

myMutex.ReleaseMutex();

on aura l’exception System.IO.IOException – The process cannot access the file…because it is being used by another process.

Si un thread se termine sans libérer un mutex, le prochain thread qui tentera de l’acquérir lèvera un AbandonedMutexException.

Mutex système nommés

Les mutex que nous venons de voir permettent de gérer l’accès concurrent de plusieurs threads appartenant au même programme. Mais avec les mutex systèmes nommés, plusieurs programmes peuvent accéder au même mutex identifié par son nom. On pourrait ainsi gérer l’accès concurrent de plusieurs exécutables à une même ressource. Voyons comment ça marche.

Création:

var myMutex = new Mutex(initiallyOwned: false, "mutexName");

Tous les mutex du système qui ont ce nom font référence à ce mutex.

La convention veut que les noms des mutex globaux soient préfixés par Global\. Exemple : Global\monMutex.

Modifions notre programme précédent et utilisons des mutex nommés.

La méthode main devient

 static void Main()
{
      Thread t = new Thread(() => WriteToFile(Environment.ProcessId));
      t.Start();
}

et la méthode WriteToFile

static void WriteToFile(int threadId)
{
    for (int i = 0; i < 100; i++)
    {
        myMutex.WaitOne(); // Accès exclusif au fichier
        try
        {
            using (StreamWriter sw = new StreamWriter(filePath, append: true))
            {
                string line = $"Thread {threadId} écrit la ligne {i + 1} à {DateTime.Now:dd/MM/yyyy HH:mm:ss.fff}";
                sw.WriteLine(line);
                Console.WriteLine(line);
            }
        }
        finally
        {
            myMutex.ReleaseMutex();
        }

        Thread.Sleep(500); // Simule un traitement entre les écritures
    }
}

L’exécution de ce programme crée un thread qui écrit 100 lignes dans un fichier. Avant d’écrire une ligne dans le fichier, il tente d’acquérir le mutex. Une fois qu’il l’a obtenu, il écrit une ligne dans le fichier puis le libère pour que qu’un autre thread puisse le récupérer.

Nous allons lancer simultanément plusieurs instances de cet exécutable pour confirmer qu’elles peuvent toutes écrire dans le même fichier sans erreur grâce au mutex nommé.

exécution mutex nommé

Notre fichier sera rempli sans exception, car chaque process va acquérir le mutex avant d’écrire dans le fichier.

Conclusion

En résumé, les mutex en C# assurent qu’une ressource n’est utilisée que par un seul thread à la fois. Leur utilisation doit être soigneusement considérée pour éviter les problèmes de blocage ou de performances.

Auteur : Daniel MINKO FASSINOU

Laisser un commentaire




Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.