CodeGym /Corsi /C# SELF /Ottimizzazioni con ReaderW...

Ottimizzazioni con ReaderWriterLockSlim

C# SELF
Livello 56 , Lezione 4
Disponibile

1. Perché il normale lock può non bastare?

Pensiamo insieme: abbiamo una risorsa condivisa — per esempio una lista di clienti che teniamo da qualche parte nella nostra applicazione multithread. La maggior parte del tempo leggiamo solo quella lista — per esempio mostriamo i dati all'interfaccia utente, costruiamo report, filtriamo e ordiniamo le informazioni. Solo di rado qualcuno aggiunge un nuovo cliente o ne rimuove uno.

Se usiamo il solito lock, qualsiasi thread che voglia leggere è costretto ad aspettare che un altro thread finisca di leggere o, peggio, scrivere. E se il 99% delle operazioni sono letture? Allora stiamo rallentando inutilmente tutti i lettori, quando potrebbero lavorare in parallelo.

Qui entra in gioco un tipo speciale di lock — il Reader-Writer Lock. L'idea chiave: molti thread possono leggere contemporaneamente, ma se qualcuno vuole modificare, prende accesso esclusivo e blocca i lettori.

2. Teoria: cos'è ReaderWriterLockSlim

Breve sui classi

  • ReaderWriterLock — più vecchio, lento, può portare a deadlock. Usalo solo se hai bisogno di supportare .NET Framework 2.0.
  • ReaderWriterLockSlim — alternativa moderna e veloce (la parola chiave è Slim: "snello", "alleggerito"). Questa classe è ottimizzata per operazioni di lettura/scrittura concorrenziali frequenti ed è raccomandata in applicazioni con molti thread dove si legge molto più di quanto si scriva.

Principio di funzionamento

ReaderWriterLockSlim implementa tre modalità di lock:

  • Read Lock (lock di lettura): molti thread possono leggere contemporaneamente.
  • Write Lock (lock di scrittura): solo un thread può modificare i dati, e nessun altro legge.
  • Upgradeable Read Lock (lock di lettura aggiornabile): solo un thread può tenere questo lock alla volta; legge ma, se necessario, può "escalare" a write lock.

Schema dei lock

Thread-Lettore Thread-Scrittore Thread con Upgradeable Read Lock
Lettore ✅/⛔
Scrittore
Upgradeable Read ✅*

✅ — permesso

⛔ — bloccato

* — solo se l'upgradeable read lock non è stato "escalato" a write

Quando usare (e quando non usare) ReaderWriterLockSlim

Consigliato:

  • Molti thread concorrenti che principalmente leggono dati condivisi (dizionari, cache, configurazioni).
  • Le scritture avvengono più raramente (per esempio, ogni secondo/minuto/ora).
  • È importante massimizzare le prestazioni in lettura senza bloccare i thread.

Da evitare:

  • Scritture/letture in proporzioni simili (meglio il solito lock).
  • I dati cambiano spesso (il beneficio è minimo).

3. Uso pratico di ReaderWriterLockSlim

Estendiamo la nostra applicazione didattica! In precedenza abbiamo costruito una semplice rubrica clienti e provato l'accesso multithread alla collezione. Ora complichiamo il caso: supponiamo di avere una lista di clienti a cui accedono diversi thread (per esempio, un servizio notifica tutti i clienti per una nuova promozione, mentre un altro thread aggiunge nuovi clienti).

Prima descriviamo la nostra collezione di clienti:


// Classe Client — il solito protagonista dalle lezioni precedenti
public class Client
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Ora creiamo un wrapper per l'accesso thread-safe usando ReaderWriterLockSlim:

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

public class ClientDirectory
{
    private readonly List<Client> _clients = new List<Client>();
    // Slim-lock per lettura/scrittura
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

    // Aggiunta cliente (write lock)
    public void AddClient(Client client)
    {
        _lock.EnterWriteLock(); // Solo un thread può aggiungere un cliente
        try
        {
            _clients.Add(client);
            Console.WriteLine($"[Thread {Thread.CurrentThread.ManagedThreadId}] Aggiunto cliente {client.Name}");
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    // Ottenere una copia della lista clienti (read lock)
    public List<Client> GetClients()
    {
        _lock.EnterReadLock(); // Molti thread possono leggere contemporaneamente
        try
        {
            // Ritorniamo una copia così qualcuno non prova a modificare per sbaglio l'originale
            return new List<Client>(_clients);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }
}

4. Letture multithread e scritture periodiche

Supponiamo ci siano 5 thread che periodicamente leggono tutti i clienti ("generano report" o semplicemente controllano chi c'è), e un thread separato che a volte aggiunge un nuovo cliente.

using System.Threading;

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

        // Thread per aggiungere clienti
        var writerThread = new Thread(() =>
        {
            for (int i = 1; i <= 5; i++)
            {
                directory.AddClient(new Client { Id = i, Name = $"Cliente {i}" });
                Thread.Sleep(700); // Simulazione di operazioni lunghe
            }
        });

        // Più lettori
        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($"[Lettore {readerId}][Thread {Thread.CurrentThread.ManagedThreadId}] Numero clienti: {clients.Count}");
                    Thread.Sleep(200); // Legge più spesso di quanto scrive
                }
            }).Start();
        }

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

        // Dai tempo ai lettori di finire il giro:
        Thread.Sleep(3000);
    }
}

