Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Este artigo fornece comentários complementares à documentação de referência para esta API.
Use ReaderWriterLockSlim para proteger um recurso lido por vários subprocessos e que é escrito por um subprocesso de cada vez. ReaderWriterLockSlim permite que vários threads estejam no modo de leitura, um thread esteja no modo de gravação com propriedade exclusiva do bloqueio e um thread que tenha acesso de leitura esteja no modo de leitura atualizável. Nesse modo, o thread pode atualizar para o modo de gravação sem precisar abrir mão do acesso de leitura ao recurso.
Observação
- ReaderWriterLockSlim é semelhante a ReaderWriterLock, mas tem regras simplificadas para recursão e para atualizar e rebaixar o estado de bloqueio. ReaderWriterLockSlim evita muitos casos de possível deadlock. Além disso, o desempenho de ReaderWriterLockSlim é significativamente melhor do que o de ReaderWriterLock. O ReaderWriterLockSlim é recomendado para todos os novos desenvolvimentos.
- ReaderWriterLockSlim não é thread-abort safe. Você não deve usá-lo em um ambiente em que os threads que o acessam possam ser anulados, como o .NET Framework. Se você estiver usando o .NET Core ou o .NET 5+, tudo bem. Abort não tem suporte no .NET Core e está obsoleto no .NET 5 e versões posteriores.
Por padrão, novas instâncias de ReaderWriterLockSlim são criadas com a flag LockRecursionPolicy.NoRecursion e não permitem recursão. Essa política padrão é recomendada para todo o novo desenvolvimento, pois 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 sinalizador LockRecursionPolicy.SupportsRecursion para criar instâncias de ReaderWriterLockSlim que permitem a recursão.
Um thread pode inserir o 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 "entrar no modo x" é usada em preferência à frase mais longa "entrar no bloqueio no modo x".)
Independentemente da política de recursão, apenas uma thread pode estar no modo de gravação em qualquer momento. Quando um thread está no modo de gravação, nenhum outro thread pode inserir o bloqueio em nenhum modo. Somente 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 IDisposable interface. Quando terminar de usar esse tipo ou objeto, você deverá descartá-lo de forma direta ou indireta. 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 (no Visual Basic). Para obter mais informações, consulte a seção "Usando um objeto que implementa idisposable" no tópico da IDisposable interface.
ReaderWriterLockSlim possui afinidade de thread gerenciada; ou seja, cada objeto Thread 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 ReaderWriterLockSlim não permitir a recursão, um thread que tenta entrar no bloqueio poderá ser bloqueado por vários motivos:
Um thread que tenta entrar no modo de leitura será bloqueado 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 gravadores estão na fila é uma política de justiça de bloqueio que favorece gravadores. A política atual de equidade equilibra a justiça para leitores e escritores, para aumentar a eficiência nos cenários mais comuns. Versões futuras do .NET podem introduzir novas políticas de imparcialidade.
Um thread que tenta entrar no modo atualizável ficará bloqueado 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 é bloqueado se já houver um thread em qualquer um dos três modos.
Atualizar e fazer downgrade de bloqueios
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 no modo atualizável tem acesso de leitura ao recurso protegido e pode atualizar para o modo de gravação chamando os métodos EnterWriteLock ou TryEnterWriteLock. Como pode haver apenas um thread no modo atualizável de cada vez, a atualização para o modo de gravação não pode ser um deadlock quando a recursão não é permitida, que é a política padrão.
Importante
Independentemente da política de recursão, um thread que inicialmente entrou no modo de leitura não tem permissão para atualizar para o modo atualizável ou o modo de gravação, pois 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, ocorrerá um deadlock. O modo atualizável foi projetado para evitar tais deadlocks.
Se houver outros threads no modo de leitura, o thread que está tentando atualizar fica bloqueado. Enquanto o thread está bloqueado, outros threads que tentam entrar no modo de leitura são bloqueados. Depois que todos os threads tiverem saído do modo de leitura, o thread atualizável que estava bloqueado entrará no modo de gravação. Se houver outras tarefas aguardando para entrar no modo de gravação, elas permanecerão bloqueadas, pois a única tarefa que está no modo atualizável as impede de obter 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 ser atualizado e rebaixado indefinidamente, desde que seja o único thread que grava em um 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.
Uma thread no modo atualizável pode rebaixar para o modo de leitura primeiro chamando o método EnterReadLock e depois chamando o método ExitUpgradeableReadLock. Esse padrão de downgrade é permitido mesmo para todas as políticas de recursão de bloqueio, incluindo NoRecursion.
Depois de fazer downgrade para o modo de leitura, um thread não poderá reentrar no modo passível de atualização até que tenha saído do modo de leitura.
Entrar no bloqueio recursivamente
Você pode criar um ReaderWriterLockSlim que dê suporte à entrada de bloqueio recursivo usando o ReaderWriterLockSlim(LockRecursionPolicy) construtor que especifica a política de bloqueio e especificando LockRecursionPolicy.SupportsRecursion.
Observação
O uso da recursão não é recomendado para um novo desenvolvimento, pois introduz complicações desnecessárias e torna seu código mais propenso a deadlocks.
Para um ReaderWriterLockSlim que permite a recursão, o seguinte pode ser dito sobre os modos que um thread pode inserir:
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 será lançado. Entrar no modo de leitura e depois no modo de gravação ou de atualização é um padrão com uma forte probabilidade de deadlocks, por isso não é permitido. Conforme discutido anteriormente, o modo atualizável é fornecido para 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 inserir qualquer um dos três modos recursivamente. No entanto, uma tentativa de entrar no modo de gravação será bloqueada 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 inserir qualquer um dos três modos recursivamente.
Um thread que não entrou no bloqueio pode entrar em qualquer modo. Essa tentativa pode ser bloqueada pelos mesmos motivos que uma tentativa de inserir um bloqueio não recursivo.
Uma thread pode sair dos modos nos quais entrou em qualquer ordem, desde que saia de cada modo tantas vezes quanto entrou nesse modo. Se um thread tentar sair de um modo muitas vezes ou sair de um modo que ele não entrou, um SynchronizationLockException será lançado.
Estados de bloqueio
Você pode achar melhor pensar no bloqueio em termos de estados. Um ReaderWriterLockSlim pode estar em um dos quatro estados: não inserido, lido, atualizado e gravado.
Não inserido: nesse estado, nenhum thread inseriu o bloqueio (ou todos os threads saíram do bloqueio).
Leitura: 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 métodos EnterReadLock ou TryEnterReadLock, ou fazendo um downgrade do modo atualizável.
Atualização: nesse estado, um thread inseriu o bloqueio para acesso de leitura com a opção de atualizar para o 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 que um thread de cada vez pode acessar o bloqueio com a opção de atualização disponível; threads adicionais que tentam acessar o modo atualizável são bloqueados.
Gravação: nesse estado, um thread inseriu o bloqueio para acesso de gravação ao recurso protegido. Esse thread tem posse exclusiva do bloqueio. Qualquer outro thread que tenta inserir o bloqueio por qualquer motivo é bloqueado.
A tabela a seguir descreve as transições entre estados de bloqueio, para bloqueios que não permitem a recursão, quando um thread t executa a ação descrita na coluna mais à esquerda. No momento em que ele executa 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, entre parênteses, as alterações no estado de bloqueio.
| Transição | Não inserido (N) | Ler (R) | Atualização (U) | Gravar (W) |
|---|---|---|---|---|
t entra no modo de leitura |
t insere (R). |
t bloqueia se os threads estiverem aguardando pelo modo de gravação; caso contrário, t entra. |
t bloqueia se os threads estiverem aguardando pelo modo de gravação; caso contrário, t entra.1 |
t bloqueia. |
t entra no modo atualizável |
t insere (U). |
t bloqueia se um thread estiver aguardando o modo de gravação ou o modo de atualização; caso contrário, t insere (U). |
t bloqueia. |
t bloqueia. |
t entra no modo de gravação |
t insere (W). |
t bloqueia. |
t bloqueia.2 |
t bloqueia. |
1 Se t começar no modo atualizável, ele entrará no modo de leitura. Essa ação nunca bloqueia. O estado de bloqueio não é alterado. (O thread pode concluir um downgrade para o modo de leitura ao sair do modo atualizável.)
2 Se t começar no modo atualizável, ele bloqueará se houver threads no modo de leitura. Caso contrário, ele mudará para o modo de gravação. O estado de bloqueio é alterado para Gravação (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 que haja 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 existir no máximo um desses threads).
- Caso contrário, um thread que está esperando o modo de gravação.
- Caso contrário, um thread que está esperando o modo atualizável.
- Falhando nisso, todos os threads que estão aguardando o modo de leitura.
O estado subsequente do bloqueio é sempre Gravação (W) nos dois primeiros casos e Atualização (U) no terceiro caso, independentemente do estado do bloqueio quando o thread de saída disparou a alteração de estado. No último caso, o estado do bloqueio será Atualização (U) se houver um thread no modo atualizável após a alteração de estado e Leitura (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 inteiros. Uma instância de ReaderWriterLockSlim é usada para sincronizar o acesso ao Dictionary<TKey,TValue> que serve como cache interno.
O exemplo inclui métodos simples para adicionar ao cache, excluir do cache e ler do cache. Para demonstrar tempos limite, o exemplo inclui um método que adiciona ao cache somente se ele 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 estiver inalterado, o método retornará um status que indica 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 risco de deadlocks.
O exemplo inclui uma enumeração aninhada que especifica os valores retornados 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. A programação ReaderWriterLockSlim é mais simples e menos propensa a erros quando o bloqueio não permite a 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
Em seguida, o código a seguir usa o SynchronizedCache objeto para armazenar um dicionário de nomes vegetais. Ele cria três tarefas. O primeiro grava em uma instância SynchronizedCache os nomes dos vegetais armazenados em uma matriz. A segunda e a terceira tarefa exibem os nomes dos vegetais, o primeiro em ordem crescente (do índice baixo ao alto índice), o segundo em ordem decrescente. A tarefa final procura a cadeia de caracteres "pepino" e, quando a encontra, chama o EnterUpgradeableReadLock método para substituir a 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 {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