Compartir a través de


Clase System.Threading.ReaderWriterLockSlim

En este artículo se proporcionan comentarios adicionales a la documentación de referencia de esta API.

Use ReaderWriterLockSlim para proteger un recurso leído por varios subprocesos y escrito en un subproceso a la vez. ReaderWriterLockSlim permite que varios subprocesos estén en modo de lectura, permite que un subproceso esté en modo de escritura con la propiedad exclusiva del bloqueo y permite que un subproceso que tenga acceso de lectura esté en modo de lectura actualizable, desde el que el subproceso puede actualizar al modo de escritura sin tener que renunciar a su acceso de lectura al recurso.

Nota:

De forma predeterminada, se crean nuevas instancias de ReaderWriterLockSlim con la LockRecursionPolicy.NoRecursion marca y no permiten la recursividad. Esta directiva predeterminada se recomienda para todo el desarrollo nuevo, ya que la recursividad presenta complicaciones innecesarias y hace que el código sea más propenso a interbloqueos. Para simplificar la migración desde proyectos existentes que usan Monitor o ReaderWriterLock, puede usar la LockRecursionPolicy.SupportsRecursion marca para crear instancias de ReaderWriterLockSlim que permitan la recursividad.

Un subproceso puede entrar en el bloqueo en tres modos: modo de lectura, modo de escritura y modo de lectura actualizable. (En el resto de este tema, el "modo de lectura actualizable" se conoce como "modo actualizable" y la frase "entrar x en modo" se usa en preferencia a la frase más larga "entrar en x modo de bloqueo").

Independientemente de la directiva de recursividad, solo un subproceso puede estar en modo de escritura en cualquier momento. Cuando un subproceso está en modo de escritura, ningún otro subproceso puede entrar en el bloqueo en ningún modo. Solo un subproceso puede estar en modo actualizable en cualquier momento. Cualquier número de subprocesos puede estar en modo de lectura y puede haber un subproceso en modo actualizable mientras que otros subprocesos están en modo de lectura.

Importante

