Класс System.Threading.ReaderWriterLockSlim

В этой статье приводятся дополнительные замечания к справочной документации по этому API.

Используется ReaderWriterLockSlim для защиты ресурса, считываемого несколькими потоками и записываемого в один поток за раз. ReaderWriterLockSlim позволяет нескольким потокам находиться в режиме чтения, позволяет одному потоку находиться в режиме записи с монопольным владением блокировкой и позволяет одному потоку с доступом на чтение находиться в обновляемом режиме чтения, с которого поток может обновиться до режима записи, не удаляя доступ на чтение к ресурсу.

Примечание.

  • ReaderWriterLockSlim действует так же, как и ReaderWriterLock, но с более простыми правилами рекурсии и изменения состояния блокировки. ReaderWriterLockSlim позволяет избежать многих ситуаций взаимоблокировки. Кроме того, производительность ReaderWriterLockSlim значительно выше, чем у ReaderWriterLock. Мы рекомендуем применять ReaderWriterLockSlim при любых новых разработках.
  • ReaderWriterLockSlim не является безопасным потоком. Его нельзя использовать в среде, в которой потоки, обращающиеся к нему, могут быть прерваны, например платформа .NET Framework. Если вы используете .NET Core или .NET 5+, это должно быть хорошо. Abort не поддерживается в .NET Core и устарел в .NET 5 и более поздних версиях.

По умолчанию новые экземпляры ReaderWriterLockSlim создаются с флагом LockRecursionPolicy.NoRecursion и не разрешают рекурсию. Эта политика по умолчанию рекомендуется для всех новых разработок, так как рекурсия представляет ненужные осложнения и делает код более подверженным взаимоблокировкам. Чтобы упростить миграцию из существующих проектов, использующих Monitor или ReaderWriterLock, можно использовать LockRecursionPolicy.SupportsRecursion флаг для создания экземпляров ReaderWriterLockSlim , которые позволяют рекурсии.

