Partager via


Classe System.Threading.ReaderWriterLockSlim

Cet article fournit des remarques supplémentaires à la documentation de référence de 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 il a simplifié les règles de récursivité et de mise à niveau et de rétrogradation de l’état de verrouillage. ReaderWriterLockSlim évite de nombreux cas d’interblocage potentiel. En outre, la performance de ReaderWriterLockSlim est nettement meilleure que celle de ReaderWriterLock. ReaderWriterLockSlim est recommandé pour tout nouveau développement.
  • ReaderWriterLockSlim n’est pas protégé contre les arrêts de threads. 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+, cela devrait aller. 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 de ReaderWriterLockSlim sont créées avec le drapeau LockRecursionPolicy.NoRecursion et n’autorisent pas la 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 « entrer en mode x » est préférée à l'expression plus longue « entrer le verrou en mode x ».)

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.

Importante

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, appelez sa Dispose méthode dans un try/catch bloc. Pour la supprimer indirectement, utilisez une construction de langage telle que using (en C#) ou Using (en Visual Basic). Pour plus d’informations, consultez la section « Utilisation d’un objet implémentant IDisposable » dans la rubrique d’interface IDisposable .

ReaderWriterLockSlim a une affinité de threads managée, c'est-à-dire que chaque objet Thread 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 un ReaderWriterLockSlim n'autorise pas la récursion, un thread qui tente d'entrer dans le verrou peut bloquer pour plusieurs raisons :

  • Un thread qui tente d’entrer en mode lecture se bloque s’il existe des threads en attente d’entrer en mode écriture ou s’il existe un seul thread en mode écriture.

    Remarque

    Le blocage de nouveaux lecteurs lorsque les auteurs sont mis en file d'attente est une politique d'équité des verrous qui favorise les écrivains. La stratégie d'équité actuelle assure un équilibre entre l'équité envers les lecteurs et les rédacteurs, afin de promouvoir l'efficacité dans les scénarios les plus courants. Les futures versions de .NET peuvent introduire de nouvelles stratégies d’équité.

  • Un thread qui tente d’entrer en mode évolvable se bloque s'il y a déjà un thread dans ce mode, s'il y a des threads en attente pour entrer en mode écriture, ou s'il y a un seul thread en mode écriture.

  • Un fil qui tente d'entrer en mode d'écriture se bloque s'il existe un fil 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 est entré dans un ReaderWriterLockSlim dans un mode pouvant être mis à niveau dispose d'un accès en lecture à la ressource protégée et peut effectuer une mise à niveau vers le mode d’écriture en appelant les méthodes EnterWriteLock ou TryEnterWriteLock. É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.

Importante

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 évolutif peut effectuer des mises à niveau et des rétrogradations sans limite, tant qu'il reste le seul thread à écrire dans la ressource protégée.

Importante

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 qui autorise la récursivité, on peut dire ce qui suit sur les modes dans lesquels 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 de passer en mode écriture est bloquée 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 activé, une 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 saisi, lu, mise à jour, 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 dans le verrou en mode lecture à l’aide des méthodes EnterReadLock ou TryEnterReadLock, ou en rétrogradant du mode améliorable.

  • 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 en mode pouvant être mis à niveau est décrit dans les notes de bas de page du tableau.) La première ligne décrit l'état initial 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 dans (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 bloque.
t entre en mode pouvant être mis à niveau t entre dans (U). t bloque si les threads attendent le mode d’écriture ou le mode de mise à niveau ; sinon, t entre dans (U). t bloque. t bloque.
t entre en mode d’écriture t entre dans (W). t bloque. t bloque.2 t bloque.

1 Si t démarre en mode de mise à niveau, il 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 démarre en mode d'upgrade, elle bloque lorsqu'il y a des threads en mode lecture. Sinon, il passe en mode écriture. L'état de verrouillage passe à Écriture (E). Lorsque t doit attendre parce qu'il y a des threads en mode lecture, il passe en mode écriture dès que le dernier thread sort du 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 en attente du mode d’écriture.
  • En cas d’échec, un thread en attente du mode de mise à 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 de mise à niveau après la modification de l'état et Lecture (R) sinon, 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 de ReaderWriterLockSlim est plus simple et moins sujette aux erreurs 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 {Task.CurrentId} wrote {itemsWritten} items\n");
                                } ));
      // 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 {Task.CurrentId} read {items} items: {output}\n");
                                     } 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($"   {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