Fiquei emocionado ao ver o novo System.Collections.Concurrent
espaço para nome no .Net 4.0, muito bom! Eu vi ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
e BlockingCollection
.
Uma coisa que parece estar misteriosamente faltando é a ConcurrentList<T>
. Eu tenho que escrever isso sozinho (ou tirá-lo da web :))?
Estou perdendo algo óbvio aqui?
Respostas:
Eu tentei um tempo atrás (também: no GitHub ). Minha implementação teve alguns problemas, dos quais não vou entrar aqui. Deixe-me contar, mais importante, o que aprendi.
Em primeiro lugar, não há como você obter uma implementação completa
IList<T>
disso sem bloqueio e com segurança para threads. Em particular, inserções e remoções aleatórias não funcionarão, a menos que você também esqueça o acesso aleatório O (1) (ou seja, a menos que você "trapaceie" e use apenas algum tipo de lista vinculada e deixe a indexação ser uma droga).O que eu pensei que poderia ser interessante era um subconjunto thread-safe, limitado de
IList<T>
: em particular, aquele que permitiria que umAdd
e fornecer aleatório somente leitura o acesso por índice (mas nãoInsert
,RemoveAt
etc., e também não há aleatório gravação de acesso).Este foi o objetivo da minha
ConcurrentList<T>
implementação . Mas quando testei seu desempenho em cenários multithread, descobri que simplesmente a sincronização adiciona a umList<T>
era mais rápido . Basicamente, adicionar a umList<T>
já é super rápido; a complexidade das etapas computacionais envolvidas é minúscula (incrementa um índice e atribui a um elemento em uma matriz; é isso mesmo ). Você precisaria de uma tonelada de gravações simultâneas para ver qualquer tipo de contenção de bloqueio sobre isso; e mesmo assim, o desempenho médio de cada gravação ainda superaria a implementação mais cara, embora sem bloqueioConcurrentList<T>
.No caso relativamente raro em que a matriz interna da lista precise se redimensionar, você paga um pequeno custo. Por fim, concluí que esse era o único cenário de nicho em que um
ConcurrentList<T>
tipo de coleção de adição apenas faria sentido: quando você deseja garantir uma baixa sobrecarga de adicionar um elemento em cada chamada (portanto, em oposição a uma meta de desempenho amortizado).Simplesmente não é uma aula tão útil quanto você imagina.
fonte
List<T>
que usos old-skool, sincronização baseado no monitor, não estáSynchronizedCollection<T>
escondido no BCL: msdn.microsoft.com/en-us/library/ms668265.aspxConcurrentList
que uma vitória seria quando não há muita atividade adicionando à lista, mas há muitos leitores simultâneos. Pode-se reduzir a sobrecarga dos leitores a uma única barreira de memória (e eliminar até isso se os leitores não estiverem preocupados com dados levemente obsoletos).ConcurrentList<T>
tal maneira que os leitores garantam um estado consistente sem a necessidade de qualquer bloqueio, com uma sobrecarga adicional relativamente pequena. Quando a lista se expandir, por exemplo, do tamanho 32 para o 64, mantenha a matriz size-32 e crie uma nova matriz size-64. Ao adicionar cada um dos próximos 32 itens, coloque-o no slot 32-63 da nova matriz e copie um item antigo da matriz tamanho 32 para a nova. Até que o 64º item seja adicionado, os leitores procurarão na matriz tamanho 32 os itens 0 a 31 e na matriz tamanho 64 os itens 32 a 63.Para que você usaria uma ConcurrentList?
O conceito de um contêiner de acesso aleatório em um mundo encadeado não é tão útil quanto pode parecer. A declaração
como um todo ainda não seria seguro para threads.
Em vez de criar uma ConcurrentList, tente criar soluções com o que está lá. As classes mais comuns são o ConcurrentBag e, especialmente, o BlockingCollection.
fonte
Monitor
fazer isso de qualquer maneira, não há motivo para uma lista simultânea.Com todo o respeito pelas ótimas respostas já fornecidas, há momentos em que eu simplesmente quero um IList seguro para threads. Nada avançado ou sofisticado. O desempenho é importante em muitos casos, mas às vezes isso simplesmente não é uma preocupação. Sim, sempre haverá desafios sem métodos como "TryGetValue", etc., mas na maioria dos casos eu só quero algo que eu possa enumerar sem precisar se preocupar em bloquear tudo. E sim, alguém provavelmente pode encontrar algum "bug" na minha implementação que possa levar a um impasse ou algo assim (suponho), mas vamos ser honestos: quando se trata de multi-threading, se você não escrever seu código corretamente, ele está entrando em impasse de qualquer maneira. Com isso em mente, decidi fazer uma implementação ConcurrentList simples que forneça essas necessidades básicas.
E pelo que vale: fiz um teste básico de adição de 10.000.000 de itens à List e ConcurrentList regulares e os resultados foram:
Lista finalizada em: 7793 milissegundos. Simultâneo concluído em: 8064 milissegundos.
fonte
RemoveAt(int index)
nuncaInsert(int index, T item)
é seguro para threads, é seguro apenas para o índice == 0, o retorno deIndexOf()
é imediatamente desatualizado etc. Nem comece sobre othis[int]
.ReaderWriterLockSlim
pode ser impaciente facilmente usando-oEnterUpgradeableReadLock()
simultaneamente. No entanto, você não o usa, não torna o bloqueio acessível para o exterior e, por exemplo, não chama um método que insira um bloqueio de gravação enquanto mantém um bloqueio de leitura, portanto, o uso da sua classe não gera mais impasses. provável.var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";
. Em geral, qualquer combinação de leitura e gravação pode causar problemas profundos quando executada simultaneamente. É por isso que as estruturas de dados simultâneos geralmente fornecem métodos para aqueles, comoConcurrentDictionary
'sAddOrGet
etc. NB sua constante (e redundante porque os membros já estão marcadas como tal pelo sublinhado) repetição dethis.
desordens.ConcurrentList
(como uma matriz redimensionável, não uma lista vinculada) não é fácil de escrever com operações sem bloqueio. Sua API não se traduz bem em uma versão "simultânea".fonte
A razão pela qual não existe ConcurrentList é porque fundamentalmente não pode ser gravada. A razão é que várias operações importantes no IList dependem de índices e isso simplesmente não funciona. Por exemplo:
O efeito que o autor está buscando é inserir "cachorro" antes de "gato", mas em um ambiente multithread, qualquer coisa pode acontecer com a lista entre essas duas linhas de código. Por exemplo, outro encadeamento pode ser feito
list.RemoveAt(0)
, deslocando a lista inteira para a esquerda, mas, crucialmente, o catIndex não será alterado. O impacto aqui é que aInsert
operação realmente colocará o "cachorro" atrás do gato, não antes dele.As várias implementações que você vê oferecidas como "respostas" a esta pergunta são bem-intencionadas, mas, como mostra acima, elas não oferecem resultados confiáveis. Se você realmente deseja semântica de lista em um ambiente multithread, não pode chegar lá colocando bloqueios dentro os métodos de implementação lista. Você deve garantir que qualquer índice que você use viva inteiramente dentro do contexto do bloqueio. O resultado é que você pode usar uma Lista em um ambiente multithread com o bloqueio correto, mas a lista em si não pode existir nesse mundo.
Se você acha que precisa de uma lista simultânea, existem realmente apenas duas possibilidades:
Se você tem um ConcurrentBag e está em uma posição em que precisa transmiti-lo como um IList, há um problema, porque o método que você está chamando especificou que eles podem tentar fazer algo como eu fiz acima com o gato & cão. Na maioria dos mundos, o que isso significa é que o método que você está chamando simplesmente não foi criado para funcionar em um ambiente multithread. Isso significa que você o refatora para que seja ou, se não puder, terá que lidar com isso com muito cuidado. Você quase certamente precisará criar sua própria coleção com seus próprios bloqueios e chamar o método incorreto dentro de um bloqueio.
fonte
Nos casos em que as leituras superam em número as gravações ou (por mais freqüentes) que sejam, não são simultâneas , uma cópia na gravação abordagem de pode ser apropriada.
A implementação mostrada abaixo é
var snap = _list; snap[snap.Count - 1];
, nunca (bem, exceto por uma lista vazia, é claro) será lançada, e você também terá uma enumeração segura de segmentos com semântica de snapshots de graça .. como EU AMO a imutabilidade!Para que a cópia na gravação funcione, é necessário manter suas estruturas de dados efetivamente imutáveis , ou seja, ninguém poderá alterá-las depois de disponibilizá-las para outros threads. Quando você deseja modificar, você
Código
Uso
Se você precisar de mais desempenho, ajudará a não gerar o método, por exemplo, crie um método para cada tipo de modificação (Adicionar, Remover, ...) desejado e codifique os ponteiros de função
cloner
eop
.NB # 1 É de sua responsabilidade garantir que ninguém modifique a estrutura de dados (supostamente) imutável. Não há nada que possamos fazer em uma implementação genérica para impedir isso, mas ao se especializar
List<T>
, você pode se proteger contra modificações usando List.AsReadOnly ()Nota # 2 Tenha cuidado com os valores da lista. A abordagem de cópia na gravação acima protege apenas a participação na lista, mas se você não colocar strings, mas alguns outros objetos mutáveis, precisará cuidar da segurança do encadeamento (por exemplo, bloqueio). Mas isso é ortogonal a esta solução e, por exemplo, o bloqueio dos valores mutáveis pode ser facilmente usado sem problemas. Você só precisa estar ciente disso.
Nota # 3 Se sua estrutura de dados é enorme e você a modifica com frequência, a abordagem de copiar tudo na gravação pode ser proibitiva em termos de consumo de memória e no custo de cópia da CPU envolvido. Nesse caso, convém usar as coleções imutáveis da MS .
fonte
System.Collections.Generic.List<t>
já é thread-safe para vários leitores. Tentar torná-lo seguro para vários escritores não faria sentido. (Por razões que Henk e Stephen já mencionaram)fonte
Algumas pessoas sugeriram alguns pontos positivos (e alguns dos meus pensamentos):
Isso não é resposta. São apenas comentários que realmente não se encaixam em um local específico.
... Minha conclusão, a Microsoft precisa fazer algumas alterações profundas no "foreach" para facilitar o uso da coleção MultiThreaded. Também deve seguir as próprias regras de uso do IEnumerator. Até isso, podemos escrever um MultiThreadList facilmente que usaria um iterador de bloqueio, mas que não seguirá "IList". Em vez disso, você terá que definir a interface "IListPersonnal" própria que poderá falhar ao "inserir", "remover" e acessador aleatório (indexador) sem exceção. Mas quem vai querer usá-lo se não for padrão?
fonte
ConcurrentOrderedBag<T>
que inclua uma implementação somente leituraIList<T>
, mas também ofereça umint Add(T value)
método totalmente seguro para threads . Não vejo porForEach
que seriam necessárias alterações. Embora a Microsoft não diga explicitamente, a prática deles sugere que é perfeitamente aceitávelIEnumerator<T>
enumerar o conteúdo da coleção que existia quando ele foi criado; a exceção modificada por coleção é necessária apenas se o enumerador não puder garantir uma operação sem falhas.GetEnumerator
método público deixar uma coleção bloqueada após seu retorno; esses projetos podem facilmente levar a um impasse. AIEnumerable<T>
não fornece nenhuma indicação de se uma enumeração pode ser esperado para completar mesmo se uma recolha é modificado; o melhor que se pode fazer é escrever seus próprios métodos para que eles o façam e ter métodos que aceitemIEnumerable<T>
documentar o fato de que somente será seguroIEnumerable<T>
para threads se o suporte à enumeração segura para threads.IEnumerable<T>
tivesse incluído um método "Instantâneo" com o tipo de retornoIEnumerable<T>
. Coleções imutáveis podem se recuperar; uma coleção limitada poderia se nada mais se copiar para umList<T>
ouT[]
e invocarGetEnumerator
isso. Algumas coleções ilimitadas poderiam ser implementadasSnapshot
, e aquelas que não poderiam seriam capazes de lançar uma exceção sem tentar preencher uma lista com seu conteúdo.Na execução seqüencial de código, as estruturas de dados utilizadas são diferentes do código (bem escrito) de execução simultânea. A razão é que o código seqüencial implica ordem implícita. Código simultâneo, no entanto, não implica nenhum pedido; melhor ainda, implica a falta de qualquer ordem definida!
Devido a isso, estruturas de dados com ordem implícita (como Lista) não são muito úteis para resolver problemas simultâneos. Uma lista implica ordem, mas não define claramente qual é essa ordem. Por esse motivo, a ordem de execução do código que manipula a lista determinará (até certo ponto) a ordem implícita da lista, que está em conflito direto com uma solução simultânea eficiente.
Lembre-se de que a simultaneidade é um problema de dados, não de código! Você não pode implementar o código primeiro (ou reescrever o código sequencial existente) e obter uma solução simultânea bem projetada. Você precisa projetar as estruturas de dados primeiro, mantendo em mente que a ordem implícita não existe em um sistema simultâneo.
fonte
A abordagem de copiar e gravar sem bloqueio funciona muito bem se você não estiver lidando com muitos itens. Aqui está uma aula que escrevi:
exemplo de uso: orders_BUY = CopyAndWriteList.Clear (orders_BUY);
fonte
Eu implementei um semelhante ao de Brian . O meu é diferente:
yield return
para produzir um enumerador.DoSync
eGetSync
métodos que permitem interações sequenciais que requerem acesso exclusivo à lista.O código :
fonte
try
blocoRemove
ou no indexador ao mesmo tempo?IList
semântica em cenários simultâneos é limitada na melhor das hipóteses. Eu escrevi esse código provavelmente antes de chegar a essa conclusão. Minha experiência é a mesma do autor da resposta aceita: tentei com o que sabia sobre sincronização e IList <T> e aprendi algo ao fazer isso.