Classe System.Threading.ReaderWriterLockSlim

Questo articolo fornisce osservazioni supplementari alla documentazione di riferimento per questa API.

Usare ReaderWriterLockSlim per proteggere una risorsa letta da più thread e scritta in da un thread alla volta. ReaderWriterLockSlim consente a più thread di essere in modalità di lettura, consente a un thread di essere in modalità scrittura con proprietà esclusiva del blocco e consente a un thread con accesso in lettura aggiornabile, da cui il thread può eseguire l'aggiornamento alla modalità di scrittura senza dover rinunciare all'accesso in lettura alla risorsa.

Nota

Per impostazione predefinita, le nuove istanze di ReaderWriterLockSlim vengono create con il LockRecursionPolicy.NoRecursion flag e non consentono la ricorsione. Questo criterio predefinito è consigliato per tutti i nuovi sviluppi, perché la ricorsione introduce complicazioni non necessarie e rende il codice più soggetto a deadlock. Per semplificare la migrazione da progetti esistenti che usano Monitor o ReaderWriterLock, è possibile usare il LockRecursionPolicy.SupportsRecursion flag per creare istanze di ReaderWriterLockSlim che consentono la ricorsione.

Un thread può immettere il blocco in tre modalità: modalità di lettura, modalità di scrittura e modalità di lettura aggiornabile. Nella parte restante di questo argomento, "modalità di lettura aggiornabile" viene definita "modalità aggiornabile" e la frase "enter x mode" viene usata in preferenza alla frase più lunga "enter the lock in x mode".

Indipendentemente dai criteri di ricorsione, un solo thread può essere in modalità di scrittura in qualsiasi momento. Quando un thread è in modalità di scrittura, nessun altro thread può immettere il blocco in qualsiasi modalità. Un solo thread può essere in modalità aggiornabile in qualsiasi momento. Qualsiasi numero di thread può essere in modalità di lettura e può essere presente un thread in modalità aggiornabile mentre altri thread sono in modalità di lettura.

Importante

Il tipo implementa l'interfaccia IDisposable. Dopo aver utilizzato il tipo, è necessario eliminarlo direttamente o indirettamente. Per eliminare direttamente il tipo, chiamare il metodo Dispose in un blocco try/catch. Per eliminarlo indirettamente, utilizzare un costrutto di linguaggio come ad esempio using in C# o Using in Visual Basic. Per altre informazioni, vedere la sezione "Uso di un oggetto che implementa IDisposable" nell'argomento relativo all'interfaccia IDisposable.

ReaderWriterLockSlim ha affinità thread gestita; ovvero, ogni Thread oggetto deve effettuare le proprie chiamate al metodo per entrare e uscire dalla modalità di blocco. Nessun thread può modificare la modalità di un altro thread.

Se un oggetto ReaderWriterLockSlim non consente la ricorsione, un thread che tenta di immettere il blocco può bloccarsi per diversi motivi:

  • Thread che tenta di accedere ai blocchi in modalità di lettura se sono presenti thread in attesa di attivare la modalità di scrittura o se è presente un singolo thread in modalità scrittura.

    Nota

    Il blocco dei nuovi lettori quando i writer vengono accodati è un criterio di equità di blocco che favorisca gli scrittori. La politica di equità attuale bilancia l'equità ai lettori e ai writer, per promuovere la velocità effettiva negli scenari più comuni. Le versioni future di .NET possono introdurre nuovi criteri di equità.

  • Thread che tenta di attivare blocchi in modalità aggiornabile se è già presente un thread in modalità aggiornabile, se sono presenti thread in attesa di entrare in modalità scrittura o se è presente un singolo thread in modalità scrittura.

  • Thread che tenta di immettere blocchi in modalità scrittura se è presente un thread in una delle tre modalità.

Aggiornare e effettuare il downgrade dei blocchi

