Kolekcija tika modificēta; uzskaites operāciju nedrīkst izpildīt

Es nevaru izprast šo kļūdu, jo, kad ir pievienots atkļūdošanas programma, šķiet, ka tā nerodas. Zemāk ir kods.

Tas ir WCF serveris Windows pakalpojumā. Pakalpojums izsauc metodi NotifySubscribers ikreiz, kad ir kāds datu notikums (ar nejaušiem intervāliem, bet ne pārāk bieži - apmēram 800 reižu dienā).

Kad Windows Forms klients parakstās, abonenta ID tiek pievienots abonentu vārdnīcai, un, kad klients atsakās no abonēšanas, tas tiek dzēsts no vārdnīcas. Kļūda rodas, kad (vai pēc tam, kad) klients atteicas no abonēšanas. Šķiet, ka nākamreiz, kad tiek izsaukta metode NotifySubscribers(), foreach() cilpa neizdodas ar kļūdu tēmas rindā. Šī metode ieraksta kļūdu lietojumprogrammas žurnālā, kā parādīts turpmāk dotajā kodā. Ja ir pievienots atkļūdošanas programma un klients atceļ abonēšanu, kods tiek izpildīts pareizi.

Vai šajā kodā saskatāt problēmu? Vai man vārdnīca ir jāpadara droša pret vītnēm?

[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);
        }
    }
}
Risinājums

Iespējams, ka SignalData cilpas laikā netieši maina abonentu vārdnīcu zem pārsega, kā rezultātā rodas šis ziņojums. To var pārbaudīt, mainot

foreach(Subscriber s in subscribers.Values)

Uz

foreach(Subscriber s in subscribers.Values.ToList())

Ja man ir taisnība, problēma izzudīs

Izsaucot subscribers.Values.ToList(), subscribers.Values vērtības tiek kopētas uz atsevišķu sarakstu foreach sākumā. Nekam citam nav piekļuves šim sarakstam (tam pat nav mainīgā vārda!), tāpēc nekas to nevar mainīt cilpas iekšienē.

Komentāri (18)

Ja abonents atrakstās no abonementa, uzskaitīšanas laikā tiek mainīts abonentu kolekcijas saturs.

Ir vairāki veidi, kā to novērst, un viens no tiem ir mainīt for ciklu, lai izmantotu skaidru .ToList():

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
Komentāri (0)

Efektīvāks veids, manuprāt, ir izveidot vēl vienu sarakstu, kurā jūs deklarējat, ka jūs ievietojat visu, kas ir "jādzēš". Pēc tam, kad esat pabeidzis galveno cilpu (bez .ToList()), veiciet vēl vienu cilpu pār sarakstu "to be removed", noņemot katru ierakstu, kad tas notiek. Tātad savā klasē jūs pievienojat:

private List toBeRemoved = new List();

Pēc tam to maināt uz:

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 );
}

Tas ne tikai atrisinās jūsu problēmu, bet arī novērsīs nepieciešamību turpināt veidot sarakstu no vārdnīcas, kas ir dārgi, ja tajā ir daudz abonentu. Pieņemot, ka jebkurā iterācijā dzēšamo abonentu saraksts ir mazāks par kopējo sarakstā esošo abonentu skaitu, tam vajadzētu būt ātrākai. Bet, protams, varat to profilēt, lai pārliecinātos, ka tas tā ir, ja jūsu konkrētajā lietošanas situācijā rodas šaubas.

Komentāri (1)