Freigeben über


System.Threading.ReaderWriterLockSlim-Klasse

Dieser Artikel enthält ergänzende Hinweise zur Referenzdokumentation für diese API.

Verwenden Sie ReaderWriterLockSlim, um eine Ressource zu schützen, die von mehreren Threads gelesen und jeweils von einem Thread beschrieben wird. ReaderWriterLockSlim bietet mehreren Threads die Möglichkeit, sich im Lesemodus zu befinden, bietet einem Thread die Möglichkeit, sich im Schreibmodus mit exklusivem Besitz des Locks zu befinden, und bietet einem Thread, der über Lesezugriff verfügt, die Möglichkeit, sich im upgradefähigen Lesemodus zu befinden, von dem aus der Thread in den Schreibmodus wechseln kann, ohne seinen Lesezugriff auf die Ressource aufgeben zu müssen.

Hinweis

  • ReaderWriterLockSlim ähnelt ReaderWriterLock, aber es hat vereinfachte Regeln für Rekursion sowie für das Aktualisieren und Herabstufen des Sperrzustands. ReaderWriterLockSlim verhindert viele der Fälle potenzieller Deadlock-Situationen. Darüber hinaus ist die Leistung von ReaderWriterLockSlim deutlich besser als die von ReaderWriterLock. ReaderWriterLockSlim wird für alle Neuentwicklungen empfohlen.
  • ReaderWriterLockSlim ist nicht thread-abort-sicher. Sie sollten sie nicht in einer Umgebung verwenden, in der Threads, die darauf zugreifen, abgebrochen werden können, z. B. .NET Framework. Wenn Sie .NET Core oder .NET 5+ verwenden, sollte dies in Ordnung sein. Abort wird in .NET Core nicht unterstützt und ist in .NET 5 und höheren Versionen veraltet.

Standardmäßig werden neue Instanzen von ReaderWriterLockSlim mit dem LockRecursionPolicy.NoRecursion-Flag erstellt und lassen keine Rekursion zu. Diese Standardrichtlinie wird für alle neuen Entwicklungen empfohlen, da rekursion unnötige Komplikationen einführt und Ihren Code anfälliger für Deadlocks macht. Um die Migration von vorhandenen Projekten, die Monitor oder ReaderWriterLock verwenden, zu vereinfachen, können Sie das LockRecursionPolicy.SupportsRecursion-Flag nutzen, um Instanzen von ReaderWriterLockSlim zu erstellen, die Rekursion zulassen.

Ein Thread kann die Sperre in drei Modi eingeben: Lesemodus, Schreibmodus und upgradefähiger Lesemodus. (Im weiteren Verlauf dieses Themas wird der „aktualisierbare Lesemodus“ als „aktualisierbarer Modus“ bezeichnet und der Ausdruck „in den Modus x eintreten“ dem längeren Ausdruck „in den Lock im Modus x eintreten“ vorgezogen.)

Unabhängig von der Rekursionsrichtlinie kann immer nur ein Thread im Schreibmodus ausgeführt werden. Wenn sich ein Thread im Schreibmodus befindet, kann kein anderer Thread das Lock in irgendeinem Modus betreten. Es kann sich immer nur ein Thread im aktualisierbaren Modus befinden. Eine beliebige Anzahl von Threads kann sich im Lesemodus befinden, und es kann einen Thread im Upgrademodus geben, während sich andere Threads im Lesemodus befinden.

Von Bedeutung