La modalità aggiornabile è destinata ai casi in cui un thread legge in genere dalla risorsa protetta, ma potrebbe essere necessario scrivervi se viene soddisfatta una condizione. Un thread che è entrato in modalità ReaderWriterLockSlim aggiornabile ha accesso in lettura alla risorsa protetta e può eseguire l'aggiornamento alla modalità di scrittura chiamando i EnterWriteLock metodi o TryEnterWriteLock . Poiché può essere presente un solo thread in modalità aggiornabile alla volta, l'aggiornamento alla modalità di scrittura non può bloccarsi quando la ricorsione non è consentita, ovvero il criterio predefinito.

Importante

Indipendentemente dai criteri di ricorsione, un thread che inizialmente è entrato in modalità di lettura non può eseguire l'aggiornamento alla modalità aggiornabile o alla modalità di scrittura, perché questo modello crea una forte probabilità di deadlock. Ad esempio, se due thread in modalità di lettura tentano entrambi di attivare la modalità di scrittura, questi verranno deadlock. La modalità aggiornabile è progettata per evitare tali deadlock.

Se sono presenti altri thread in modalità di lettura, il thread che sta aggiornando i blocchi. Mentre il thread è bloccato, gli altri thread che tentano di accedere alla modalità di lettura vengono bloccati. Quando tutti i thread sono usciti dalla modalità di lettura, il thread aggiornabile bloccato passa alla modalità di scrittura. Se sono presenti altri thread in attesa di entrare in modalità di scrittura, rimangono bloccati, perché il singolo thread in modalità aggiornabile impedisce loro di ottenere l'accesso esclusivo alla risorsa.

Quando il thread in modalità aggiornabile esce dalla modalità di scrittura, altri thread in attesa di entrare in modalità di lettura possono farlo, a meno che non ci siano thread in attesa di attivare la modalità di scrittura. Il thread in modalità aggiornabile può eseguire l'aggiornamento e il downgrade illimitato, purché sia l'unico thread che scrive nella risorsa protetta.

Importante

Se si consente a più thread di accedere alla modalità di scrittura o alla modalità aggiornabile, non è necessario consentire a un thread di monopolizzare la modalità aggiornabile. In caso contrario, i thread che tentano di accedere direttamente alla modalità di scrittura verranno bloccati a tempo indeterminato e, mentre sono bloccati, altri thread non potranno accedere alla modalità di lettura.

Un thread in modalità aggiornabile può effettuare il downgrade alla modalità di lettura chiamando prima il EnterReadLock metodo e quindi chiamando il ExitUpgradeableReadLock metodo . Questo modello di downgrade è consentito per tutti i criteri di ricorsione dei blocchi, anche NoRecursion.

Dopo il downgrade alla modalità di lettura, un thread non può immettere nuovamente la modalità aggiornabile fino a quando non è uscito dalla modalità di lettura.

Immettere il blocco in modo ricorsivo

È possibile creare un oggetto che supporti la ReaderWriterLockSlim voce di blocco ricorsiva usando il ReaderWriterLockSlim(LockRecursionPolicy) costruttore che specifica i criteri di blocco e specificando LockRecursionPolicy.SupportsRecursion.

Nota

L'uso della ricorsione non è consigliato per il nuovo sviluppo, perché introduce complicazioni non necessarie e rende il codice più soggetto a deadlock.

