Não consigo chegar ao fundo desse erro, porque quando o depurador é anexado, ele não parece ocorrer. Abaixo está o código.
Este é um servidor WCF em um serviço do Windows. O método NotifySubscribers é chamado pelo serviço sempre que houver um evento de dados (em intervalos aleatórios, mas não com muita frequência - cerca de 800 vezes por dia).
Quando um cliente Windows Forms se inscreve, o ID do assinante é adicionado ao dicionário de assinantes e, quando o cliente cancela a inscrição, é excluído do dicionário. O erro ocorre quando (ou depois) um cliente cancela a inscrição. Parece que na próxima vez que o método NotifySubscribers () for chamado, o loop foreach () falhará com o erro na linha de assunto. O método grava o erro no log do aplicativo, conforme mostrado no código abaixo. Quando um depurador é anexado e um cliente cancela a inscrição, o código é executado corretamente.
Você vê algum problema com este código? Preciso tornar o dicionário seguro para threads?
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SubscriptionServer : ISubscriptionServer
{
private static IDictionary<Guid, Subscriber> subscribers;
public SubscriptionServer()
{
subscribers = new Dictionary<Guid, Subscriber>();
}
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values)
{
try
{
s.Callback.SignalData(sr);
}
catch (Exception e)
{
DCS.WriteToApplicationLog(e.Message,
System.Diagnostics.EventLogEntryType.Error);
UnsubscribeEvent(s.ClientId);
}
}
}
public Guid SubscribeEvent(string clientDescription)
{
Subscriber subscriber = new Subscriber();
subscriber.Callback = OperationContext.Current.
GetCallbackChannel<IDCSCallback>();
subscribers.Add(subscriber.ClientId, subscriber);
return subscriber.ClientId;
}
public void UnsubscribeEvent(Guid clientId)
{
try
{
subscribers.Remove(clientId);
}
catch(Exception e)
{
System.Diagnostics.Debug.WriteLine("Unsubscribe Error " +
e.Message);
}
}
}
fonte
Respostas:
O que provavelmente está acontecendo é que o SignalData está indiretamente alterando o dicionário de assinantes sob o capô durante o loop e levando a essa mensagem. Você pode verificar isso alterando
Para
Se eu estiver certo, o problema desaparecerá
A chamada
subscribers.Values.ToList()
copia os valores desubscribers.Values
para uma lista separada no início doforeach
. Nada mais tem acesso a essa lista (ele nem sequer tem um nome de variável!); Portanto, nada pode modificá-lo dentro do loop.fonte
subscribers.Values
está sendo modificado dentro doforeach
loop. A chamadasubscribers.Values.ToList()
copia os valores desubscribers.Values
para uma lista separada no início doforeach
. Nada mais tem acesso a esta lista (nem sequer tem um nome de variável!) , Portanto, nada pode modificá-la dentro do loop.ToList
também pode ser lançado se a coleção for modificada enquantoToList
estiver em execução.ToList
não é uma operação atômica. O que é ainda mais engraçado:ToList
basicamente faz o seu próprioforeach
internamente para copiar itens para uma nova instância da lista, o que significa que você corrigiu umforeach
problema adicionando umaforeach
iteração adicional (embora mais rápida) .Quando um assinante cancela a inscrição, você está alterando o conteúdo da coleção de Assinantes durante a enumeração.
Existem várias maneiras de corrigir isso, sendo uma delas alterar o loop for para usar um explícito
.ToList()
:fonte
Uma maneira mais eficiente, na minha opinião, é ter outra lista na qual você declara colocar tudo o que "deve ser removido". Depois que você terminar o loop principal (sem o .ToList ()), faça outro loop na lista "a ser removido", removendo cada entrada conforme ela ocorrer. Então na sua turma você adiciona:
Então você o altera para:
Isso não apenas resolverá o seu problema, como também impedirá que você continue criando uma lista do seu dicionário, o que é caro se houver muitos assinantes. Supondo que a lista de assinantes a ser removida em qualquer iteração fornecida seja menor que o número total na lista, isso deve ser mais rápido. Mas, é claro, sinta-se à vontade para criar um perfil para garantir que esse seja o caso, se houver alguma dúvida em sua situação específica de uso.
fonte
Você também pode bloquear o dicionário de inscritos para impedir que seja modificado sempre que houver um loop:
fonte
MarkerFrequencies
dicionário dentro do próprio bloqueio, o que significa que a instância original não está mais bloqueada. Tente também usar a emfor
vez de aforeach
, veja isto e isto . Deixe-me saber se isso resolve.System.Collections.Concurrent
espaço para nome.Por que esse erro?
Em geral, as coleções .Net não oferecem suporte para serem enumeradas e modificadas ao mesmo tempo. Se você tentar modificar a lista de coleções durante a enumeração, isso gera uma exceção. Portanto, o problema por trás desse erro é que não podemos modificar a lista / dicionário enquanto percorremos o mesmo.
Uma das soluções
Se iterarmos um dicionário usando uma lista de suas chaves, em paralelo, podemos modificar o objeto de dicionário, pois estamos repetindo a coleção de chaves e não o dicionário (e repetindo sua coleção de chaves).
Exemplo
Aqui está um post sobre esta solução.
E para um mergulho profundo no StackOverflow: Por que esse erro ocorre?
fonte
Na verdade, o problema me parece que você está removendo elementos da lista e esperando continuar a ler a lista como se nada tivesse acontecido.
O que você realmente precisa fazer é começar do fim e voltar ao início. Mesmo se você remover elementos da lista, poderá continuar a lê-lo.
fonte
InvalidOperationException - Ocorreu uma InvalidOperationException. Ele informa que uma "coleção foi modificada" em um loop foreach
Use a instrução break, uma vez que o objeto é removido.
ex:
fonte
Eu tive o mesmo problema e foi resolvido quando usei um
for
loop em vez deforeach
.fonte
Ok, então o que me ajudou foi iterar para trás. Eu estava tentando remover uma entrada de uma lista, mas iterando para cima e estragou o loop porque a entrada não existia mais:
fonte
Eu já vi muitas opções para isso, mas para mim essa foi a melhor.
Em seguida, basta percorrer a coleção.
Esteja ciente de que um ListItemCollection pode conter duplicatas. Por padrão, não há nada que impeça a adição de duplicados à coleção. Para evitar duplicatas, você pode fazer isso:
fonte
A resposta aceita é imprecisa e incorreta no pior dos casos. Se forem feitas alterações durante
ToList()
, você ainda poderá acabar com um erro. Além dissolock
, qual desempenho e segurança de encadeamento precisam ser levados em consideração se você tiver um membro público, uma solução adequada pode ser usar tipos imutáveis .Em geral, um tipo imutável significa que você não pode alterar o estado dele depois de criado. Portanto, seu código deve se parecer com:
Isso pode ser especialmente útil se você tiver um membro público. Outras classes podem sempre
foreach
nos tipos imutáveis sem se preocupar com a coleção que está sendo modificada.fonte
Aqui está um cenário específico que merece uma abordagem especializada:
Dictionary
é enumerado com freqüência.Dictionary
é modificado com pouca frequência.Nesse cenário, criar uma cópia do
Dictionary
(ou doDictionary.Values
) antes de toda enumeração pode ser bastante dispendiosa. Minha idéia sobre como resolver esse problema é reutilizar a mesma cópia em cache em várias enumerações e assistir a umIEnumerator
original emDictionary
busca de exceções. O enumerador será armazenado em cache junto com os dados copiados e interrogado antes de iniciar uma nova enumeração. No caso de uma exceção, a cópia em cache será descartada e uma nova será criada. Aqui está a minha implementação desta ideia:Exemplo de uso:
Infelizmente, esta ideia não pode ser utilizado atualmente com a classe
Dictionary
no .NET núcleo 3.0, porque esta classe não lançar uma coleção foi modificada exceção quando enumerado e os métodosRemove
eClear
são invocados. Todos os outros contêineres que verifiquei estão se comportando de maneira consistente. Eu verifiquei sistematicamente essas classes:List<T>
,Collection<T>
,ObservableCollection<T>
,HashSet<T>
,SortedSet<T>
,Dictionary<T,V>
eSortedDictionary<T,V>
. Somente os dois métodos mencionados daDictionary
classe no .NET Core não estão invalidando a enumeração.Atualização: Corrigi o problema acima comparando também os comprimentos da coleção em cache e da original. Esta solução assume que o dicionário vai ser passada directamente como um argumento para o
EnumerableSnapshot
construtor 's, e a sua identidade não será escondido por (por exemplo) uma projecção como:dictionary.Select(e => e).ΤοEnumerableSnapshot()
.Importante: A classe acima não é segura para threads. Destina-se a ser usado a partir de código executado exclusivamente em um único encadeamento.
fonte
Você pode copiar o objeto de dicionário de assinantes para um mesmo tipo de objeto de dicionário temporário e, em seguida, iterar o objeto de dicionário temporário usando o loop foreach.
fonte
Portanto, uma maneira diferente de resolver esse problema seria, em vez de remover os elementos, criar um novo dicionário e adicionar apenas os elementos que você não deseja remover e substituir o dicionário original pelo novo. Eu não acho que isso seja um problema de eficiência porque não aumenta o número de vezes que você itera sobre a estrutura.
fonte
Há um link onde ele elaborou muito bem e a solução também é fornecida. Experimente se você tiver uma solução adequada, poste aqui para que outros possam entender. Dada solução está ok, então, como o post para que outros possam tentar essas soluções.
para você referenciar o link original: - https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/
Quando usamos classes .Net Serialization para serializar um objeto em que sua definição contém um tipo Enumerable, ou seja, coleção, você receberá facilmente InvalidOperationException dizendo "Coleção foi modificada; operação de enumeração pode não ser executada" onde sua codificação está em cenários de vários segmentos. A causa inferior é que as classes de serialização irão percorrer a coleção via enumerador; assim, o problema é tentar percorrer uma coleção enquanto a modifica.
Primeira solução, podemos simplesmente usar o bloqueio como uma solução de sincronização para garantir que a operação no objeto List possa ser executada apenas a partir de um encadeamento por vez. Obviamente, você receberá uma penalidade de desempenho que, se desejar serializar uma coleção desse objeto, para cada um deles, o bloqueio será aplicado.
Bem, o .Net 4.0, que facilita o manuseio de cenários com vários threads. para esse problema de campo da coleção de serialização, descobri que podemos tirar proveito da classe ConcurrentQueue (Check MSDN), que é uma coleção FIFO e segura para threads e torna o código livre de bloqueios.
Usando esta classe, em sua simplicidade, as coisas que você precisa modificar para o seu código estão substituindo o tipo de Coleção por ele, use Enfileirar para adicionar um elemento ao final do ConcurrentQueue, remova esses códigos de bloqueio. Ou, se o cenário em que você está trabalhando exigir itens de coleção como List, você precisará de mais um código para adaptar o ConcurrentQueue em seus campos.
BTW, ConcurrentQueue não possui um método Clear devido ao algoritmo subjacente que não permite a limpeza atomizada da coleção. para que você faça isso sozinho, a maneira mais rápida é recriar um novo ConcurrentQueue vazio para substituição.
fonte
Pessoalmente, deparei com isso recentemente em código desconhecido, onde estava em um dbcontext aberto com o .Remove (item) do Linq. Tive muita dificuldade em localizar a depuração de erros porque os itens foram removidos na primeira vez em que iteramos e, se eu tentasse dar um passo para trás e refazer a etapa, a coleção estava vazia, pois eu estava no mesmo contexto!
fonte