Dieser Typ implementiert die IDisposable Schnittstelle. Nach Abschluss der Verwendung sollten Sie den Typ entweder direkt oder indirekt löschen. Rufen Sie die Methode Dispose in einem try/catch-Block auf, um den Typ direkt zu entsorgen. Verwenden Sie zum indirekten Löschen ein Sprachkonstrukt wie using (in C#) oder Using (in Visual Basic). Weitere Informationen finden Sie im Abschnitt "Verwenden eines Objekts, das IDisposable implementiert" im IDisposable Schnittstellenthema.

ReaderWriterLockSlim hat die Threadaffinität verwaltet; d. h., jedes Thread Objekt muss eigene Methodenaufrufe ausführen, um den Sperrmodus ein- und auszuschalten. Kein Thread kann den Modus eines anderen Threads ändern.

Wenn eine ReaderWriterLockSlim Rekursion nicht zulässt, kann ein Thread, der versucht, die Sperre einzugeben, aus mehreren Gründen blockieren:

  • Ein Thread, der versucht, in den Lesemodus zu wechseln, wird blockiert, wenn Threads vorhanden sind, die auf den Schreibmodus warten, oder wenn ein einzelner Thread im Schreibmodus ist.

    Hinweis

    Die Blockierung neuer Reader, wenn Autoren in der Warteschlange stehen, ist eine Richtlinie zur Fairness von Locks, die Autoren bevorzugt. Die aktuelle Richtlinie hält die Fairness für Reader und Writer im Gleichgewicht, um den Durchsatz in den häufigsten Szenarien zu fördern. Zukünftige Versionen von .NET können neue Fairnessrichtlinien einführen.

  • Ein Thread, der versucht, in den aufwertungfähigen Modus zu wechseln, wird blockiert, wenn bereits ein Thread im aufwertungfähigen Modus ist, wenn Threads darauf warten, in den Schreibmodus zu wechseln, oder wenn nur ein einzelner Thread im Schreibmodus ist.

  • Ein Thread, der versucht, in den Schreibmodus zu wechseln, blockiert, sobald ein Thread in einem der drei Modi aktiv ist.

Sperren upgraden und downgraden

Der Upgrade-Modus ist für Fälle gedacht, in denen ein Thread in der Regel von der geschützten Ressource liest, aber möglicherweise darauf schreiben muss, wenn eine bestimmte Anforderung erfüllt ist. Ein Thread, der einen ReaderWriterLockSlim-Modus im upgradefähigen Zustand betreten hat, hat Lesezugriff auf die geschützte Ressource und kann zum Schreibmodus durch Aufrufen der EnterWriteLock- oder TryEnterWriteLock-Methoden wechseln. Da sich immer nur ein Thread im upgradefähigen Modus befinden kann, kann es beim Upgrade in den Schreibmodus nicht zu einer Blockade kommen, wenn keine Rekursion möglich ist, was die Standardrichtlinie ist.

Von Bedeutung

Unabhängig von der Rekursionsrichtlinie darf ein Thread, der zunächst in den Lesemodus wechselt, nicht in den upgradefähigen Modus oder Schreibmodus wechseln, da dieses Muster wahrscheinlich zu Deadlocks führt. Wenn zum Beispiel zwei Threads im Lesemodus beide versuchen, in den Schreibmodus zu wechseln, kommt es zu einer Blockierung. Der upgradefähige Modus wurde entwickelt, um solche Deadlocks zu vermeiden.

Wenn sich andere Threads im Lesemodus befinden, blockiert der Thread, der das Upgrade durchführt. Während der Thread blockiert ist, werden andere Threads, die versuchen, in den Lesemodus zu wechseln, blockiert. Wenn alle Threads den Lesemodus verlassen haben, geht der blockierte aktualisierbare Thread in den Schreibmodus über. Wenn andere Threads auf den Schreibmodus warten, bleiben sie blockiert, da der einzelne Thread, der sich im upgradefähigen Modus befindet, verhindert, dass sie exklusiven Zugriff auf die Ressource erhalten.

Wenn der Thread im upgradefähigen Modus den Schreibmodus verlässt, können andere Threads, die auf den Lesemodus warten, dies tun, es sei denn, es gibt Threads, die auf den Schreibmodus warten. Der Thread im upgradefähigen Modus kann unbegrenzt aktualisiert und heruntergestuft werden, solange es sich um den einzigen Thread handelt, der in die geschützte Ressource schreibt.

Von Bedeutung

Wenn Sie zulassen, dass mehrere Threads in den Schreibmodus oder in den upgradefähigen Modus wechseln können, dürfen Sie nicht zulassen, dass ein Thread den upgradefähigen Modus monopolisiert. Andernfalls werden Threads, die versuchen, den Schreibmodus direkt einzugeben, unbegrenzt blockiert, und während sie blockiert werden, können andere Threads nicht in den Lesemodus wechseln.

Ein Thread im upgradefähigen Modus kann einen Downgrade in den Lesemodus durchführen, indem zuerst die EnterReadLock Methode aufgerufen und dann die ExitUpgradeableReadLock Methode aufgerufen wird. Dieses Downgrade-Muster ist für alle Richtlinien zum Sperren von Sperren erlaubt, auch für NoRecursion.

Nach dem Herunterstufen in den Lesemodus kann ein Thread nicht in den upgradefähigen Modus zurückkehren, bevor er den Lesemodus verlassen hat.

Das Lock rekursiv betreten

Sie können ein ReaderWriterLockSlim erstellen, das die rekursive Sperreingabe unterstützt, indem Sie den ReaderWriterLockSlim(LockRecursionPolicy)-Konstruktor verwenden, der die Sperrrichtlinie angibt, und LockRecursionPolicy.SupportsRecursion spezifizieren.

Hinweis

Die Verwendung von Rekursionen wird für die neue Entwicklung nicht empfohlen, da sie unnötige Komplikationen einführt und Ihren Code anfälliger für Deadlocks macht.

Für ein ReaderWriterLockSlim Element, das rekursion zulässt, kann Folgendes über die Modi gesagt werden, die ein Thread eingeben kann:

  • Ein Thread im Lesemodus kann rekursiv in den Lesemodus wechseln, aber nicht in den Schreibmodus oder in den Upgrademodus wechseln. Wenn er dies versucht, wird ein LockRecursionException ausgelöst. Wenn Sie in den Lesemodus wechseln und dann in den Schreib- oder Upgrademodus wechseln, handelt es sich um ein Muster mit einer starken Wahrscheinlichkeit von Deadlocks, sodass sie nicht zulässig ist. Wie bereits erwähnt, wird der upgradefähige Modus für Fälle bereitgestellt, in denen ein Upgrade einer Sperre erforderlich ist.

  • Ein Thread im upgradefähigen Modus kann in den Schreib- und/oder Lesemodus wechseln und einen der drei Modi rekursiv eingeben. Ein Versuch, in den Schreibmodus zu wechseln, blockiert jedoch, wenn andere Threads im Lesemodus vorhanden sind.

  • Ein Thread im Schreibmodus kann in den Lese- und/oder Upgrademodus wechseln und einen der drei Modi rekursiv eingeben.

  • Ein Thread, der die Sperre nicht eingegeben hat, kann in einen beliebigen Modus wechseln. Dieser Versuch kann aus denselben Gründen wie ein Versuch, eine nicht rekursive Sperre einzugeben, blockieren.

Ein Thread kann die modi beenden, die er in beliebiger Reihenfolge eingegeben hat, solange er jeden Modus genau so oft beendet, wie er in diesen Modus gelangt ist. Wenn ein Thread versucht, einen Modus zu oft zu beenden oder einen nicht eingegebenen Modus zu beenden, wird ein SynchronizationLockException Fehler ausgelöst.

Sperrstatus

Es kann nützlich sein, sich das Lock in Form seiner Status vorzustellen. Ein ReaderWriterLockSlim kann sich in einem von vier Zuständen befinden: nicht eingegeben, Lesen, Upgrade und Schreiben.

  • Nicht eingetreten: In diesem Zustand sind keine Threads in die Sperre eingetreten (oder alle Threads haben die Sperre verlassen).

  • Lesen: In diesem Status sind ein oder mehrere Threads in die Sperre eingetreten, um lesend auf die geschützte Ressource zuzugreifen.

    Hinweis

    Ein Thread kann die Sperre im Lesemodus betreten, indem er die Methoden EnterReadLock oder TryEnterReadLock verwendet oder indem er vom upgradefähigen Modus herunterstuft.

  • Upgrade: In diesem Zustand hat ein Thread die Sperre für den Lesezugriff mit der Option zum Upgrade auf Schreibzugriff (d. h. im upgradefähigen Modus) eingegeben, und null oder mehr Threads haben die Sperre für den Lesezugriff eingegeben. Nicht mehr als ein Thread kann zu einem bestimmten Zeitpunkt die Sperre mit der Option zum Upgrade betreten. Zusätzliche Threads, die versuchen, in den Upgrademodus zu wechseln, werden blockiert.

  • Schreiben: In diesem Zustand hat ein Thread die Sperre für den Schreibzugriff auf die geschützte Ressource eingegeben. Dieser Thread ist im exklusiven Besitz des Locks. Jeder andere Thread, der aus irgendeinem Grund versucht, das Lock zu sperren, wird blockiert.

In der folgenden Tabelle werden die Übergänge zwischen Sperreszuständen für Sperren beschrieben, die keine Rekursion zulassen, wenn ein Thread t die in der spalte ganz links beschriebene Aktion ausführt. Zum Zeitpunkt, an dem die Aktion ausgeführt wird, hat t keinen Modus. (Der Sonderfall, in dem t sich im aktualisierbaren Modus befindet, wird in den Tabellenfußnoten beschrieben.) In der obersten Zeile wird der Startzustand der Sperre beschrieben. Die Zellen beschreiben, was mit dem Thread geschieht, und zeigen in Klammern Änderungen des Status der Sperre an.

Übergang Nicht eingegeben (N) Lesen (Read, R) Upgrade (U) Schreiben (Write, W)
t Wechselt in den Lesemodus t betritt (R). t blockiert, wenn Threads auf den Schreibmodus warten; andernfalls tritt t ein. t blockiert, wenn Threads auf den Schreibmodus warten; andernfalls wird t betreten.1 t blockiert.
t Wechselt in den upgradefähigen Modus t betritt (U). t blockiert, wenn Threads auf den Schreibmodus oder den Upgrade-Modus warten; andernfalls tritt t in (U) ein. t blockiert. t blockiert.
t Wechselt in den Schreibmodus t betritt (W). t blockiert. t blockiert.2 t blockiert.

1 Wenn t im upgradefähigen Modus startet, wechselt es in den Lesemodus. Diese Aktion blockiert nie. Der Sperrzustand ändert sich nicht. (Der Thread kann dann ein Downgrade zum Lesemodus abschließen, indem er den upgradefähigen Modus beendet.)

2 Wenn t im upgradefähigen Modus startet, blockiert es, wenn Threads im Lesemodus vorhanden sind. Andernfalls wird in den Schreibmodus gewechselt. Der Status des Locks ändert sich auf Write (W). Wenn t blockiert, weil sich Threads im Lesemodus befinden, wechselt er in den Schreibmodus, sobald der letzte Thread den Lesemodus verlässt, selbst wenn Threads warten, um in den Schreibmodus zu wechseln.

Wenn eine Zustandsänderung auftritt, weil ein Thread die Sperre verlässt, wird der nächste zu weckende Thread wie folgt ausgewählt:

  • Zuerst wartet ein Thread auf den Schreibmodus und befindet sich bereits im Upgrademodus (es kann höchstens einen solchen Thread geben).
  • Andernfalls ein Thread, der auf den Schreibmodus wartet.
  • Wenn dies nicht der Fall ist, ein Thread, der auf den Upgrade-Modus wartet.
  • Andernfalls alle Threads, die auf den Lesemodus warten.

Der nachfolgende Status des Locks ist in den ersten beiden Fällen immer Write (W) und im dritten Fall Upgrade (U), unabhängig davon, in welchem Status sich das Lock befand, als der austretende Thread die Statusänderung auslöste. Im letzten Fall ist der Status der Sperre Upgrade (U), wenn sich nach der Statusänderung ein Thread im upgradefähigen Modus befindet, und ansonsten Read (R), unabhängig vom vorherigen Status.

Beispiele

Das folgende Beispiel zeigt einen einfachen synchronisierten Cache, der Zeichenfolgen mit ganzzahligen Schlüsseln enthält. Eine Instanz von ReaderWriterLockSlim wird verwendet, um den Zugriff auf den Dictionary<TKey,TValue> internen Cache zu synchronisieren.

Das Beispiel enthält einfache Methoden zum Hinzufügen zum Cache, Löschen aus dem Cache und Lesen aus dem Cache. Zum Veranschaulichen von Timeouts enthält das Beispiel eine Methode, die dem Cache nur hinzugefügt wird, wenn dies innerhalb eines angegebenen Timeouts möglich ist.

Um den upgradefähigen Modus zu veranschaulichen, enthält das Beispiel eine Methode, die den einem Schlüssel zugeordneten Wert abruft und ihn mit einem neuen Wert vergleicht. Wenn der Wert unverändert ist, gibt die Methode einen Status zurück, der keine Änderung angibt. Wenn kein Wert für den Schlüssel gefunden wird, wird das Schlüssel-Wert-Paar eingefügt. Wenn sich der Wert geändert hat, wird er aktualisiert. Der upgradefähige Modus ermöglicht es dem Thread, von Lesezugriff auf Schreibzugriff nach Bedarf zu aktualisieren, ohne dass Das Risiko von Deadlocks besteht.

Das Beispiel enthält eine geschachtelte Enumeration, die die Rückgabewerte für die Methode angibt, die den upgradefähigen Modus veranschaulicht.

Im Beispiel wird der parameterlose Konstruktor zum Erstellen der Sperre verwendet, sodass rekursion nicht zulässig ist. Die ReaderWriterLockSlim Programmierung ist einfacher und weniger anfällig für Fehler, wenn die Sperre keine Rekursion zulässt.

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

Der folgende Code verwendet dann das SynchronizedCache Objekt, um ein Wörterbuch mit Gemüsenamen zu speichern. Es erstellt drei Aufgaben. Die erste schreibt die Namen der in einem Array gespeicherten Gemüse in eine SynchronizedCache-Instanz. Die zweite und dritte Aufgabe zeigt die Namen der Gemüse an, die erste in aufsteigender Reihenfolge (vom niedrigen Index zum hohen Index), die zweite in absteigender Reihenfolge. Die letzte Aufgabe sucht nach der Zeichenfolge „Gurke“ und ruft, wenn sie sie findet, die Methode EnterUpgradeableReadLock auf, um die Zeichenfolge „grüne Bohne“ zu ersetzen.

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