Este tipo implementa la interfaz IDisposable. Cuando haya terminado de utilizar el tipo, debe desecharlo directa o indirectamente. Para eliminar el tipo directamente, llame a su método Dispose en un bloque try/catch. Para deshacerse de él indirectamente, use una construcción de lenguaje como using (en C#) o Using (en Visual Basic). Para más información, vea la sección "Uso de objetos que implementan IDisposable" en el tema de la interfaz IDisposable.

ReaderWriterLockSlim tiene afinidad de subproceso administrado; es decir, cada Thread objeto debe realizar sus propias llamadas de método para entrar y salir de los modos de bloqueo. Ningún subproceso puede cambiar el modo de otro subproceso.

ReaderWriterLockSlim Si no permite la recursividad, un subproceso que intenta entrar en el bloqueo puede bloquearse por varias razones:

  • Un subproceso que intenta entrar en bloques en modo de lectura si hay subprocesos que esperan entrar en modo de escritura o si hay un único subproceso en modo de escritura.

    Nota:

    Bloquear nuevos lectores cuando los escritores están en cola es una directiva de equidad de bloqueo que favorece a los escritores. La directiva de equidad actual equilibra la equidad con los lectores y escritores, para promover el rendimiento en los escenarios más comunes. Las versiones futuras de .NET pueden introducir nuevas directivas de equidad.

  • Un subproceso que intenta entrar en modo actualizable bloquea si ya hay un subproceso en modo actualizable, si hay subprocesos que esperan entrar en modo de escritura o si hay un único subproceso en modo de escritura.

  • Un subproceso que intenta entrar en bloques de modo de escritura si hay un subproceso en cualquiera de los tres modos.

Bloqueos de actualización y degradación

El modo actualizable está pensado para los casos en los que un subproceso suele leer desde el recurso protegido, pero es posible que tenga que escribir en él si se cumple alguna condición. Un subproceso que ha entrado en modo ReaderWriterLockSlim actualizable tiene acceso de lectura al recurso protegido y puede actualizar al modo de escritura llamando a los EnterWriteLock métodos o TryEnterWriteLock . Dado que solo puede haber un subproceso en modo actualizable a la vez, la actualización al modo de escritura no puede interbloquear cuando no se permite la recursividad, que es la directiva predeterminada.

Importante

Independientemente de la directiva de recursividad, un subproceso que inicialmente entró en modo de lectura no se permite actualizar al modo actualizable o al modo de escritura, ya que ese patrón crea una probabilidad fuerte de interbloqueos. Por ejemplo, si dos subprocesos en modo de lectura intentan entrar en modo de escritura, se interbloquearán. El modo actualizable está diseñado para evitar estos interbloqueos.

Si hay otros subprocesos en modo de lectura, el subproceso que está actualizando bloques. Mientras el subproceso está bloqueado, se bloquean otros subprocesos que intentan entrar en modo de lectura. Cuando todos los subprocesos han salido del modo de lectura, el subproceso actualizable bloqueado entra en modo de escritura. Si hay otros subprocesos que esperan entrar en modo de escritura, permanecen bloqueados, ya que el único subproceso que está en modo actualizable impide que obtengan acceso exclusivo al recurso.

Cuando el subproceso en modo actualizable sale del modo de escritura, otros subprocesos que esperan entrar en modo de lectura pueden hacerlo, a menos que haya subprocesos que esperan entrar en modo de escritura. El subproceso en modo actualizable puede actualizar y degradarse indefinidamente, siempre y cuando sea el único subproceso que escribe en el recurso protegido.

Importante

Si permite que varios subprocesos entren en el modo de escritura o el modo actualizable, no debe permitir que un subproceso monopolíce el modo actualizable. De lo contrario, los subprocesos que intentan entrar en modo de escritura directamente se bloquearán indefinidamente y, mientras están bloqueados, otros subprocesos no podrán entrar en modo de lectura.

Un subproceso en modo actualizable puede degradarse al modo de lectura llamando primero al EnterReadLock método y, a continuación, llamando al ExitUpgradeableReadLock método . Este patrón de degradación se permite para todas las directivas de recursividad de bloqueo, incluso NoRecursion.

Después de degradar al modo de lectura, un subproceso no puede volver a escribir el modo actualizable hasta que haya salido del modo de lectura.

Escriba el bloqueo de forma recursiva.

Puede crear un ReaderWriterLockSlim que admita la entrada de bloqueo recursivo mediante el constructor que especifica la ReaderWriterLockSlim(LockRecursionPolicy) directiva de bloqueo y especificando LockRecursionPolicy.SupportsRecursion.

Nota:

No se recomienda el uso de recursividad para el nuevo desarrollo, ya que presenta complicaciones innecesarias y hace que el código sea más propenso a interbloqueos.

Para un ReaderWriterLockSlim que permite la recursividad, se puede decir lo siguiente sobre los modos en los que puede entrar un subproceso:

  • Un subproceso en modo de lectura puede entrar en modo de lectura de forma recursiva, pero no puede entrar en modo de escritura ni en modo actualizable. Si intenta hacerlo, se produce una LockRecursionException excepción . Entrar en modo de lectura y, a continuación, entrar en modo de escritura o modo actualizable es un patrón con una probabilidad fuerte de interbloqueos, por lo que no se permite. Como se explicó anteriormente, se proporciona el modo actualizable para los casos en los que es necesario actualizar un bloqueo.

  • Un subproceso en modo actualizable puede entrar en modo de escritura o modo de lectura, y puede especificar cualquiera de los tres modos recursivamente. Sin embargo, un intento de entrar en bloques de modo de escritura si hay otros subprocesos en modo de lectura.

  • Un subproceso en modo de escritura puede entrar en modo de lectura o modo actualizable, y puede especificar cualquiera de los tres modos recursivamente.

  • Un subproceso que no ha entrado en el bloqueo puede entrar en cualquier modo. Este intento puede bloquearse por las mismas razones que un intento de introducir un bloqueo no recursivo.

Un subproceso puede salir de los modos especificados en cualquier orden, siempre y cuando salga de cada modo exactamente tantas veces como entró en ese modo. Si un subproceso intenta salir de un modo demasiadas veces o para salir de un modo que no ha entrado, se produce una SynchronizationLockException excepción .

Estados de bloqueo

Es posible que le resulte útil pensar en el bloqueo en términos de sus estados. Un ReaderWriterLockSlim puede estar en uno de los cuatro estados: no especificado, leído, actualizado y escritura.

  • No especificado: en este estado, ningún subproceso ha entrado en el bloqueo (o todos los subprocesos han salido del bloqueo).

  • Lectura: En este estado, uno o varios subprocesos han entrado en el bloqueo para el acceso de lectura al recurso protegido.

    Nota:

    Un subproceso puede entrar en el bloqueo en modo de lectura mediante los EnterReadLock métodos o TryEnterReadLock o degradando desde el modo actualizable.

  • Actualización: en este estado, un subproceso ha entrado en el bloqueo para el acceso de lectura con la opción de actualizar al acceso de escritura (es decir, en modo actualizable) y cero o más subprocesos han entrado en el bloqueo para el acceso de lectura. No más de un subproceso a la vez puede entrar en el bloqueo con la opción de actualizar; Se bloquean los subprocesos adicionales que intentan entrar en modo actualizable.

  • Escritura: en este estado, un subproceso ha entrado en el bloqueo para el acceso de escritura al recurso protegido. Ese subproceso tiene posesión exclusiva del bloqueo. Cualquier otro subproceso que intente entrar en el bloqueo por cualquier motivo está bloqueado.

En la tabla siguiente se describen las transiciones entre estados de bloqueo, para bloqueos que no permiten la recursividad, cuando un subproceso t realiza la acción descrita en la columna situada más a la izquierda. En el momento en que realiza la acción, t no tiene modo. (El caso especial en t el que se encuentra en modo actualizable se describe en las notas al pie de la tabla). La fila superior describe el estado inicial del bloqueo. Las celdas describen lo que sucede con el subproceso y muestran los cambios en el estado de bloqueo entre paréntesis.

Transición No especificado (N) Lectura (R) Actualización (U) Escritura (W)
t entra en modo de lectura t entra (R). t bloquea si los subprocesos están esperando el modo de escritura; de lo contrario, t escribe. t bloquea si los subprocesos están esperando el modo de escritura; de lo contrario, t escribe.1 t Bloques.
t entra en el modo actualizable t entra (U). t bloquea si los subprocesos están esperando el modo de escritura o el modo de actualización; de lo contrario, t entra (U). t Bloques. t Bloques.
t entra en el modo de escritura t entra (W). t Bloques. t Bloques.2 t Bloques.

1 Si t comienza en modo actualizable, entra en modo de lectura. Esta acción nunca se bloquea. El estado de bloqueo no cambia. (Después, el subproceso puede completar una degradación al modo de lectura si sale del modo actualizable).

2 Si t comienza en modo actualizable, bloquea si hay subprocesos en modo de lectura. De lo contrario, se actualiza al modo de escritura. El estado de bloqueo cambia a Write (W). Si t bloquea porque hay subprocesos en modo de lectura, entra en modo de escritura en cuanto el último subproceso sale del modo de lectura, incluso si hay subprocesos que esperan entrar en modo de escritura.

Cuando se produce un cambio de estado porque un subproceso sale del bloqueo, el siguiente subproceso que se va a despertar se selecciona de la siguiente manera:

  • En primer lugar, un subproceso que está esperando el modo de escritura y que ya está en modo actualizable (puede haber como máximo un subproceso de este tipo).
  • Con errores, un subproceso que está esperando el modo de escritura.
  • Con errores, un subproceso que está esperando el modo actualizable.
  • Con errores, todos los subprocesos que están esperando el modo de lectura.

El estado posterior del bloqueo siempre es Write (W) en los dos primeros casos y Upgrade (U) en el tercer caso, independientemente del estado del bloqueo cuando el subproceso de salida desencadene el cambio de estado. En el último caso, el estado del bloqueo es Upgrade (U) si hay un subproceso en modo actualizable después del cambio de estado y Read (R) de lo contrario, independientemente del estado anterior.

Ejemplos

En el ejemplo siguiente se muestra una caché sincronizada sencilla que contiene cadenas con claves enteras. Se usa una instancia de ReaderWriterLockSlim para sincronizar el acceso a que Dictionary<TKey,TValue> actúa como caché interna.

En el ejemplo se incluyen métodos sencillos para agregar a la memoria caché, eliminar de la memoria caché y leer de la memoria caché. Para demostrar los tiempos de espera, el ejemplo incluye un método que agrega a la memoria caché solo si puede hacerlo dentro de un tiempo de espera especificado.

Para demostrar el modo actualizable, el ejemplo incluye un método que recupera el valor asociado a una clave y lo compara con un nuevo valor. Si el valor no cambia, el método devuelve un estado que indica ningún cambio. Si no se encuentra ningún valor para la clave, se inserta el par clave-valor. Si el valor ha cambiado, se actualiza. El modo actualizable permite al subproceso actualizar desde el acceso de lectura al acceso de escritura según sea necesario, sin riesgo de interbloqueos.

En el ejemplo se incluye una enumeración anidada que especifica los valores devueltos para el método que muestra el modo actualizable.

En el ejemplo se usa el constructor sin parámetros para crear el bloqueo, por lo que no se permite la recursividad. Programar es ReaderWriterLockSlim más sencillo y menos propenso a errores cuando el bloqueo no permite la recursividad.

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

A continuación, el código siguiente usa el SynchronizedCache objeto para almacenar un diccionario de nombres vegetales. Crea tres tareas. La primera escribe los nombres de las verduras almacenadas en una matriz en una SynchronizedCache instancia de . La segunda y tercera tarea muestran los nombres de las verduras, el primero en orden ascendente (de índice bajo a índice alto), el segundo en orden descendente. La tarea final busca la cadena "pepino" y, cuando la encuentra, llama al EnterUpgradeableReadLock método para sustituir la cadena "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