Quasi sempre più thread possono leggere la lista clienti contemporaneamente (senza aspettare l'un l'altro), ma quando un thread aggiunge un cliente — gli altri devono aspettare che la scrittura finisca. Questo permette di servire molti lettori senza blocchi inutili.

5. Lock aggiornabile: UpgradeableReadLock

A volte succede che un thread legga i dati e solo occasionalmente, se una condizione è soddisfatta, voglia modificarli. Potrebbe sembrare sufficiente ottenere un read lock, controllare la condizione, poi rilasciare il read lock e prendere il write lock. Ma tra i due momenti un altro thread potrebbe aver cambiato i dati! Qui entra in gioco il lock aggiornabile.

Usarlo è semplice:

public bool AddClientIfNotExists(string name)
{
    _lock.EnterUpgradeableReadLock(); // Solo un thread può tenere questo lock
    try
    {
        bool exists = _clients.Exists(c => c.Name == name);
        if (!exists)
        {
            _lock.EnterWriteLock(); // Escaliamo a write lock
            try
            {
                _clients.Add(new Client { Name = name });
                return true;
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        return false;
    }
    finally
    {
        _lock.ExitUpgradeableReadLock();
    }
}

Prima il thread legge, e se deve modificare — esegue l'escalation del lock senza lasciare la lista agli altri thread.

6. Sfumature utili

Confronto: ReaderWriterLockSlim vs lock

lock
ReaderWriterLockSlim
Lettura concorrente No
Scrittura concorrente No No
Supporto per lettura aggiornabile No Sì (EnterUpgradeableReadLock)
Velocità Molto veloce Piu veloce con letture frequenti
Memoria Minima Un po' di più
Semplicità Facile Più complesso, ci sono sottigliezze

Come appare nei progetti reali

In applicazioni grandi, con tabelle enormi o cache lette da centinaia di thread ma modificate raramente, usare ReaderWriterLockSlim aiuta a evitare i "colli" sulle letture. Per esempio:

  • Cache di configurazione usata dai microservizi.
  • Archivio di dizionari per calcoli finanziari.
  • Cache interna per il routing dei messaggi.

Sottolineo — nella maggior parte dei casi, soprattutto se stai iniziando con la multithreading, il semplice lock è più facile e più sicuro. ReaderWriterLockSlim è una soluzione per scenari con letture concorrenti davvero frequenti.

Visualizzazione: schema di accesso

flowchart TD
    A[Thread-Lettore 1] --Read--> D[ReaderWriterLockSlim]
    B[Thread-Lettore 2] --Read--> D
    C[Thread-Scrittore] --Write--> D
    D --Read permesso--> E[Lettura della collezione condivisa]
    D --Write blocca lettura/scrittura--> F[Scrittura della collezione condivisa]

Finché ci sono solo letture — l'accesso è aperto a tutti; al tentativo di scrittura — tutti aspettano che la scrittura finisca.

7. ReaderWriterLockSlim: sottigliezze ed errori tipici

Lock Recursion (ricorsione del lock): Di default ReaderWriterLockSlim non permette allo stesso thread di acquisire ripetutamente lo stesso tipo di lock. Quindi, se un thread tiene già un read lock e prova a prenderlo di nuovo — viene lanciata un'eccezione. Lo stesso vale per il write lock. Tuttavia, UpgradeableReadLock può essere escalato a write lock dall'interno.

Non invertire l'ordine: Acquisisci EnterUpgradeableReadLock() → al suo interno EnterWriteLock(). Ma non il contrario!

Eccezioni: Se non rilasci il lock (per esempio non chiami ExitWriteLock()), gli altri thread aspetteranno per sempre. Per questo usa sempre try { ... } finally { ... }.

Non tenere a lungo: Non eseguire operazioni IO lunghe dentro il lock. Più a lungo un thread tiene la lock, più ritardi causerà agli altri. Cerca di uscire dai blocchi lock il prima possibile.

Valuta la complessità: Non usare ReaderWriterLockSlim per proteggere operazioni "piccole" dove il normale lock è più semplice e veloce.

2
Compito
C# SELF, livello 56, lezione 4
Bloccato
Utilizzo di ReaderWriterLockSlim per organizzare la lettura
Utilizzo di ReaderWriterLockSlim per organizzare la lettura
1
Sondaggio/quiz
Sincronizzazione dei thread, livello 56, lezione 4
Non disponibile
Sincronizzazione dei thread
Problema delle risorse condivise
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION