Usando um hashtable dentro de um Parallel.ForEach?
-
11-09-2019 - |
Pergunta
Eu tenho um loop Parallel.ForEach executando uma operação intensiva no interior do corpo.
A operação pode usar um Hashtable para armazenar os valores, e pode ser reutilizado para outros itens de loop consecutivos. I acrescentar ao Hashtable após a operação intensiva estiver concluída, o próximo item loop pode olhar para cima na tabela de hash e reutilizar o objeto, em vez de executar a operação intensiva novamente.
No entanto, porque eu estou usando Parallel.ForEach há um problema inseguro, causando o Hashtable.Add eo (key) ContainsKey chamadas ir fora de sincronia, como eles podem ser executados em paralelo. Apresentando bloqueios podem causar perf questões.
Aqui está o código de exemplo:
Hashtable myTable = new Hashtable;
Parallel.ForEach(items, (item, loopState) =>
{
// If exists in myTable use it, else add to hashtable
if(myTable.ContainsKey(item.Key))
{
myObj = myTable[item.Key];
}
else
{
myObj = SomeIntensiveOperation();
myTable.Add(item.Key, myObj); // Issue is here : breaks with exc during runtime
}
// Do something with myObj
// some code here
}
Deve haver alguma API, definindo a propriedade dentro da biblioteca TPL, que poderia lidar com esse cenário. Existe?
Solução
Você está procurando System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>
. As novas colecções simultâneas utilizam significativamente melhorada mecanismos de bloqueio e deve executar excellectly em algoritmos paralelos.
Edit: O resultado pode ter esta aparência:
ConcurrentDictionary<T,K> cache = ...;
Parallel.ForEach(items, (item, loopState) =>
{
K value;
if (!cache.TryGetValue(item.Key, out value))
{
value = SomeIntensiveOperation();
cache.TryAdd(item.Key, value);
}
// Do something with value
} );
Palavra de advertência: se os elementos em items
não têm item.Key
único, então SomeIntensiveOperation
poderia ter chamado duas vezes para essa chave. No exemplo, a chave não é passado para SomeIntensiveOperation
, mas isso significa que o código "fazer algo com valor" poderia executar chave / valorA e pares chave / valorB, e apenas um resultado seria ficam armazenados no cache (não necessariamente o um primeiro calculado por SomeIntensiveOperation ambos). Você precisaria de uma fábrica preguiçoso paralelo para lidar com este se é um problema. Além disso, por razões óbvias SomeIntensiveOperation deve ser thread-safe.
Outras dicas
System.Collections. Concurrent namespace acho que você precisa ConcurrentDictionary
Use um ReaderWriterLock, isso tem um bom desempenho para o trabalho que tem muitas leituras e algumas gravações que são de curta duração. Seu problema parece se encaixar esta especificação.
Todas as lidas operações será executado rapidamente e bloquear livre, a única vez que alguém será bloqueado é quando uma gravação está acontecendo, e que escrever é apenas o tempo que for preciso para empurrar algo em um Hashtable.
Eu acho que vou derrubar algum código ...
ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
Hashtable myTable = new Hashtable();
Parallel.ForEach(items, (item, loopState) =>
{
cacheLock.EnterReadLock();
MyObject myObj = myTable.TryGet(item.Key);
cacheLock.ExitReadLock();
// If the object isn't cached, calculate it and cache it
if(myObj == null)
{
myObj = SomeIntensiveOperation();
cacheLock.EnterWriteLock();
try
{
myTable.Add(item.Key, myObj);
}
finally
{
cacheLock.ExitWriteLock();
}
}
// Do something with myObj
// some code here
}
static object TryGet(this Hashtable table, object key)
{
if(table.Contains(key))
return table[key]
else
return null;
}
Não vejo outra escolha correta do que fechaduras uso (mais ou menos explícita) (A sincronizado Hashtable apenas substitui todos os métodos com fechaduras).
Outra opção poderia ser a de permitir que o dicionário para ir fora de sincronia. A condição de corrida não vai corromper o dicionário, ela só vai exigir que o código para fazer alguns cálculos supérfluos. Perfil o código para verificar se o bloqueio ou memoization faltando tem efeitos piores.