Classe System.Threading.ReaderWriterLockSlim

Cet article vous offre des remarques complémentaires à la documentation de référence pour cette API.

Permet ReaderWriterLockSlim de protéger une ressource lue par plusieurs threads et écrite par un thread à la fois. ReaderWriterLockSlim permet à plusieurs threads d’être en mode lecture, permet à un thread d’être en mode écriture avec la propriété exclusive du verrou et permet à un thread disposant d’un accès en lecture pouvant être en mode lecture mis à niveau, à partir duquel le thread peut effectuer une mise à niveau vers le mode d’écriture sans avoir à renoncer à son accès en lecture à la ressource.

Remarque

  • ReaderWriterLockSlim est similaire à ReaderWriterLock, mais a des règles simplifiées pour la récursivité ainsi que la mise à niveau et la rétrogradation de l’état de verrou. ReaderWriterLockSlim évite de nombreux cas d’interblocage potentiel. En outre, les performances de ReaderWriterLockSlim sont considérablement meilleures que celles de ReaderWriterLock. ReaderWriterLockSlim est recommandé pour tout nouveau développement.
  • ReaderWriterLockSlim n’est pas safe thread-abort. Vous ne devez pas l’utiliser dans un environnement où les threads qui y accèdent peuvent être abandonnés, tels que .NET Framework. Si vous utilisez .NET Core ou .NET 5+, il doit être correct. Abort n’est pas pris en charge dans .NET Core et est obsolète dans .NET 5 et versions ultérieures.

Par défaut, de nouvelles instances sont ReaderWriterLockSlim créées avec l’indicateur et n’autorisent pas la LockRecursionPolicy.NoRecursion récursivité. Cette stratégie par défaut est recommandée pour tous les nouveaux développements, car la récursivité introduit des complications inutiles et rend votre code plus susceptible d’interblocages. Pour simplifier la migration à partir de projets existants qui utilisent Monitor ou ReaderWriterLock, vous pouvez utiliser l’indicateur LockRecursionPolicy.SupportsRecursion pour créer des instances de ReaderWriterLockSlim ce qui autorise la récursivité.