Per un ReaderWriterLockSlim oggetto che consente la ricorsione, è possibile specificare quanto segue sulle modalità che un thread può immettere:

  • Un thread in modalità di lettura può entrare in modalità di lettura in modo ricorsivo, ma non può entrare in modalità di scrittura o aggiornabile. Se tenta di eseguire questa operazione, viene generata un'eccezione LockRecursionException . L'immissione della modalità di lettura e quindi l'immissione della modalità di scrittura o aggiornabile è un modello con una forte probabilità di deadlock, quindi non è consentito. Come illustrato in precedenza, viene fornita la modalità aggiornabile per i casi in cui è necessario aggiornare un blocco.

  • Un thread in modalità aggiornabile può attivare la modalità di scrittura e/o la modalità di lettura e può immettere una delle tre modalità in modo ricorsivo. Tuttavia, un tentativo di immettere blocchi in modalità scrittura se sono presenti altri thread in modalità di lettura.

  • Un thread in modalità scrittura può entrare in modalità di lettura e/o aggiornabile e può entrare in una delle tre modalità in modo ricorsivo.

  • Un thread che non ha immesso il blocco può entrare in qualsiasi modalità. Questo tentativo può bloccarsi per gli stessi motivi del tentativo di immettere un blocco non ricorsivo.

Un thread può uscire dalle modalità immesse in qualsiasi ordine, purché esce da ogni modalità esattamente quante volte è entrato in tale modalità. Se un thread tenta di uscire da una modalità troppe volte o di uscire da una modalità non immessa, viene generata un'eccezione SynchronizationLockException .

Stati di blocco

Può risultare utile pensare al blocco in termini di stati. Un ReaderWriterLockSlim può essere in uno dei quattro stati: non immesso, letto, aggiornato e scritto.

  • Non immesso: in questo stato, nessun thread ha immesso il blocco (o tutti i thread hanno chiuso il blocco).

  • Lettura: in questo stato, uno o più thread hanno immesso il blocco per l'accesso in lettura alla risorsa protetta.

    Nota

    Un thread può accedere al blocco in modalità di lettura usando i EnterReadLock metodi o TryEnterReadLock o eseguendo il downgrade dalla modalità aggiornabile.

  • Aggiornamento: in questo stato, un thread ha immesso il blocco per l'accesso in lettura con l'opzione di aggiornamento all'accesso in scrittura (ovvero in modalità aggiornabile) e zero o più thread hanno immesso il blocco per l'accesso in lettura. Non più di un thread alla volta può immettere il blocco con l'opzione di aggiornamento; i thread aggiuntivi che tentano di accedere alla modalità aggiornabile vengono bloccati.

  • Scrittura: in questo stato, un thread ha immesso il blocco per l'accesso in scrittura alla risorsa protetta. Tale thread ha il possesso esclusivo del blocco. Qualsiasi altro thread che tenta di immettere il blocco per qualsiasi motivo viene bloccato.

Nella tabella seguente vengono descritte le transizioni tra stati di blocco, per i blocchi che non consentono la ricorsione, quando un thread t esegue l'azione descritta nella colonna più a sinistra. Al momento dell'esecuzione dell'azione, t non è disponibile alcuna modalità. Il caso speciale in cui t è in modalità aggiornabile è descritto nelle note a piè di pagina della tabella. La riga superiore descrive lo stato iniziale del blocco. Le celle descrivono cosa accade al thread e mostrano le modifiche apportate allo stato di blocco tra parentesi.

Transizione Non immesso (N) Lettura (R) Aggiornamento (U) Scrittura (W)
t entra in modalità lettura t entra (R). t blocca se i thread sono in attesa della modalità di scrittura; in caso contrario, t entra. t blocca se i thread sono in attesa della modalità di scrittura; in caso contrario, t entra.1 t Blocchi.
t entra in modalità aggiornabile t entra (U). t blocca se i thread sono in attesa di modalità di scrittura o modalità di aggiornamento; in caso contrario, t immette (U). t Blocchi. t Blocchi.
t entra in modalità scrittura t entra (W). t Blocchi. t Blocchi.2 t Blocchi.

1 Se t viene avviato in modalità aggiornabile, passa alla modalità di lettura. Questa azione non si blocca mai. Lo stato del blocco non cambia. Il thread può quindi completare un downgrade alla modalità di lettura chiudendo la modalità aggiornabile.

