CodeGym /Cours /C# SELF /Optimisations avec ReaderW...

Optimisations avec ReaderWriterLockSlim

C# SELF
Niveau 56 , Leçon 4
Disponible

1. Pourquoi le lock classique peut être insuffisant ?

Réfléchissons : on a une ressource partagée — par exemple, une liste de clients qu'on stocke quelque part dans notre application multithread. La plupart du temps on ne fait que lire cette liste — afficher les données à l'utilisateur, générer des rapports, filtrer et trier l'info. Seuls de temps en temps quelqu'un ajoute un client ou en supprime un.

Si on utilise le lock habituel, tout thread qui veut lire doit attendre que l'autre thread ait fini sa lecture ou, pire, son écriture. Et si 99% des opérations sont des lectures ? On freine inutilement tous les lecteurs alors qu'ils pourraient s'exécuter en parallèle.

C'est là qu'intervient un type spécial de verrou — le Reader-Writer Lock. Idée clé : plusieurs threads peuvent lire en même temps, mais si quelqu'un veut modifier, il obtient un accès exclusif qui bloque les lecteurs.

2. Théorie : qu'est-ce que ReaderWriterLockSlim

Bref sur les classes

  • ReaderWriterLock — plus ancien, lent, peut mener à des deadlocks. À utiliser seulement si vous devez supporter .NET Framework 2.0.
  • ReaderWriterLockSlim — alternative moderne et rapide (Slim : «maigre», «allégé»). Cette classe est optimisée pour des opérations fréquentes de lecture/écriture concurrentes et est recommandée quand il y a beaucoup de threads et que la lecture est beaucoup plus fréquente que l'écriture.

Principe de fonctionnement

ReaderWriterLockSlim implémente trois modes de verrouillage :

  • Read Lock (verrou de lecture) : plusieurs threads peuvent lire simultanément.
  • Write Lock (verrou d'écriture) : un seul thread peut modifier les données, et aucun autre ne lit.
  • Upgradeable Read Lock (verrou de lecture upgradable) : un seul thread peut détenir ce type de verrou ; il lit, mais peut, si besoin, «monter» en write lock.

Schéma de verrouillage

Thread-Lecteur Thread-Écrivain Thread avec Upgradeable Read Lock
Lecteur ✅/⛔
Écrivain
Upgradeable Read ✅*

✅ — autorisé

⛔ — bloqué

* — seulement si l'upgradeable read lock n'a pas été «monté» en write

Quand utiliser (et ne pas utiliser) ReaderWriterLockSlim

Recommandé :

  • Beaucoup de threads concurrents qui lisent majoritairement des données partagées (répertoires, cache, configuration).
  • Les écritures sont rares (par ex. une fois par seconde/minute/heure).
  • On veut optimiser la performance des lectures sans gêner les threads lecteurs.

À éviter :

  • Lecture/écriture en proportions égales (mieux vaut un lock classique).
  • Les données changent souvent (le gain est minime).

3. Utiliser ReaderWriterLockSlim en pratique

Allongeons notre appli didactique ! On avait un répertoire simple de clients et montré l'accès multithread à la collection. Maintenant compliquons : imaginons une liste de clients consultée par plusieurs threads (un service notifie tous les clients d'une promo, un autre ajoute des clients).

Décrivons d'abord la collection de clients :


// Classe Client — le héros familier des leçons précédentes
public class Client
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Maintenant créons un wrapper pour l'accès thread-safe via ReaderWriterLockSlim :

using System;
using System.Collections.Generic;
using System.Threading;

public class ClientDirectory
{
    private readonly List<Client> _clients = new List<Client>();
    // Slim-lock pour lecture/écriture
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

    // Ajout d'un client (write lock)
    public void AddClient(Client client)
    {
        _lock.EnterWriteLock(); // Un seul thread peut ajouter un client
        try
        {
            _clients.Add(client);
            Console.WriteLine($"[Thread {Thread.CurrentThread.ManagedThreadId}] Client ajouté {client.Name}");
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    // Obtention d'une copie de la liste des clients (read lock)
    public List<Client> GetClients()
    {
        _lock.EnterReadLock(); // Plusieurs threads peuvent lire en même temps
        try
        {
            // On retourne une copie pour éviter qu'on modifie par erreur l'original
            return new List<Client>(_clients);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }
}

4. Lecture multithread et écritures périodiques

Imaginons 5 threads qui lisent périodiquement tous les clients (génèrent des rapports ou vérifient qui est là) et un thread séparé qui ajoute parfois un client.

using System.Threading;

class Program
{
    static void Main()
    {
        var directory = new ClientDirectory();

        // Thread pour ajouter des clients
        var writerThread = new Thread(() =>
        {
            for (int i = 1; i <= 5; i++)
            {
                directory.AddClient(new Client { Id = i, Name = $"Client {i}" });
                Thread.Sleep(700); // Simulation d'opérations longues
            }
        });

        // Plusieurs lecteurs
        for (int j = 0; j < 5; j++)
        {
            int readerId = j + 1;
            new Thread(() =>
            {
                for (int k = 0; k < 10; k++)
                {
                    var clients = directory.GetClients();
                    Console.WriteLine($"[Lecteur {readerId}][Thread {Thread.CurrentThread.ManagedThreadId}] Nombre de clients : {clients.Count}");
                    Thread.Sleep(200); // Lecture plus fréquente que l'écriture
                }
            }).Start();
        }

        writerThread.Start();
        writerThread.Join();

        // Laisser le temps aux lecteurs de finir :
        Thread.Sleep(3000);
    }
}

Presque toujours plusieurs threads peuvent lire la liste simultanément (sans attendre les uns les autres), mais quand un thread ajoute un client — les autres doivent attendre la fin de l'écriture. Ça permet de servir beaucoup de lecteurs sans blocages inutiles.

5. Verrou upgradable : UpgradeableReadLock

Parfois un thread lit les données et seulement si une condition est remplie il veut les modifier. On pourrait obtenir un read lock, vérifier, relâcher puis obtenir un write lock. Mais pendant l'intervalle, un autre thread aurait pu changer les données ! L'upgradeable lock aide ici.

Son usage est simple :

public bool AddClientIfNotExists(string name)
{
    _lock.EnterUpgradeableReadLock(); // Un seul thread peut détenir ce verrou
    try
    {
        bool exists = _clients.Exists(c => c.Name == name);
        if (!exists)
        {
            _lock.EnterWriteLock(); // On monte en write lock
            try
            {
                _clients.Add(new Client { Name = name });
                return true;
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        return false;
    }
    finally
    {
        _lock.ExitUpgradeableReadLock();
    }
}

D'abord le thread lit, et s'il doit modifier — il élève le niveau du verrou sans laisser passer d'autres threads.

6. Petites astuces utiles

Comparaison : ReaderWriterLockSlim vs lock

lock
ReaderWriterLockSlim
Lecture simultanée Non Oui
Écriture simultanée Non Non
Support du read upgradable Non Oui (EnterUpgradeableReadLock)
Vitesse Très rapide Plus rapide quand les lectures sont fréquentes
Mémoire Minimale Légèrement plus
Simplicité Facile Un peu plus complexe, avec des subtilités

À quoi ça ressemble dans des vrais projets

Dans de grosses applis, où il y a de grandes tables ou caches lus par des centaines de threads mais modifiés rarement, ReaderWriterLockSlim évite les goulets d'étranglement sur les lectures. Par exemple :

  • Cache de configuration consulté par des microservices.
  • Référentiel (lookup) pour des calculs financiers.
  • Cache interne pour le routage de messages.

Je souligne — dans la majorité des cas, surtout si vous débutez en multithreading, le lock classique est plus simple et plus sûr. ReaderWriterLockSlim est pour les cas où il y a vraiment beaucoup de lectures concurrentes.

Visualisation : schéma d'accès

flowchart TD
    A[Thread-Lecteur 1] --Read--> D[ReaderWriterLockSlim]
    B[Thread-Lecteur 2] --Read--> D
    C[Thread-Écrivain] --Write--> D
    D --Read autorisé--> E[Lecture de la collection partagée]
    D --Write bloque lecture/écriture--> F[Écriture de la collection partagée]

Tant qu'il n'y a que des lectures — l'accès est ouvert à tous ; dès qu'on tente une écriture — tout le monde attend la fin de l'écriture.

7. ReaderWriterLockSlim : subtilités et erreurs typiques

Lock Recursion (récursion de verrou) : Par défaut ReaderWriterLockSlim n'autorise pas qu'un même thread acquière plusieurs fois le même type de verrou. Si un thread a déjà un read lock et tente de le référencer de nouveau — une exception est levée. Même chose pour le write lock. En revanche, l'UpgradeableReadLock peut être monté en write lock depuis l'intérieur.

Ne mélangez pas l'ordre : Appelez EnterUpgradeableReadLock() → à l'intérieur EnterWriteLock(). Mais jamais l'inverse !

Exceptions : Si vous n'êtes pas sûr de relâcher le verrou (par ex. vous oubliez ExitWriteLock()), les autres threads attendront indéfiniment. Utilisez toujours try { ... } finally { ... }.

Ne gardez pas le verrou longtemps : N'effectuez pas d'opérations IO longues à l'intérieur d'un lock. Plus vous gardez la lock, plus vous retardez les autres threads. Sortez du verrou le plus vite possible.

Évaluez la complexité : N'utilisez pas ReaderWriterLockSlim pour protéger des petites opérations — là où un simple lock est plus simple et souvent plus rapide.

1
Étude/Quiz
Synchronisation des threads, niveau 56, leçon 4
Indisponible
Synchronisation des threads
Problème des ressources partagées
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION