Eu posso'não chegar ao fundo deste erro, porque quando o depurador está ligado, parece que não ocorre. Abaixo está o código.
Este é um servidor WCF em um serviço Windows. O método NotifySubscribers é chamado pelo serviço sempre que há 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 assina, o ID do assinante é adicionado ao dicionário de assinantes, e quando o cliente anula a assinatura, ela é excluída do dicionário. O erro acontece quando (ou depois) um cliente se desinscreve. Parece que na próxima vez que o método NotifySubscribers() for chamado, o loop foreach() falha com o erro na linha de assunto. O método grava o erro no log da aplicação como mostrado no código abaixo. Quando um debugger é anexado e um cliente anula a inscrição, o código executa bem.
Você vê algum problema com este código? Preciso de tornar o dicionário seguro?
[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);
}
}
}
O que provavelmente está acontecendo é que o SignalData está mudando indiretamente o dicionário de assinantes sob a capa durante o loop e levando a essa mensagem. Você pode verificar isso mudando
foreach(Subscriber s in subscribers.Values)
Para
foreach(Subscriber s in subscribers.Values.ToList())
Se eu estiver certo, o problema vai desaparecer.
Calling subscribers.Values.ToList() copia os valores dos subscribers.Values para uma lista separada no início da lista. Nada mais tem acesso a esta lista (ela não tem sequer um nome de variável!), então nada pode modificá-la dentro do laço.
Quando um subscritor anula a inscrição você está mudando o conteúdo da coleção de subscritores durante a enumeração.
Há várias maneiras de corrigir isso, uma delas é mudar o loop para utilizar um .ToList()
explícito:
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
Uma maneira mais eficiente, na minha opinião, é ter outra lista em que você declare que colocou qualquer coisa que seja "para ser removida". Depois de terminar seu loop principal (sem o .ToList()), você faz outro loop sobre a lista "a ser removido", removendo cada entrada à medida que ela acontece. Então na sua aula você adiciona:
private List<Guid> toBeRemoved = new List<Guid>();
Então você muda para:
public void NotifySubscribers(DataRecord sr)
{
toBeRemoved.Clear();
...your unchanged code skipped...
foreach ( Guid clientId in toBeRemoved )
{
try
{
subscribers.Remove(clientId);
}
catch(Exception e)
{
System.Diagnostics.Debug.WriteLine("Unsubscribe Error " +
e.Message);
}
}
}
...your unchanged code skipped...
public void UnsubscribeEvent(Guid clientId)
{
toBeRemoved.Add( clientId );
}
Isto não só resolverá o seu problema, como também o evitará de ter de continuar a criar uma lista a partir do seu dicionário, o que é caro se houver muitos subscritores lá dentro. Assumindo que a lista de assinantes a ser removida em qualquer iteração é inferior ao número total da lista, isto deve ser mais rápido. Mas, claro, sinta-se à vontade para traçar o perfil para ter certeza de que é esse o caso se houver alguma dúvida em sua situação específica de uso.