2 Se t viene avviato in modalità aggiornabile, si blocca se sono presenti thread in modalità di lettura. In caso contrario, esegue l'aggiornamento alla modalità di scrittura. Lo stato del blocco cambia in Scrittura (W). Se t i blocchi perché sono presenti thread in modalità di lettura, passa alla modalità di scrittura non appena l'ultimo thread esce dalla modalità di lettura, anche se sono presenti thread in attesa di attivare la modalità di scrittura.

Quando si verifica una modifica dello stato perché un thread esce dal blocco, il thread successivo da risvegliare viene selezionato come segue:

  • In primo luogo, un thread in attesa della modalità di scrittura ed è già in modalità aggiornabile (può essere presente al massimo un thread di questo tipo).
  • In caso contrario, un thread in attesa della modalità di scrittura.
  • In caso contrario, un thread in attesa della modalità aggiornabile.
  • In caso contrario, tutti i thread in attesa della modalità di lettura.

Lo stato successivo del blocco è sempre Write (W) nei primi due casi e Upgrade (U) nel terzo caso, indipendentemente dallo stato del blocco quando il thread di uscita ha attivato la modifica dello stato. Nell'ultimo caso, lo stato del blocco è Upgrade (U) se è presente un thread in modalità aggiornabile dopo la modifica dello stato e Read (R) in caso contrario, indipendentemente dallo stato precedente.

Esempi

Nell'esempio seguente viene illustrata una semplice cache sincronizzata che contiene stringhe con chiavi integer. Un'istanza di ReaderWriterLockSlim viene usata per sincronizzare l'accesso Dictionary<TKey,TValue> a che funge da cache interna.

L'esempio include metodi semplici da aggiungere alla cache, eliminare dalla cache e leggere dalla cache. Per illustrare i timeout, l'esempio include un metodo che aggiunge alla cache solo se può farlo entro un timeout specificato.

Per illustrare la modalità aggiornabile, l'esempio include un metodo che recupera il valore associato a una chiave e lo confronta con un nuovo valore. Se il valore è invariato, il metodo restituisce uno stato che indica che non viene apportata alcuna modifica. Se non viene trovato alcun valore per la chiave, viene inserita la coppia chiave/valore. Se il valore è stato modificato, viene aggiornato. La modalità aggiornabile consente al thread di eseguire l'aggiornamento dall'accesso in lettura all'accesso in scrittura in base alle esigenze, senza il rischio di deadlock.

L'esempio include un'enumerazione annidata che specifica i valori restituiti per il metodo che illustra la modalità aggiornabile.

Nell'esempio viene usato il costruttore senza parametri per creare il blocco, pertanto la ricorsione non è consentita. La programmazione di ReaderWriterLockSlim è più semplice e meno soggetta a errori quando il blocco non consente la ricorsione.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks
public class SynchronizedCache 
{
    private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
    private Dictionary<int, string> innerCache = new Dictionary<int, string>();

    public int Count
    { get { return innerCache.Count; } }

    public string Read(int key)
    {
        cacheLock.EnterReadLock();
        try
        {
            return innerCache[key];
        }
        finally
        {
            cacheLock.ExitReadLock();
        }
    }

    public void Add(int key, string value)
    {
        cacheLock.EnterWriteLock();
        try
        {
            innerCache.Add(key, value);
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }

    public bool AddWithTimeout(int key, string value, int timeout)
    {
        if (cacheLock.TryEnterWriteLock(timeout))
        {
            try
            {
                innerCache.Add(key, value);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
            return true;
        }
        else
        {
            return false;
        }
    }

    public AddOrUpdateStatus AddOrUpdate(int key, string value)
    {
        cacheLock.EnterUpgradeableReadLock();
        try
        {
            string result = null;
            if (innerCache.TryGetValue(key, out result))
            {
                if (result == value)
                {
                    return AddOrUpdateStatus.Unchanged;
                }
                else
                {
                    cacheLock.EnterWriteLock();
                    try
                    {
                        innerCache[key] = value;
                    }
                    finally
                    {
                        cacheLock.ExitWriteLock();
                    }
                    return AddOrUpdateStatus.Updated;
                }
            }
            else
            {
                cacheLock.EnterWriteLock();
                try
                {
                    innerCache.Add(key, value);
                }
                finally
                {
                    cacheLock.ExitWriteLock();
                }
                return AddOrUpdateStatus.Added;
            }
        }
        finally
        {
            cacheLock.ExitUpgradeableReadLock();
        }
    }

    public void Delete(int key)
    {
        cacheLock.EnterWriteLock();
        try
        {
            innerCache.Remove(key);
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }

    public enum AddOrUpdateStatus
    {
        Added,
        Updated,
        Unchanged
    };

    ~SynchronizedCache()
    {
       if (cacheLock != null) cacheLock.Dispose();
    }
}
Public Class SynchronizedCache
    Private cacheLock As New ReaderWriterLockSlim()
    Private innerCache As New Dictionary(Of Integer, String)

    Public ReadOnly Property Count As Integer
       Get
          Return innerCache.Count
       End Get
    End Property
    
    Public Function Read(ByVal key As Integer) As String
        cacheLock.EnterReadLock()
        Try
            Return innerCache(key)
        Finally
            cacheLock.ExitReadLock()
        End Try
    End Function

    Public Sub Add(ByVal key As Integer, ByVal value As String)
        cacheLock.EnterWriteLock()
        Try
            innerCache.Add(key, value)
        Finally
            cacheLock.ExitWriteLock()
        End Try
    End Sub

    Public Function AddWithTimeout(ByVal key As Integer, ByVal value As String, _
                                   ByVal timeout As Integer) As Boolean
        If cacheLock.TryEnterWriteLock(timeout) Then
            Try
                innerCache.Add(key, value)
            Finally
                cacheLock.ExitWriteLock()
            End Try
            Return True
        Else
            Return False
        End If
    End Function

    Public Function AddOrUpdate(ByVal key As Integer, _
                                ByVal value As String) As AddOrUpdateStatus
        cacheLock.EnterUpgradeableReadLock()
        Try
            Dim result As String = Nothing
            If innerCache.TryGetValue(key, result) Then
                If result = value Then
                    Return AddOrUpdateStatus.Unchanged
                Else
                    cacheLock.EnterWriteLock()
                    Try
                        innerCache.Item(key) = value
                    Finally
                        cacheLock.ExitWriteLock()
                    End Try
                    Return AddOrUpdateStatus.Updated
                End If
            Else
                cacheLock.EnterWriteLock()
                Try
                    innerCache.Add(key, value)
                Finally
                    cacheLock.ExitWriteLock()
                End Try
                Return AddOrUpdateStatus.Added
            End If
        Finally
            cacheLock.ExitUpgradeableReadLock()
        End Try
    End Function

    Public Sub Delete(ByVal key As Integer)
        cacheLock.EnterWriteLock()
        Try
            innerCache.Remove(key)
        Finally
            cacheLock.ExitWriteLock()
        End Try
    End Sub

    Public Enum AddOrUpdateStatus
        Added
        Updated
        Unchanged
    End Enum

    Protected Overrides Sub Finalize()
       If cacheLock IsNot Nothing Then cacheLock.Dispose()
    End Sub
End Class

Il codice seguente usa quindi l'oggetto SynchronizedCache per archiviare un dizionario di nomi vegetali. Crea tre attività. Il primo scrive i nomi delle verdure archiviate in una matrice in un'istanza SynchronizedCache di . La seconda e la terza attività visualizzano i nomi delle verdure, il primo in ordine crescente (da indice basso a indice elevato), il secondo in ordine decrescente. L'attività finale cerca la stringa "cetriolo" e, quando lo trova, chiama il EnterUpgradeableReadLock metodo per sostituire la stringa "bean verde".

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks
public class Example
{
   public static void Main()
   {
      var sc = new SynchronizedCache();
      var tasks = new List<Task>();
      int itemsWritten = 0;

      // Execute a writer.
      tasks.Add(Task.Run( () => { String[] vegetables = { "broccoli", "cauliflower",
                                                          "carrot", "sorrel", "baby turnip",
                                                          "beet", "brussel sprout",
                                                          "cabbage", "plantain",
                                                          "spinach", "grape leaves",
                                                          "lime leaves", "corn",
                                                          "radish", "cucumber",
                                                          "raddichio", "lima beans" };
                                  for (int ctr = 1; ctr <= vegetables.Length; ctr++)
                                     sc.Add(ctr, vegetables[ctr - 1]);

                                  itemsWritten = vegetables.Length;
                                  Console.WriteLine("Task {0} wrote {1} items\n",
                                                    Task.CurrentId, itemsWritten);
                                } ));
      // Execute two readers, one to read from first to last and the second from last to first.
      for (int ctr = 0; ctr <= 1; ctr++) {
         bool desc = ctr == 1;
         tasks.Add(Task.Run( () => { int start, last, step;
                                     int items;
                                     do {
                                        String output = String.Empty;
                                        items = sc.Count;
                                        if (! desc) {
                                           start = 1;
                                           step = 1;
                                           last = items;
                                        }
                                        else {
                                           start = items;
                                           step = -1;
                                           last = 1;
                                        }

                                        for (int index = start; desc ? index >= last : index <= last; index += step)
                                           output += String.Format("[{0}] ", sc.Read(index));

                                        Console.WriteLine("Task {0} read {1} items: {2}\n",
                                                          Task.CurrentId, items, output);
                                     } while (items < itemsWritten | itemsWritten == 0);
                             } ));
      }
      // Execute a red/update task.
      tasks.Add(Task.Run( () => { Thread.Sleep(100);
                                  for (int ctr = 1; ctr <= sc.Count; ctr++) {
                                     String value = sc.Read(ctr);
                                     if (value == "cucumber")
                                        if (sc.AddOrUpdate(ctr, "green bean") != SynchronizedCache.AddOrUpdateStatus.Unchanged)
                                           Console.WriteLine("Changed 'cucumber' to 'green bean'");
                                  }
                                } ));

      // Wait for all three tasks to complete.
      Task.WaitAll(tasks.ToArray());

      // Display the final contents of the cache.
      Console.WriteLine();
      Console.WriteLine("Values in synchronized cache: ");
      for (int ctr = 1; ctr <= sc.Count; ctr++)
         Console.WriteLine("   {0}: {1}", ctr, sc.Read(ctr));
   }
}
// The example displays the following output:
//    Task 1 read 0 items:
//
//    Task 3 wrote 17 items
//
//
//    Task 1 read 17 items: [broccoli] [cauliflower] [carrot] [sorrel] [baby turnip] [
//    beet] [brussel sprout] [cabbage] [plantain] [spinach] [grape leaves] [lime leave
//    s] [corn] [radish] [cucumber] [raddichio] [lima beans]
//
//    Task 2 read 0 items:
//
//    Task 2 read 17 items: [lima beans] [raddichio] [cucumber] [radish] [corn] [lime
//    leaves] [grape leaves] [spinach] [plantain] [cabbage] [brussel sprout] [beet] [b
//    aby turnip] [sorrel] [carrot] [cauliflower] [broccoli]
//
//    Changed 'cucumber' to 'green bean'
//
//    Values in synchronized cache:
//       1: broccoli
//       2: cauliflower
//       3: carrot
//       4: sorrel
//       5: baby turnip
//       6: beet
//       7: brussel sprout
//       8: cabbage
//       9: plantain
//       10: spinach
//       11: grape leaves
//       12: lime leaves
//       13: corn
//       14: radish
//       15: green bean
//       16: raddichio
//       17: lima beans
Public Module Example
   Public Sub Main()
      Dim sc As New SynchronizedCache()
      Dim tasks As New List(Of Task)
      Dim itemsWritten As Integer
      
      ' Execute a writer.
      tasks.Add(Task.Run( Sub()
                             Dim vegetables() As String = { "broccoli", "cauliflower",
                                                            "carrot", "sorrel", "baby turnip",
                                                            "beet", "brussel sprout",
                                                            "cabbage", "plantain",
                                                            "spinach", "grape leaves",
                                                            "lime leaves", "corn",
                                                            "radish", "cucumber",
                                                            "raddichio", "lima beans" }
                             For ctr As Integer = 1 to vegetables.Length
                                sc.Add(ctr, vegetables(ctr - 1))
                             Next
                             itemsWritten = vegetables.Length
                             Console.WriteLine("Task {0} wrote {1} items{2}",
                                               Task.CurrentId, itemsWritten, vbCrLf)
                          End Sub))
      ' Execute two readers, one to read from first to last and the second from last to first.
      For ctr As Integer = 0 To 1
         Dim flag As Integer = ctr
         tasks.Add(Task.Run( Sub()
                                Dim start, last, stp As Integer
                                Dim items As Integer
                                Do
                                   Dim output As String = String.Empty
                                   items = sc.Count
                                   If flag = 0 Then
                                      start = 1 : stp = 1 : last = items
                                   Else
                                      start = items : stp = -1 : last = 1
                                   End If
                                   For index As Integer = start To last Step stp
                                      output += String.Format("[{0}] ", sc.Read(index))
                                   Next
                                   Console.WriteLine("Task {0} read {1} items: {2}{3}",
                                                           Task.CurrentId, items, output,
                                                           vbCrLf)
                                Loop While items < itemsWritten Or itemsWritten = 0
                             End Sub))
      Next
      ' Execute a red/update task.
      tasks.Add(Task.Run( Sub()
                             For ctr As Integer = 1 To sc.Count
                                Dim value As String = sc.Read(ctr)
                                If value = "cucumber" Then
                                   If sc.AddOrUpdate(ctr, "green bean") <> SynchronizedCache.AddOrUpdateStatus.Unchanged Then
                                      Console.WriteLine("Changed 'cucumber' to 'green bean'")
                                   End If
                                End If
                             Next
                          End Sub ))

      ' Wait for all three tasks to complete.
      Task.WaitAll(tasks.ToArray())

      ' Display the final contents of the cache.
      Console.WriteLine()
      Console.WriteLine("Values in synchronized cache: ")
      For ctr As Integer = 1 To sc.Count
         Console.WriteLine("   {0}: {1}", ctr, sc.Read(ctr))
      Next
   End Sub
End Module
' The example displays output like the following:
'    Task 1 read 0 items:
'
'    Task 3 wrote 17 items
'
'    Task 1 read 17 items: [broccoli] [cauliflower] [carrot] [sorrel] [baby turnip] [
'    beet] [brussel sprout] [cabbage] [plantain] [spinach] [grape leaves] [lime leave
'    s] [corn] [radish] [cucumber] [raddichio] [lima beans]
'
'    Task 2 read 0 items:
'
'    Task 2 read 17 items: [lima beans] [raddichio] [cucumber] [radish] [corn] [lime
'    leaves] [grape leaves] [spinach] [plantain] [cabbage] [brussel sprout] [beet] [b
'    aby turnip] [sorrel] [carrot] [cauliflower] [broccoli]
'
'    Changed 'cucumber' to 'green bean'
'
'    Values in synchronized cache:
'       1: broccoli
'       2: cauliflower
'       3: carrot
'       4: sorrel
'       5: baby turnip
'       6: beet
'       7: brussel sprout
'       8: cabbage
'       9: plantain
'       10: spinach
'       11: grape leaves
'       12: lime leaves
'       13: corn
'       14: radish
'       15: green bean
'       16: raddichio
'       17: lima beans