Поток может ввести блокировку в трех режимах: режим чтения, режим записи и обновляемый режим чтения. (В остальной части этого раздела "обновляемый режим чтения" называется "обновляемым режимом", а фраза "ввод x в режим" используется в предпочтениях более длинной фразы "ввод блокировки в x режиме".

Независимо от политики рекурсии, только один поток может находиться в режиме записи в любое время. Если поток находится в режиме записи, ни один другой поток не может ввести блокировку в любом режиме. Только один поток может находиться в режиме обновления в любое время. Любое количество потоков может находиться в режиме чтения, и в режиме чтения может быть один поток в обновляемом режиме, а другие потоки находятся в режиме чтения.

Важно!

Этот тип реализует интерфейс IDisposable. По окончании использования выдаленную ему память следует прямо или косвенно освободить. Чтобы сделать это прямо, вызовите его метод Dispose в блоке try/catch. Чтобы сделать это косвенно, используйте языковые конструкции, такие как using (в C#) или Using (в Visual Basic). Дополнительные сведения см. в разделе "Использование объекта, реализующего IDisposable" в статье об интерфейсе IDisposable.

ReaderWriterLockSlim имеет сходство управляемых потоков; То есть каждый Thread объект должен выполнять собственные вызовы метода для ввода и выхода из режима блокировки. Ни один поток не может изменить режим другого потока.

ReaderWriterLockSlim Если рекурсия не разрешена, поток, который пытается ввести блокировку, может блокироваться по нескольким причинам:

  • Поток, который пытается войти в блоки режима чтения, если есть потоки, ожидающие ввода режима записи или один поток в режиме записи.

    Примечание.

    Блокировка новых читателей, когда писатели помещаются в очередь, является политикой справедливости блокировки, которая благоприятствует писателям. Текущая политика справедливости балансирует справедливость для читателей и писателей, чтобы повысить пропускную способность в наиболее распространенных сценариях. Будущие версии .NET могут вводить новые политики справедливости.

  • Поток, который пытается войти в блоки режима обновления, если в режиме обновления уже существует поток, если есть потоки, ожидающие ввода режима записи, или если в режиме записи есть один поток.

  • Поток, который пытается ввести блоки режима записи, если в любом из трех режимов существует поток.

Блокировки обновления и понижения уровня

Режим обновления предназначен для случаев, когда поток обычно считывается из защищенного ресурса, но может потребоваться записать в него, если какое-то условие выполнено. Поток, который вошел ReaderWriterLockSlim в обновляемый режим, имеет доступ на чтение к защищенному ресурсу и может обновляться до режима записи, вызывая EnterWriteLock методы или TryEnterWriteLock методы. Так как одновременно может быть только один поток в обновляемом режиме, обновление до режима записи не может быть взаимоблокировкой, если рекурсия не допускается, что является политикой по умолчанию.

Важно!

Независимо от политики рекурсии, поток, изначально введенный в режим чтения, не может обновляться до обновляемого режима или режима записи, так как этот шаблон создает сильную вероятность взаимоблокировок. Например, если два потока в режиме чтения пытаются войти в режим записи, они будут взаимоблокировкой. Режим обновления предназначен для предотвращения таких взаимоблокировок.

Если в режиме чтения существуют другие потоки, поток, обновляющий блоки. Пока поток заблокирован, другие потоки, которые пытаются войти в режим чтения, блокируются. Когда все потоки вышли из режима чтения, заблокированный обновляемый поток входит в режим записи. Если есть другие потоки, ожидающие ввода режима записи, они остаются заблокированными, так как один поток, который находится в обновляемом режиме, запрещает им получать эксклюзивный доступ к ресурсу.

Когда поток в обновляемом режиме выходит из режима записи, другие потоки, ожидающие ввода режима чтения, могут сделать это, если только потоки не ожидают ввода режима записи. Поток в обновляемом режиме может обновляться и изменяться на неопределенный срок, если это единственный поток, который записывает в защищенный ресурс.

Важно!

Если вы разрешаете нескольким потокам вводить режим записи или обновляемый режим, нельзя разрешить одному потоку монополизировать обновляемый режим. В противном случае потоки, которые пытаются войти в режим записи, будут заблокированы на неопределенный срок, и во время их блокировки другие потоки не смогут войти в режим чтения.

Поток в обновляемом режиме может перейти на режим чтения, сначала вызвав EnterReadLock метод, а затем вызов ExitUpgradeableReadLock метода. Этот шаблон понижения разрешен для всех политик рекурсии блокировки, даже NoRecursion.

После понижения до режима чтения поток не может повторно ввести обновляемый режим, пока он не выйдет из режима чтения.

Введите рекурсивную блокировку

Вы можете создать ReaderWriterLockSlim запись рекурсивной блокировки с помощью конструктора ReaderWriterLockSlim(LockRecursionPolicy) , указывающего политику блокировки, и указать LockRecursionPolicy.SupportsRecursion.

Примечание.

Использование рекурсии не рекомендуется для новой разработки, так как это создает ненужные осложнения и делает код более склонным к взаимоблокировкам.

Для параметра, позволяющего ReaderWriterLockSlim рекурсии, можно сказать следующее о режимах, в которые может входить поток:

  • Поток в режиме чтения может входить в режим чтения рекурсивно, но не может ввести режим записи или режим обновления. Если он пытается сделать это, LockRecursionException создается исключение. Ввод режима чтения, а затем ввод режима записи или обновляемого режима является шаблоном с высокой вероятностью взаимоблокировок, поэтому это не допускается. Как упоминалось ранее, режим обновления предоставляется для случаев, когда необходимо обновить блокировку.

  • Поток в обновляемом режиме может входить в режим записи и (или) режим чтения и может ввести любой из трех режимов рекурсивно. Однако попытка ввести блоки режима записи, если в режиме чтения существуют другие потоки.

  • Поток в режиме записи может входить в режим чтения и (или) обновляемый режим и может вводить любой из трех режимов рекурсивно.

  • Поток, который не вошел в блокировку, может входить в любой режим. Эта попытка может блокироваться по тем же причинам, что и попытка ввести не рекурсивную блокировку.

Поток может выйти из режимов, введенных в любом порядке, если он выходит из каждого режима ровно столько раз, сколько он вошел в этот режим. Если поток пытается выйти из режима слишком много раз или выйти из режима, который он не ввел, SynchronizationLockException создается исключение.

Состояния блокировки

Возможно, вы можете подумать о блокировке с точки зрения его состояний. A ReaderWriterLockSlim может находиться в одном из четырех состояний: не введены, считываются, обновляются и записываются.

  • Не введено: в этом состоянии потоки не вошли в блокировку (или все потоки завершили блокировку).

  • Чтение. В этом состоянии один или несколько потоков ввели блокировку для доступа на чтение к защищенному ресурсу.

    Примечание.

    Поток может ввести блокировку в режиме чтения с помощью EnterReadLock методов или TryEnterReadLock более поздней версии из режима обновления.

  • Обновление. В этом состоянии один поток ввел блокировку для доступа на чтение с возможностью обновления доступа на запись (т. е. в режиме обновления), а ноль или несколько потоков ввели блокировку для доступа на чтение. Не более одного потока за раз может ввести блокировку с параметром обновления; дополнительные потоки, которые пытаются войти в режим обновления, блокируются.

  • Запись. В этом состоянии один поток ввел блокировку для доступа на запись к защищенному ресурсу. Этот поток имеет эксклюзивное владение блокировкой. Любой другой поток, который пытается войти в блокировку по любой причине, заблокирован.

В следующей таблице описываются переходы между состояниями блокировки для блокировок, которые не позволяют рекурсии, когда поток t принимает действие, описанное в левом столбце. В то время, когда действие выполняется, t не имеет режима. (Особый случай, когда t находится в обновляемом режиме, описан в сносках таблицы.) Верхняя строка описывает начальное состояние блокировки. Ячейки описывают, что происходит с потоком, и отображают изменения состояния блокировки в скобках.

Transition Не введен (N) Разрешение на чтение (R) Обновление (U) Разрешение на запись (W)
t введите режим чтения t ввод (R). t блоки, если потоки ожидают режима записи; t в противном случае введите. t блоки, если потоки ожидают режима записи; t в противном случае введите.1 t Блоки.
t введите режим обновления t ввод (U). t блокируется, если потоки ожидают режима записи или режима обновления; t в противном случае введите (U). t Блоки. t Блоки.
t введите режим записи t ввод (W). t Блоки. t Блоки.2 t Блоки.

1 Если t запускается в обновляемом режиме, он входит в режим чтения. Это действие никогда не блокируется. Состояние блокировки не изменяется. (Затем поток может завершить переход на режим чтения, выходя из обновляемого режима.)

2 При t запуске в обновляемом режиме он блокирует наличие потоков в режиме чтения. В противном случае он обновляется до режима записи. Состояние блокировки изменяется на запись (W). Если t блоки из-за наличия потоков в режиме чтения, он вступает в режим записи, как только последний поток выходит из режима чтения, даже если есть потоки, ожидающие ввода режима записи.

Когда происходит изменение состояния, так как поток выходит из блокировки, следующий поток будет выбран следующим образом:

  • Во-первых, поток, ожидающий режима записи и уже в обновляемом режиме (может быть не более одного такого потока).
  • Сбой этого, поток, ожидающий режима записи.
  • Сбой этого, поток, ожидающий обновляемого режима.
  • Сбой этого, все потоки, ожидающие режима чтения.

Последующее состояние блокировки всегда выполняется запись (W) в первых двух случаях и обновление (U) в третьем случае независимо от состояния блокировки, когда выход из потока активировал изменение состояния. В последнем случае состояние блокировки — обновление (U), если после изменения состояния существует поток в режиме обновления, а в противном случае — чтение (R) независимо от предыдущего состояния.

Примеры

В следующем примере показан простой синхронизированный кэш, содержащий строки с целыми ключами. Экземпляр используется для синхронизации ReaderWriterLockSlim доступа к Dictionary<TKey,TValue> внутреннему кэшу.

Пример включает простые методы для добавления в кэш, удаления из кэша и чтения из кэша. Для демонстрации времени ожидания пример включает метод, добавляющий в кэш только в том случае, если он может сделать это в течение указанного времени ожидания.

Для демонстрации обновляемого режима пример включает метод, который извлекает значение, связанное с ключом, и сравнивает его с новым значением. Если значение не изменилось, метод возвращает состояние, указывающее, что изменения не изменяются. Если для ключа не найдено значение, вставляется пара "ключ-значение". Если значение изменилось, оно обновляется. Режим обновления позволяет потоку обновлять доступ на чтение до доступа к записи по мере необходимости без риска взаимоблокировок.

В примере содержится вложенное перечисление, указывающее возвращаемые значения для метода, демонстрирующего режим обновления.

В примере используется конструктор без параметров для создания блокировки, поэтому рекурсия не допускается. ReaderWriterLockSlim Программирование проще и менее подвержено ошибкам, если блокировка не разрешает рекурсию.

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

Следующий код затем использует SynchronizedCache объект для хранения словаря имен овощей. Он создает три задачи. Первый записывает имена овощей, хранящихся в массиве SynchronizedCache , в экземпляр. Вторая и третья задача отображают имена овощей, первый в порядке возрастания (от низкого индекса до высокого индекса), второй в порядке убывания. Последняя задача ищет строку "огурец" и, когда она находит его, вызывает EnterUpgradeableReadLock метод для замены строки "зеленый боб".

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