Ik kan deze fout niet tot op de bodem uitzoeken, want als de debugger is aangesloten, lijkt het niet voor te komen. Hieronder staat de code.
Dit is een WCF server in een Windows service. De methode NotifySubscribers wordt door de service aangeroepen wanneer er een data-event is (met willekeurige intervallen, maar niet erg vaak - ongeveer 800 keer per dag).
Wanneer een Windows Forms client zich abonneert, wordt het abonnee-ID toegevoegd aan het abonnees woordenboek, en wanneer de client zich afmeldt, wordt het abonnee-ID uit het woordenboek verwijderd. De fout treedt op wanneer (of nadat) een client zich afmeldt. Het blijkt dat de volgende keer dat de NotifySubscribers() methode wordt aangeroepen, de foreach() loop faalt met de fout in de onderwerpregel. De methode schrijft de fout in het applicatielogboek zoals in de onderstaande code wordt getoond. Wanneer een debugger is aangesloten en een klant zich afmeldt, wordt de code prima uitgevoerd.
Ziet u een probleem met deze code? Moet ik de dictionary thread-safe maken?
[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);
}
}
}
Wat's waarschijnlijk gebeurt is dat SignalData indirect het abonneewoordenboek onder de motorkap verandert tijdens de lus en tot dat bericht leidt. Je kunt dit verifiëren door het veranderen van
foreach(Subscriber s in subscribers.Values)
Naar
foreach(Subscriber s in subscribers.Values.ToList())
Als ik gelijk heb, zal het probleem verdwijnen.
Het aanroepen van subscribers.Values.ToList() kopieert de waarden van subscribers.Values naar een aparte lijst aan het begin van de foreach. Niets anders heeft toegang tot deze lijst (hij heeft niet eens een variabele naam!), dus niets kan hem binnen de lus wijzigen.
Wanneer een abonnee zich afmeldt, verandert de inhoud van de verzameling abonnees tijdens de opsomming.
Er zijn verschillende manieren om dit op te lossen, een daarvan is het veranderen van de for-lus om een expliciete .ToList()
te gebruiken:
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
Een efficiëntere manier, naar mijn mening, is om een andere lijst te hebben die je verklaart waar je alles in zet dat "to be removed" is. Dan, nadat je je hoofdlus beëindigd hebt (zonder de .ToList()), doe je nog een lus over de "to be removed" lijst, waarbij je elk item verwijdert wanneer het gebeurt. Dus in je klasse voeg je toe:
private List<Guid> toBeRemoved = new List<Guid>();
Dan verander je het 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 );
}
Dit lost niet alleen je probleem op, maar voorkomt ook dat je steeds een lijst moet aanmaken vanuit je woordenboek, wat duur is als er veel abonnees in staan. Ervan uitgaande dat de lijst van abonnees die verwijderd moeten worden op een gegeven iteratie lager is dan het totale aantal in de lijst, zou dit sneller moeten zijn. Maar voel je natuurlijk vrij om het te profileren om er zeker van te zijn dat dit het geval is als er'enige twijfel is in je specifieke gebruikssituatie.