System.Threading.ReaderWriterLockSlim classe

Este artigo fornece observações complementares à documentação de referência para essa API.

Use ReaderWriterLockSlim para proteger um recurso que é lido por vários threads e gravado por um thread de cada vez. ReaderWriterLockSlim permite que vários threads estejam no modo de leitura, permite que um thread esteja no modo de gravação com propriedade exclusiva do bloqueio e permite que um thread que tenha acesso de leitura esteja no modo de leitura atualizável, a partir do qual o thread pode atualizar para o modo de gravação sem ter que abrir mão de seu acesso de leitura ao recurso.

Observação

Por padrão, novas instâncias de ReaderWriterLockSlim são criadas com o LockRecursionPolicy.NoRecursion sinalizador e não permitem recursão. Essa política padrão é recomendada para todos os novos desenvolvimentos, porque a recursão introduz complicações desnecessárias e torna seu código mais propenso a deadlocks. Para simplificar a migração de projetos existentes que usam Monitor ou ReaderWriterLock, você pode usar o LockRecursionPolicy.SupportsRecursion sinalizador para criar instâncias que ReaderWriterLockSlim permitem a recursão.

Um thread pode entrar no bloqueio em três modos: modo de leitura, modo de gravação e modo de leitura atualizável. (No restante deste tópico, "modo de leitura atualizável" é referido como "modo atualizável", e a frase "modo de entrada" é usada em preferência à frase mais longa "entrar no x modo x de bloqueio".)

Independentemente da política de recursão, apenas um thread pode estar no modo de gravação a qualquer momento. Quando um thread está no modo de gravação, nenhum outro thread pode entrar no bloqueio em qualquer modo. Apenas um thread pode estar no modo atualizável a qualquer momento. Qualquer número de threads pode estar no modo de leitura, e pode haver um thread no modo atualizável enquanto outros threads estão no modo de leitura.

Importante

