Jeg kan ikke komme til bunns i denne feilen, for når feilsøkingsprogrammet er koblet til, ser det ikke ut til å oppstå. Nedenfor er koden.
Dette er en WCF-server i en Windows-tjeneste. Metoden NotifySubscribers kalles av tjenesten når det er en datahendelse (med tilfeldige intervaller, men ikke veldig ofte - omtrent 800 ganger per dag).
Når en Windows Forms-klient abonnerer, legges abonnent-ID-en til i abonnentordboken, og når klienten avslutter abonnementet, slettes den fra ordboken. Feilen oppstår når (eller etter) en klient avslutter abonnementet. Det ser ut til at neste gang NotifySubscribers()-metoden kalles, mislykkes foreach()-sløyfen med feilen i emnelinjen. Metoden skriver feilen i applikasjonsloggen som vist i koden nedenfor. Når en debugger er koblet til og en klient avslutter abonnementet, kjøres koden fint.
Ser du et problem med denne koden? Må jeg gjøre ordboken trådsikker?
[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);
}
}
}
Det som sannsynligvis skjer er at SignalData indirekte endrer abonnentens ordbok under panseret under sløyfen og fører til den meldingen. Du kan bekrefte dette ved å endre
foreach(Subscriber s in subscribers.Values)
Til
foreach(Subscriber s in subscribers.Values.ToList())
Hvis jeg har rett, vil problemet forsvinne...
Å kalle subscribers.Values.ToList() kopierer verdiene til subscribers.Values til en egen liste ved starten av foreach. Ingenting annet har tilgang til denne listen (den har ikke engang et variabelnavn!), så ingenting kan endre den inne i løkken.
Når en abonnent avslutter abonnementet, endrer du innholdet i samlingen av abonnenter under opptelling.
Det er flere måter å fikse dette på, en er å endre for-sløyfen til å bruke en eksplisitt .ToList()
:
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
En mer effektiv måte, etter min mening, er å ha en annen liste som du erklærer at du legger alt som skal fjernes i. Så etter at du er ferdig med hovedløkken (uten .ToList()), gjør du en ny løkke over listen "skal fjernes", og fjerner hver oppføring etter hvert som det skjer. Så i klassen din legger du til:
private List<Guid> toBeRemoved = new List<Guid>();
Deretter endrer du den til:
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 );
}
Dette vil ikke bare løse problemet ditt, det vil forhindre at du må fortsette å opprette en liste fra ordboken din, noe som er dyrt hvis det er mange abonnenter der inne. Forutsatt at listen over abonnenter som skal fjernes på en gitt iterasjon er lavere enn det totale antallet i listen, bør dette være raskere. Men du kan selvfølgelig gjerne profilere den for å være sikker på at det er tilfelle hvis det er noen tvil i din spesifikke brukssituasjon.