Un thread peut entrer le verrou en trois modes : mode lecture, mode écriture et mode lecture pouvant être mis à niveau. (Dans le reste de cette rubrique, le « mode de lecture pouvant être mis à niveau » est appelé « mode pouvant être mis à niveau », et l’expression « mode entrée x » est utilisée en préférence pour l’expression plus longue « entrer le verrou en x mode ».

Quelle que soit la stratégie de récursivité, un seul thread peut être en mode écriture à tout moment. Lorsqu’un thread est en mode écriture, aucun autre thread ne peut entrer dans le verrou en tout mode. Un seul thread peut être en mode pouvant être mis à niveau à tout moment. Un nombre quelconque de threads peut être en mode lecture, et il peut y avoir un thread en mode mise à niveau alors que d’autres threads sont en mode lecture.

Important

Ce type implémente l'interface IDisposable. Une fois que vous avez fini d’utiliser le type, vous devez le supprimer directement ou indirectement. Pour supprimer directement le type Dispose, appelez sa méthode dans un bloc try/catch. Pour la supprimer indirectement, utilisez une construction de langage telle que using (dans C#) ou Using (dans Visual Basic). Pour plus d’informations, consultez la section « Utilisation d’un objet qui implémente IDisposable » dans la rubrique de l’interface IDisposable.

ReaderWriterLockSlim a une affinité de thread managé ; autrement dit, chaque Thread objet doit effectuer ses propres appels de méthode pour entrer et quitter les modes de verrouillage. Aucun thread ne peut modifier le mode d’un autre thread.

Si une ReaderWriterLockSlim récursivité n’autorise pas la récursivité, un thread qui tente d’entrer dans le verrou peut bloquer pour plusieurs raisons :

  • Thread qui tente d’entrer des blocs en mode lecture s’il existe des threads en attente d’entrer en mode écriture ou s’il existe un thread unique en mode écriture.

    Remarque

    Le blocage de nouveaux lecteurs lorsque les enregistreurs sont mis en file d’attente est une stratégie d’équité des verrous qui favorise les enregistreurs. La stratégie d’équité actuelle équilibre l’équité avec les lecteurs et les rédacteurs, afin de promouvoir le débit dans les scénarios les plus courants. Les futures versions de .NET peuvent introduire de nouvelles stratégies d’équité.

  • Thread qui tente d’entrer des blocs de mode pouvant être mis à niveau s’il existe déjà un thread en mode mise à niveau, s’il existe des threads qui attendent d’entrer en mode écriture ou s’il existe un thread unique en mode écriture.

  • Thread qui tente d’entrer des blocs de mode d’écriture s’il existe un thread dans l’un des trois modes.

Mettre à niveau et rétrograder les verrous

Le mode pouvant être mis à niveau est destiné aux cas où un thread lit généralement à partir de la ressource protégée, mais il peut être nécessaire d’y écrire si une condition est remplie. Un thread qui a entré un mode pouvant être mis à niveau a un ReaderWriterLockSlim accès en lecture à la ressource protégée et peut effectuer une mise à niveau vers le mode d’écriture en appelant le ou TryEnterWriteLock les EnterWriteLock méthodes. Étant donné qu’il ne peut y avoir qu’un seul thread en mode pouvant être mis à niveau à la fois, la mise à niveau vers le mode d’écriture ne peut pas se bloquer lorsque la récursivité n’est pas autorisée, qui est la stratégie par défaut.

Important

Quelle que soit la stratégie de récursivité, un thread entré initialement en mode lecture n’est pas autorisé à effectuer une mise à niveau vers un mode pouvant être mis à niveau ou un mode d’écriture, car ce modèle crée une probabilité forte d’interblocages. Par exemple, si deux threads en mode lecture essaient d’entrer en mode écriture, ils sont bloqués. Le mode pouvant être mis à niveau est conçu pour éviter ces interblocages.

S’il existe d’autres threads en mode lecture, le thread qui met à niveau les blocs. Pendant que le thread est bloqué, d’autres threads qui tentent d’entrer en mode lecture sont bloqués. Lorsque tous les threads ont quitté le mode lecture, le thread pouvant être mis à niveau bloqué entre en mode écriture. S’il existe d’autres threads qui attendent d’entrer en mode écriture, ils restent bloqués, car le thread unique en mode pouvant être mis à niveau les empêche d’accéder exclusivement à la ressource.

Lorsque le thread en mode mise à niveau quitte le mode d’écriture, d’autres threads qui attendent d’entrer en mode lecture peuvent le faire, sauf s’il existe des threads qui attendent d’entrer en mode écriture. Le thread en mode mise à niveau peut mettre à niveau et rétrograder indéfiniment, tant qu’il s’agit du seul thread qui écrit dans la ressource protégée.

Important

Si vous autorisez plusieurs threads à entrer en mode écriture ou en mode mise à niveau, vous ne devez pas autoriser un thread à monopoliser le mode pouvant être mis à niveau. Dans le cas contraire, les threads qui tentent d’entrer directement en mode écriture seront bloqués indéfiniment et, pendant qu’ils sont bloqués, d’autres threads ne pourront pas entrer en mode lecture.

Un thread en mode pouvant être mis à niveau peut passer en mode lecture en appelant d’abord la EnterReadLock méthode, puis en appelant la ExitUpgradeableReadLock méthode. Ce modèle de rétrogradation est autorisé pour toutes les stratégies de récursivité de verrou, même NoRecursion.

Après la rétrogradation en mode lecture, un thread ne peut pas reentérer le mode pouvant être mis à niveau tant qu’il n’a pas quitté le mode lecture.

Entrez le verrou de manière récursive

Vous pouvez créer un ReaderWriterLockSlim qui prend en charge l’entrée de verrou récursif à l’aide du ReaderWriterLockSlim(LockRecursionPolicy) constructeur qui spécifie la stratégie de verrouillage et en spécifiant LockRecursionPolicy.SupportsRecursion.

Remarque

L’utilisation de la récursivité n’est pas recommandée pour le nouveau développement, car elle introduit des complications inutiles et rend votre code plus susceptible d’interblocages.

Pour une ReaderWriterLockSlim opération qui autorise la récursivité, vous pouvez indiquer ce qui suit sur les modes qu’un thread peut entrer :

  • Un thread en mode lecture peut entrer en mode lecture récursivement, mais ne peut pas entrer en mode écriture ou en mode mise à niveau. S’il tente de le faire, une LockRecursionException est levée. Entrer en mode lecture, puis entrer en mode écriture ou mise à niveau est un modèle avec une probabilité forte d’interblocages, de sorte qu’il n’est pas autorisé. Comme indiqué précédemment, le mode pouvant être mis à niveau est fourni dans les cas où il est nécessaire de mettre à niveau un verrou.

  • Un thread en mode pouvant être mis à niveau peut entrer en mode écriture et/ou en mode lecture, et peut entrer l’un des trois modes de manière récursive. Toutefois, une tentative d’entrer des blocs en mode écriture s’il existe d’autres threads en mode lecture.

  • Un thread en mode écriture peut entrer en mode lecture et/ou en mode mise à niveau, et peut entrer l’un des trois modes de manière récursive.

  • Un thread qui n’a pas entré le verrou peut entrer n’importe quel mode. Cette tentative peut bloquer pour les mêmes raisons qu’une tentative d’entrée d’un verrou non récursif.

Un thread peut quitter les modes qu’il a entrés dans n’importe quel ordre, tant qu’il quitte chaque mode exactement autant de fois qu’il est entré dans ce mode. Si un thread tente de quitter un mode trop de fois ou de quitter un mode qu’il n’a pas entré, il SynchronizationLockException est levée.

États de verrouillage

Vous trouverez peut-être utile de penser au verrou en termes de ses états. Un ReaderWriterLockSlim peut être dans l’un des quatre états suivants : non entré, lu, mis à niveau et écriture.

  • Non entré : dans cet état, aucun thread n’a entré le verrou (ou tous les threads ont quitté le verrou).

  • Lecture : dans cet état, un ou plusieurs threads ont entré le verrou pour l’accès en lecture à la ressource protégée.

    Remarque

    Un thread peut entrer le verrou en mode lecture à l’aide des EnterReadLock méthodes ou TryEnterReadLock des méthodes, ou en rétrogradant à partir du mode pouvant être mis à niveau.

  • Mise à niveau : dans cet état, un thread a entré le verrou pour l’accès en lecture avec l’option de mise à niveau pour l’accès en écriture (autrement dit, en mode pouvant être mis à niveau) et zéro ou plusieurs threads ont entré le verrou pour l’accès en lecture. Plus d’un thread à la fois ne peut entrer dans le verrou avec l’option de mise à niveau ; d’autres threads qui tentent d’entrer en mode pouvant être mis à niveau sont bloqués.

  • Écriture : dans cet état, un thread a entré le verrou pour l’accès en écriture à la ressource protégée. Ce thread a la possession exclusive du verrou. Tout autre thread qui tente d’entrer le verrou pour une raison quelconque est bloqué.

Le tableau suivant décrit les transitions entre les états de verrou, pour les verrous qui n’autorisent pas la récursivité, lorsqu’un thread t effectue l’action décrite dans la colonne la plus à gauche. Au moment où elle prend l’action, t n’a aucun mode. (Le cas particulier où t se trouve le mode pouvant être mis à niveau est décrit dans les notes de bas de page du tableau.) La ligne supérieure décrit l’état de départ du verrou. Les cellules décrivent ce qui arrive au thread et affichent les modifications apportées à l’état de verrouillage entre parenthèses.

Transition Non entré (N) Lecture (R) Mise à niveau (U) Écriture (W)
t entre en mode lecture t entre (R). t bloque si les threads attendent le mode d’écriture ; sinon, t entre. t bloque si les threads attendent le mode d’écriture ; sinon, t entre.1 t Blocs.
t entre en mode pouvant être mis à niveau t entre (U). t bloque si les threads attendent le mode d’écriture ou le mode de mise à niveau ; sinon, t entre (U). t Blocs. t Blocs.
t entre en mode d’écriture t entre (W). t Blocs. t Blocs.2 t Blocs.

1 Si t elle démarre en mode mise à niveau, elle entre en mode lecture. Cette action ne bloque jamais. L’état du verrou ne change pas. (Le thread peut ensuite effectuer une rétrogradation en mode lecture en quittant le mode pouvant être mis à niveau.)

2 Si t elle démarre en mode mise à niveau, elle bloque s’il existe des threads en mode lecture. Sinon, il est mis à niveau vers le mode d’écriture. L’état de verrouillage change en écriture (W). Si t des blocs sont bloqués, car il existe des threads en mode lecture, il entre en mode écriture dès que le dernier thread quitte le mode lecture, même s’il y a des threads en attente d’entrer en mode écriture.

Lorsqu’une modification d’état se produit parce qu’un thread quitte le verrou, le thread suivant à réveiller est sélectionné comme suit :

  • Tout d’abord, un thread qui attend le mode d’écriture et qui est déjà en mode mise à niveau (il peut y avoir au maximum un thread de ce type).
  • En cas d’échec, un thread qui attend le mode d’écriture.
  • En cas d’échec, un thread qui attend le mode pouvant être mis à niveau.
  • En cas d’échec, tous les threads qui attendent le mode lecture.

L’état suivant du verrou est toujours Écriture (W) dans les deux premiers cas et Mise à niveau (U) dans le troisième cas, quel que soit l’état du verrou lorsque le thread de sortie a déclenché la modification de l’état. Dans le dernier cas, l’état du verrou est Mise à niveau (U) s’il existe un thread en mode pouvant être mis à niveau après la modification de l’état et en lecture (R) dans le cas contraire, quel que soit l’état précédent.

Exemples

L’exemple suivant montre un cache synchronisé simple qui contient des chaînes avec des clés entières. Une instance de ReaderWriterLockSlim est utilisée pour synchroniser l’accès au Dictionary<TKey,TValue> cache interne.

L’exemple inclut des méthodes simples à ajouter au cache, supprimer du cache et lire à partir du cache. Pour illustrer les délais d’attente, l’exemple inclut une méthode qui ajoute au cache uniquement s’il peut le faire dans un délai d’attente spécifié.

Pour illustrer le mode pouvant être mis à niveau, l’exemple inclut une méthode qui récupère la valeur associée à une clé et la compare à une nouvelle valeur. Si la valeur n’est pas modifiée, la méthode retourne un état indiquant qu’aucune modification n’est apportée. Si aucune valeur n’est trouvée pour la clé, la paire clé/valeur est insérée. Si la valeur a changé, elle est mise à jour. Le mode pouvant être mis à niveau permet au thread de mettre à niveau à partir de l’accès en lecture à l’accès en écriture si nécessaire, sans risque d’interblocages.

L’exemple inclut une énumération imbriquée qui spécifie les valeurs de retour de la méthode qui illustre le mode pouvant être mis à niveau.

L’exemple utilise le constructeur sans paramètre pour créer le verrou, de sorte que la récursivité n’est pas autorisée. La programmation est ReaderWriterLockSlim plus simple et moins sujette à une erreur lorsque le verrou n’autorise pas la récursivité.

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

Le code suivant utilise ensuite l’objet SynchronizedCache pour stocker un dictionnaire de noms de légumes. Il crée trois tâches. Le premier écrit les noms des légumes stockés dans un tableau dans une SynchronizedCache instance. La deuxième et la troisième tâche affichent les noms des légumes, le premier dans l’ordre croissant (de l’index faible à l’index élevé), le deuxième dans l’ordre décroissant. La tâche finale recherche la chaîne « concombre » et, lorsqu’elle la trouve, appelle la EnterUpgradeableReadLock méthode pour remplacer la chaîne « bean vert ».

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