Δεν μπορώ να βρω την αιτία αυτού του σφάλματος, διότι όταν είναι συνδεδεμένος ο αποσφαλματωτής, δεν φαίνεται να εμφανίζεται. Ακολουθεί ο κώδικας.
Πρόκειται για έναν διακομιστή WCF σε μια υπηρεσία των Windows. Η μέθοδος NotifySubscribers καλείται από την υπηρεσία κάθε φορά που υπάρχει ένα γεγονός δεδομένων (σε τυχαία χρονικά διαστήματα, αλλά όχι πολύ συχνά - περίπου 800 φορές την ημέρα).
Όταν ένας πελάτης των Windows Forms εγγράφεται, το αναγνωριστικό του συνδρομητή προστίθεται στο λεξικό subscribers, και όταν ο πελάτης διαγράφει τη συνδρομή του, διαγράφεται από το λεξικό. Το σφάλμα συμβαίνει όταν (ή μετά) ένας πελάτης διαγράφει τη συνδρομή του. Φαίνεται ότι την επόμενη φορά που καλείται η μέθοδος NotifySubscribers(), ο βρόχος foreach() αποτυγχάνει με το σφάλμα στη γραμμή θέματος. Η μέθοδος γράφει το σφάλμα στο αρχείο καταγραφής της εφαρμογής, όπως φαίνεται στον παρακάτω κώδικα. Όταν είναι συνδεδεμένο ένα πρόγραμμα εντοπισμού σφαλμάτων και ένας πελάτης διαγράφει την εγγραφή του, ο κώδικας εκτελείται κανονικά.
Βλέπετε κάποιο πρόβλημα με αυτόν τον κώδικα; Μήπως πρέπει να κάνω το λεξικό ασφαλές για το νήμα;
[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);
}
}
}
Αυτό που πιθανότατα συμβαίνει είναι ότι το SignalData αλλάζει έμμεσα το λεξικό των συνδρομητών κάτω από την κουκούλα κατά τη διάρκεια του βρόχου και οδηγεί σε αυτό το μήνυμα. Μπορείτε να το επαληθεύσετε αυτό αλλάζοντας το
foreach(Subscriber s in subscribers.Values)
To
foreach(Subscriber s in subscribers.Values.ToList())
Αν έχω δίκιο, το πρόβλημα θα εξαφανιστεί.
Η κλήση της subscribers.Values.ToList() αντιγράφει τις τιμές της subscribers.Values σε μια ξεχωριστή λίστα στην αρχή της foreach. Τίποτα άλλο δεν έχει πρόσβαση σε αυτή τη λίστα (δεν έχει καν όνομα μεταβλητής!), οπότε τίποτα δεν μπορεί να την τροποποιήσει μέσα στο βρόχο.
Όταν ένας συνδρομητής διαγράφεται, αλλάζετε τα περιεχόμενα της συλλογής των συνδρομητών κατά την απαρίθμηση.
Υπάρχουν διάφοροι τρόποι για να το διορθώσετε αυτό, ένας από τους οποίους είναι η αλλαγή του βρόχου for ώστε να χρησιμοποιείται ρητά η .ToList()
:
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
Ένας πιο αποτελεσματικός τρόπος, κατά τη γνώμη μου, είναι να έχετε μια άλλη λίστα την οποία δηλώνετε και στην οποία τοποθετείτε οτιδήποτε είναι "προς αφαίρεση". Στη συνέχεια, αφού τελειώσετε τον κύριο βρόχο σας (χωρίς την .ToList()), κάνετε έναν άλλο βρόχο πάνω από τη λίστα "to be removed", αφαιρώντας κάθε εγγραφή καθώς συμβαίνει. Έτσι, στην κλάση σας προσθέτετε:
private List<Guid> toBeRemoved = new List<Guid>();
Στη συνέχεια, το αλλάζετε σε:
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 );
}
Αυτό όχι μόνο θα λύσει το πρόβλημά σας, αλλά θα σας αποτρέψει από το να χρειάζεται να δημιουργείτε συνεχώς μια λίστα από το λεξικό σας, κάτι που είναι ακριβό αν υπάρχουν πολλοί συνδρομητές εκεί μέσα. Υποθέτοντας ότι η λίστα των συνδρομητών που πρέπει να αφαιρεθούν σε κάθε επανάληψη είναι μικρότερη από το συνολικό αριθμό στη λίστα, αυτό θα πρέπει να είναι ταχύτερο. Αλλά φυσικά μη διστάσετε να το σκιαγραφήσετε για να βεβαιωθείτε ότι αυτό ισχύει, αν υπάρχει οποιαδήποτε αμφιβολία στη συγκεκριμένη κατάσταση χρήσης σας.