Je ne parviens pas à trouver la cause de cette erreur, car lorsque le débogueur est connecté, elle ne semble pas se produire. Voici le code.
Il s'agit d'un serveur WCF dans un service Windows. La méthode NotifySubscribers est appelée par le service chaque fois qu'il y a un événement de données (à des intervalles aléatoires, mais pas très souvent - environ 800 fois par jour).
Lorsqu'un client Windows Forms s'abonne, l'ID de l'abonné est ajouté au dictionnaire des abonnés, et lorsque le client se désabonne, il est supprimé du dictionnaire. L'erreur se produit lorsque (ou après) un client se désabonne. Il semble que la prochaine fois que la méthode NotifySubscribers() est appelée, la boucle foreach() échoue avec l'erreur dans la ligne d'objet. La méthode écrit l'erreur dans le journal de l'application comme indiqué dans le code ci-dessous. Lorsqu'un débogueur est connecté et qu'un client se désabonne, le code s'exécute correctement.
Voyez-vous un problème avec ce code ? Dois-je rendre le dictionnaire 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);
}
}
}
Ce qui se passe probablement, c'est que SignalData modifie indirectement le dictionnaire des abonnés pendant la boucle, ce qui entraîne ce message. Vous pouvez le vérifier en modifiant
foreach(Subscriber s in subscribers.Values)
à
foreach(Subscriber s in subscribers.Values.ToList())
Si j'ai raison, le problème disparaîtra.
L'appel à subscribers.Values.ToList() copie les valeurs de subscribers.Values dans une liste séparée au début du foreach. Rien d'autre n'a accès à cette liste (elle n'a même pas de nom de variable !), donc rien ne peut la modifier à l'intérieur de la boucle.
Lorsqu'un abonné se désinscrit, vous modifiez le contenu de la collection d'abonnés pendant l'énumération.
Il y a plusieurs façons de résoudre ce problème, l'une d'entre elles étant de modifier la boucle for pour utiliser un .ToList()
explicite :
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
Une manière plus efficace, à mon avis, est d'avoir une autre liste que vous déclarez et dans laquelle vous mettez tout ce qui est "à supprimer". Ensuite, après avoir terminé votre boucle principale (sans le .ToList()), vous faites une autre boucle sur la liste "à supprimer", en supprimant chaque entrée au fur et à mesure. Donc, dans votre classe, vous ajoutez :
private List<Guid> toBeRemoved = new List<Guid>();
Puis vous le changez en :
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 );
}
Cela ne résoudra pas seulement votre problème, mais vous évitera de devoir continuer à créer une liste à partir de votre dictionnaire, ce qui est coûteux si la liste contient beaucoup d'abonnés. En supposant que la liste des abonnés à supprimer à chaque itération est inférieure au nombre total d'abonnés dans la liste, cela devrait être plus rapide. Mais bien sûr, n'hésitez pas à le profiler pour être sûr que c'est le cas si vous avez un doute sur votre situation d'utilisation spécifique.