Non riesco a venire a capo di questo errore, perché quando il debugger è collegato, non sembra verificarsi. Di seguito è riportato il codice.
Si tratta di un server WCF in un servizio Windows. Il metodo NotifySubscribers viene chiamato dal servizio ogni volta che c'è un evento dati (a intervalli casuali, ma non molto spesso - circa 800 volte al giorno).
Quando un client Windows Forms si iscrive, l'ID del sottoscrittore viene aggiunto al dizionario dei sottoscrittori, e quando il client si disiscrive, viene cancellato dal dizionario. L'errore si verifica quando (o dopo) un cliente si disiscrive. Sembra che la prossima volta che il metodo NotifySubscribers() viene chiamato, il ciclo foreach() fallisce con l'errore nella riga dell'oggetto. Il metodo scrive l'errore nel log dell'applicazione come mostrato nel codice qui sotto. Quando un debugger è collegato e un cliente si disiscrive, il codice viene eseguito correttamente.
Vedete un problema con questo codice? Devo rendere il dizionario thread-safe?
[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);
}
}
}
Quello che probabilmente sta accadendo è che SignalData sta cambiando indirettamente il dizionario degli abbonati sotto il cappuccio durante il ciclo e portando a quel messaggio. Puoi verificarlo cambiando
foreach(Subscriber s in subscribers.Values)
a
foreach(Subscriber s in subscribers.Values.ToList())
Se ho ragione, il problema sparirà
Chiamando subscribers.Values.ToList() si copiano i valori di subscribers.Values in una lista separata all'inizio del foreach. Nient'altro ha accesso a questa lista (non ha nemmeno un nome di variabile!), quindi niente può modificarla all'interno del ciclo.
Quando un abbonato si disiscrive, state cambiando il contenuto della collezione di abbonati durante l'enumerazione.
Ci sono diversi modi per risolvere questo problema, uno dei quali è cambiare il ciclo for per usare un esplicito .ToList()
:
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
Un modo più efficiente, secondo me, è quello di avere un'altra lista che dichiarate in cui mettete tutto ciò che è "da rimuovere". Poi, dopo aver finito il vostro ciclo principale (senza il .ToList()), fate un altro ciclo sulla lista "to be removed", rimuovendo ogni voce come capita. Quindi nella vostra classe aggiungete:
private List<Guid> toBeRemoved = new List<Guid>();
Poi lo cambiate in:
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 );
}
Questo non solo risolverà il tuo problema, ma ti eviterà di dover continuare a creare una lista dal tuo dizionario, il che è costoso se ci sono molti iscritti. Supponendo che l'elenco degli abbonati da rimuovere in ogni iterazione sia inferiore al numero totale nella lista, questo dovrebbe essere più veloce. Ma naturalmente sentitevi liberi di fare un profilo per essere sicuri che sia così, se c'è qualche dubbio nella vostra specifica situazione d'uso.