Há alguns problemas fudamentais com a maioria das coleçãoes thread-safe. Enquanto as operações individuais são thread-safe, as operações não são geralmente combináveis. Operações comuns como a verficação da contagem de elementos em uma pilha antes de se retirar um item do seu topo (operação “pop”) são perigosas. Há APIs que tentam combinar operações como o .NET 4’s Coordination Data Structures, mas que acabam disponibilzando métodos desajeitados como o TryDequeue.
Outra tentativa foi observada nas coleções do .NET 1. Ao invés de fazer travas (locks) internos, elas foram expostas através da propriedade SyncRoot. Apesar de SyncRoot continuar sendo o nome padrão para objetos de sincronização, o pattern SyncRoot/Wrapper foi descontinuado no .NET 2..
Então como criar APIs combináveis que são realmente úteis? Jared Parsons propõe que não se exponha a API diretamente. Ao invés disso, deve-se expor todos os métodos através de um objeto temporário que é criado e só está disponível enquanto o código está em posse de uma trava no objeto. Este objeto temporário é a chave (key) na coleção e apenas seu detentor pode conseguir acesso ao seu conteúdo (value).
Aqui está um exemplo de uma fila thread-safe do Jared Parson:
static void Example1(ThreadSafeQueuequeue) {
using (var locked = queue.Lock()) {
if (locked.Count > 0) {
var first = locked.Dequeue();
}
}
}
O objeto chamado locked não é thread-safe por si mesmo e espera-se que os desenvolvedores façam a implementação correta ao utilizá-lo dentro de um bloco using. Mas contanto que eles obedeçam esta regra simples, operações dentro do bloco using serão thread-safe. O Jared comenta sobre isso:
Assim como na maioria das arquiteturas thread-safe, há vários de se utilizar este código incorretamente
Utilizar uma instância de ILockedQueueapós ela ter sido encerrada. Este erro já é considerado um tabu e pode-se confiar no conhecimento dos desenvolvedores e acreditar que ele não será cometido. Além disso, ferramentas de análise estática de código como a FxCop vão indicar isto como um erro. Se o desenvolvedor tiver um pouco mais de rigor, isso pode-se prevenir que ele não acontecerá. Basta simplesmente adicionar um flag “disposed” e verificá-lo na entrada de qualquer método. É possível que o desenvolvedor mantenha os valores internos da coleção, como a contagem entre as chamados de travamento (lock) e utilizá-los mais tarde para fazer suposições imprecisas sobre o estado da coleção. Se o desenvolvedor não descartar a instância do ILockedQueue, ela ficará travada para sempre. Felizmente, o FxCop é capaz de indicar isso como um erro uma vez que é uma implementação da IDisposable. Entretanto, essa verificação não é a prova de falhas. Nada diz ao usuário “por favor, utilize o IlockedQueueapenas por um período curto de tempo”. Uma IDisposable por si só transmite esta idéia, mas certamente isso não é perfeito. A implementação do ILockedQueuenão é thread-safe. Idealmente, os desenvolvedores não deveriam passar instâncias de uma IDisposable entre threads, mas isso é algo a se pensar.