Esse tipo implementa a interface IDisposable. Quando você terminar de usar o tipo, deverá descartá-lo direta ou indiretamente. Para descartar o tipo diretamente, chame o método Dispose dele em um bloco try/catch. Para descartá-lo indiretamente, use um constructo de linguagem como using ( em C#) ou Using (em Visual Basic). Saiba mais na seção "Como usar um objeto que implementa IDisposable" no tópico da interface IDisposable.

ReaderWriterLockSlim gerenciou afinidade de threads; ou seja, cada Thread objeto deve fazer suas próprias chamadas de método para entrar e sair dos modos de bloqueio. Nenhum thread pode alterar o modo de outro thread.

Se um não permitir recursão, um ReaderWriterLockSlim thread que tenta entrar no bloqueio pode bloquear por vários motivos:

  • Um thread que tenta entrar no modo de leitura bloqueia se houver threads aguardando para entrar no modo de gravação ou se houver um único thread no modo de gravação.

    Observação

    Bloquear novos leitores quando os escritores estão na fila é uma política de justiça de bloqueio que favorece os escritores. A atual política de equidade equilibra a equidade para leitores e escritores, para promover o rendimento nos cenários mais comuns. Versões futuras do .NET podem introduzir novas políticas de equidade.

  • Um thread que tenta entrar no modo atualizável bloqueia se já houver um thread no modo atualizável, se houver threads aguardando para entrar no modo de gravação ou se houver um único thread no modo de gravação.

  • Um thread que tenta entrar no modo de gravação bloqueia se houver um thread em qualquer um dos três modos.

Bloqueios de upgrade e downgrade

O modo atualizável destina-se a casos em que um thread geralmente lê do recurso protegido, mas pode precisar gravar nele se alguma condição for atendida. Um thread que entrou em um ReaderWriterLockSlim modo atualizável tem acesso de leitura ao recurso protegido e pode atualizar para o modo de gravação chamando os EnterWriteLock métodos ou TryEnterWriteLock . Como pode haver apenas um thread no modo atualizável por vez, a atualização para o modo de gravação não pode travar quando a recursão não é permitida, que é a política padrão.

Importante

Independentemente da política de recursão, um thread que entrou inicialmente no modo de leitura não tem permissão para atualizar para o modo atualizável ou modo de gravação, porque esse padrão cria uma forte probabilidade de deadlocks. Por exemplo, se dois threads no modo de leitura tentarem entrar no modo de gravação, eles entrarão em deadlock. O modo atualizável foi projetado para evitar esses bloqueios.

Se houver outros threads no modo de leitura, o thread que está atualizando será bloqueado. Enquanto o thread é bloqueado, outros threads que tentam entrar no modo de leitura são bloqueados. Quando todos os threads tiverem saído do modo de leitura, o thread atualizável bloqueado entrará no modo de gravação. Se houver outros threads aguardando para entrar no modo de gravação, eles permanecerão bloqueados, porque o único thread que está no modo atualizável impede que eles obtenham acesso exclusivo ao recurso.

Quando o thread no modo atualizável sai do modo de gravação, outros threads que estão aguardando para entrar no modo de leitura podem fazer isso, a menos que haja threads aguardando para entrar no modo de gravação. O thread no modo atualizável pode fazer upgrade e downgrade indefinidamente, desde que seja o único thread que grava no recurso protegido.

Importante

Se você permitir que vários threads entrem no modo de gravação ou no modo atualizável, não deverá permitir que um thread monopolize o modo atualizável. Caso contrário, os threads que tentarem entrar no modo de gravação diretamente serão bloqueados indefinidamente e, enquanto estiverem bloqueados, outros threads não poderão entrar no modo de leitura.

Um thread no modo atualizável pode fazer downgrade para o modo de leitura chamando primeiro o método e, em seguida, chamando o EnterReadLockExitUpgradeableReadLock método. Esse padrão de downgrade é permitido para todas as políticas de recursão de bloqueio, mesmo NoRecursion.

Após o downgrade para o modo de leitura, um thread não pode reentrar no modo atualizável até que tenha saído do modo de leitura.

Digite o bloqueio recursivamente

Você pode criar um ReaderWriterLockSlim que ofereça suporte à entrada de bloqueio recursivo usando o construtor que especifica a ReaderWriterLockSlim(LockRecursionPolicy) diretiva de bloqueio e especificando LockRecursionPolicy.SupportsRecursion.

Observação

O uso de recursão não é recomendado para novos desenvolvimentos, pois introduz complicações desnecessárias e torna seu código mais propenso a deadlocks.

Para um que permite a recursão, o seguinte pode ser dito sobre os modos que um ReaderWriterLockSlim thread pode entrar:

  • Um thread no modo de leitura pode entrar no modo de leitura recursivamente, mas não pode entrar no modo de gravação ou no modo atualizável. Se ele tentar fazer isso, um LockRecursionException é jogado. Entrar no modo de leitura e, em seguida, entrar no modo de gravação ou no modo atualizável é um padrão com uma forte probabilidade de deadlocks, portanto, não é permitido. Conforme discutido anteriormente, o modo atualizável é fornecido para os casos em que é necessário atualizar um bloqueio.

  • Um thread no modo atualizável pode entrar no modo de gravação e/ou no modo de leitura e pode entrar em qualquer um dos três modos recursivamente. No entanto, uma tentativa de entrar no modo de gravação bloqueia se houver outros threads no modo de leitura.

  • Um thread no modo de gravação pode entrar no modo de leitura e/ou no modo atualizável e pode entrar em qualquer um dos três modos recursivamente.

  • Um thread que não entrou no bloqueio pode entrar em qualquer modo. Essa tentativa pode bloquear pelos mesmos motivos que uma tentativa de entrar em um bloqueio não recursivo.

Um thread pode sair dos modos que entrou em qualquer ordem, desde que saia de cada modo exatamente quantas vezes entrou nesse modo. Se um thread tentar sair de um modo muitas vezes, ou para sair de um modo que ele não entrou, um SynchronizationLockException é lançado.

Estados de bloqueio

Você pode achar útil pensar no bloqueio em termos de seus estados. A ReaderWriterLockSlim pode estar em um dos quatro estados: não inserido, leitura, atualização e gravação.

  • Não inserido: nesse estado, nenhum thread entrou no bloqueio (ou todos os threads saíram do bloqueio).

  • Leia: nesse estado, um ou mais threads entraram no bloqueio para acesso de leitura ao recurso protegido.

    Observação

    Um thread pode entrar no bloqueio no modo de leitura usando os EnterReadLock métodos ou TryEnterReadLock fazendo downgrade do modo atualizável.

  • Atualização: nesse estado, um thread entrou no bloqueio para acesso de leitura com a opção de atualizar para acesso de gravação (ou seja, no modo atualizável) e zero ou mais threads entraram no bloqueio para acesso de leitura. Não mais de um thread por vez pode entrar no bloqueio com a opção de atualizar; Threads adicionais que tentam entrar no modo atualizável são bloqueados.

  • Gravação: nesse estado, um thread entrou no bloqueio para acesso de gravação ao recurso protegido. Esse fio tem posse exclusiva da fechadura. Qualquer outro thread que tente entrar no bloqueio por qualquer motivo é bloqueado.

A tabela a seguir descreve as transições entre estados de bloqueio, para bloqueios que não permitem recursão, quando um thread t executa a ação descrita na coluna mais à esquerda. Na hora que toma a ação, t não tem modo. (O caso especial em que t está no modo atualizável é descrito nas notas de rodapé da tabela.) A linha superior descreve o estado inicial do bloqueio. As células descrevem o que acontece com o thread e mostram alterações no estado de bloqueio entre parênteses.

Transição Não inserido (N) Ler (R) Atualização (U) Gravar (W)
t entra no modo de leitura t entra (R). t bloqueia se os threads estiverem aguardando o modo de gravação; caso contrário, t entra. t bloqueia se os threads estiverem aguardando o modo de gravação; caso contrário, t entra.1 t Blocos.
t entra no modo atualizável t entra (U). t bloqueia se os threads estiverem aguardando o modo de gravação ou o modo de atualização; caso contrário, t digite (U). t Blocos. t Blocos.
t entra no modo de gravação t entra (W). t Blocos. t Blocos.2 t Blocos.

1 Se t iniciar no modo atualizável, ele entra no modo de leitura. Essa ação nunca bloqueia. O estado de bloqueio não é alterado. (O thread pode então concluir um downgrade para o modo de leitura saindo do modo atualizável.)

2 Se t iniciar no modo atualizável, ele bloqueará se houver threads no modo de leitura. Caso contrário, ele atualiza para o modo de gravação. O estado de bloqueio muda para Gravar (W). Se t bloquear porque há threads no modo de leitura, ele entrará no modo de gravação assim que o último thread sair do modo de leitura, mesmo se houver threads aguardando para entrar no modo de gravação.

Quando ocorre uma alteração de estado porque um thread sai do bloqueio, o próximo thread a ser despertado é selecionado da seguinte maneira:

  • Primeiro, um thread que está aguardando o modo de gravação e já está no modo atualizável (pode haver no máximo um desses threads).
  • Na falta disso, um thread que está aguardando o modo de gravação.
  • Na falta disso, um thread que está aguardando o modo atualizável.
  • Na falha, todos os threads que estão aguardando o modo de leitura.

O estado subsequente do bloqueio é sempre Write (W) nos dois primeiros casos e Upgrade (U) no terceiro caso, independentemente do estado do bloqueio quando o thread de saída acionou a alteração de estado. No último caso, o estado do bloqueio é Upgrade (U) se houver um thread no modo atualizável após a alteração de estado e Read (R) caso contrário, independentemente do estado anterior.

Exemplos

O exemplo a seguir mostra um cache sincronizado simples que contém cadeias de caracteres com chaves inteiras. Uma instância de ReaderWriterLockSlim é usada para sincronizar o Dictionary<TKey,TValue> acesso ao que serve como o cache interno.

O exemplo inclui métodos simples para adicionar ao cache, excluir do cache e ler do cache. Para demonstrar tempos limites, o exemplo inclui um método que adiciona ao cache somente se puder fazer isso dentro de um tempo limite especificado.

Para demonstrar o modo atualizável, o exemplo inclui um método que recupera o valor associado a uma chave e o compara com um novo valor. Se o valor for inalterado, o método retornará um status indicando nenhuma alteração. Se nenhum valor for encontrado para a chave, o par chave/valor será inserido. Se o valor tiver sido alterado, ele será atualizado. O modo atualizável permite que o thread atualize do acesso de leitura para o acesso de gravação, conforme necessário, sem o risco de deadlocks.

O exemplo inclui uma enumeração aninhada que especifica os valores de retorno para o método que demonstra o modo atualizável.

O exemplo usa o construtor sem parâmetros para criar o bloqueio, portanto, a recursão não é permitida. Programar o é mais simples e menos propenso a erros quando o ReaderWriterLockSlim bloqueio não permite recursão.

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

O código a seguir, em seguida, usa o SynchronizedCache objeto para armazenar um dicionário de nomes vegetais. Ele cria três tarefas. O primeiro grava os nomes dos vegetais armazenados em uma matriz em uma SynchronizedCache instância. A segunda e a terceira tarefa exibem os nomes dos vegetais, a primeira em ordem crescente (de baixo índice para alto índice), a segunda em ordem decrescente. A tarefa final procura a cadeia de caracteres "pepino" e, quando a encontra, chama o método para substituir a EnterUpgradeableReadLock cadeia de caracteres "feijão verde".

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks
public class Example
{
   public static void Main()
   {
      var sc = new SynchronizedCache();
      var tasks = new List<Task>();
      int itemsWritten = 0;

      // Execute a writer.
      tasks.Add(Task.Run( () => { String[] vegetables = { "broccoli", "cauliflower",
                                                          "carrot", "sorrel", "baby turnip",
                                                          "beet", "brussel sprout",
                                                          "cabbage", "plantain",
                                                          "spinach", "grape leaves",
                                                          "lime leaves", "corn",
                                                          "radish", "cucumber",
                                                          "raddichio", "lima beans" };
                                  for (int ctr = 1; ctr <= vegetables.Length; ctr++)
                                     sc.Add(ctr, vegetables[ctr - 1]);

                                  itemsWritten = vegetables.Length;
                                  Console.WriteLine("Task {0} wrote {1} items\n",
                                                    Task.CurrentId, itemsWritten);
                                } ));
      // Execute two readers, one to read from first to last and the second from last to first.
      for (int ctr = 0; ctr <= 1; ctr++) {
         bool desc = ctr == 1;
         tasks.Add(Task.Run( () => { int start, last, step;
                                     int items;
                                     do {
                                        String output = String.Empty;
                                        items = sc.Count;
                                        if (! desc) {
                                           start = 1;
                                           step = 1;
                                           last = items;
                                        }
                                        else {
                                           start = items;
                                           step = -1;
                                           last = 1;
                                        }

                                        for (int index = start; desc ? index >= last : index <= last; index += step)
                                           output += String.Format("[{0}] ", sc.Read(index));

                                        Console.WriteLine("Task {0} read {1} items: {2}\n",
                                                          Task.CurrentId, items, output);
                                     } while (items < itemsWritten | itemsWritten == 0);
                             } ));
      }
      // Execute a red/update task.
      tasks.Add(Task.Run( () => { Thread.Sleep(100);
                                  for (int ctr = 1; ctr <= sc.Count; ctr++) {
                                     String value = sc.Read(ctr);
                                     if (value == "cucumber")
                                        if (sc.AddOrUpdate(ctr, "green bean") != SynchronizedCache.AddOrUpdateStatus.Unchanged)
                                           Console.WriteLine("Changed 'cucumber' to 'green bean'");
                                  }
                                } ));

      // Wait for all three tasks to complete.
      Task.WaitAll(tasks.ToArray());

      // Display the final contents of the cache.
      Console.WriteLine();
      Console.WriteLine("Values in synchronized cache: ");
      for (int ctr = 1; ctr <= sc.Count; ctr++)
         Console.WriteLine("   {0}: {1}", ctr, sc.Read(ctr));
   }
}
// The example displays the following output:
//    Task 1 read 0 items:
//
//    Task 3 wrote 17 items
//
//
//    Task 1 read 17 items: [broccoli] [cauliflower] [carrot] [sorrel] [baby turnip] [
//    beet] [brussel sprout] [cabbage] [plantain] [spinach] [grape leaves] [lime leave
//    s] [corn] [radish] [cucumber] [raddichio] [lima beans]
//
//    Task 2 read 0 items:
//
//    Task 2 read 17 items: [lima beans] [raddichio] [cucumber] [radish] [corn] [lime
//    leaves] [grape leaves] [spinach] [plantain] [cabbage] [brussel sprout] [beet] [b
//    aby turnip] [sorrel] [carrot] [cauliflower] [broccoli]
//
//    Changed 'cucumber' to 'green bean'
//
//    Values in synchronized cache:
//       1: broccoli
//       2: cauliflower
//       3: carrot
//       4: sorrel
//       5: baby turnip
//       6: beet
//       7: brussel sprout
//       8: cabbage
//       9: plantain
//       10: spinach
//       11: grape leaves
//       12: lime leaves
//       13: corn
//       14: radish
//       15: green bean
//       16: raddichio
//       17: lima beans
Public Module Example
   Public Sub Main()
      Dim sc As New SynchronizedCache()
      Dim tasks As New List(Of Task)
      Dim itemsWritten As Integer
      
      ' Execute a writer.
      tasks.Add(Task.Run( Sub()
                             Dim vegetables() As String = { "broccoli", "cauliflower",
                                                            "carrot", "sorrel", "baby turnip",
                                                            "beet", "brussel sprout",
                                                            "cabbage", "plantain",
                                                            "spinach", "grape leaves",
                                                            "lime leaves", "corn",
                                                            "radish", "cucumber",
                                                            "raddichio", "lima beans" }
                             For ctr As Integer = 1 to vegetables.Length
                                sc.Add(ctr, vegetables(ctr - 1))
                             Next
                             itemsWritten = vegetables.Length
                             Console.WriteLine("Task {0} wrote {1} items{2}",
                                               Task.CurrentId, itemsWritten, vbCrLf)
                          End Sub))
      ' Execute two readers, one to read from first to last and the second from last to first.
      For ctr As Integer = 0 To 1
         Dim flag As Integer = ctr
         tasks.Add(Task.Run( Sub()
                                Dim start, last, stp As Integer
                                Dim items As Integer
                                Do
                                   Dim output As String = String.Empty
                                   items = sc.Count
                                   If flag = 0 Then
                                      start = 1 : stp = 1 : last = items
                                   Else
                                      start = items : stp = -1 : last = 1
                                   End If
                                   For index As Integer = start To last Step stp
                                      output += String.Format("[{0}] ", sc.Read(index))
                                   Next
                                   Console.WriteLine("Task {0} read {1} items: {2}{3}",
                                                           Task.CurrentId, items, output,
                                                           vbCrLf)
                                Loop While items < itemsWritten Or itemsWritten = 0
                             End Sub))
      Next
      ' Execute a red/update task.
      tasks.Add(Task.Run( Sub()
                             For ctr As Integer = 1 To sc.Count
                                Dim value As String = sc.Read(ctr)
                                If value = "cucumber" Then
                                   If sc.AddOrUpdate(ctr, "green bean") <> SynchronizedCache.AddOrUpdateStatus.Unchanged Then
                                      Console.WriteLine("Changed 'cucumber' to 'green bean'")
                                   End If
                                End If
                             Next
                          End Sub ))

      ' Wait for all three tasks to complete.
      Task.WaitAll(tasks.ToArray())

      ' Display the final contents of the cache.
      Console.WriteLine()
      Console.WriteLine("Values in synchronized cache: ")
      For ctr As Integer = 1 To sc.Count
         Console.WriteLine("   {0}: {1}", ctr, sc.Read(ctr))
      Next
   End Sub
End Module
' The example displays output like the following:
'    Task 1 read 0 items:
'
'    Task 3 wrote 17 items
'
'    Task 1 read 17 items: [broccoli] [cauliflower] [carrot] [sorrel] [baby turnip] [
'    beet] [brussel sprout] [cabbage] [plantain] [spinach] [grape leaves] [lime leave
'    s] [corn] [radish] [cucumber] [raddichio] [lima beans]
'
'    Task 2 read 0 items:
'
'    Task 2 read 17 items: [lima beans] [raddichio] [cucumber] [radish] [corn] [lime
'    leaves] [grape leaves] [spinach] [plantain] [cabbage] [brussel sprout] [beet] [b
'    aby turnip] [sorrel] [carrot] [cauliflower] [broccoli]
'
'    Changed 'cucumber' to 'green bean'
'
'    Values in synchronized cache:
'       1: broccoli
'       2: cauliflower
'       3: carrot
'       4: sorrel
'       5: baby turnip
'       6: beet
'       7: brussel sprout
'       8: cabbage
'       9: plantain
'       10: spinach
'       11: grape leaves
'       12: lime leaves
'       13: corn
'       14: radish
'       15: green bean
'       16: raddichio
'       17: lima beans