Jeg kan ikke komme til bunds i denne fejl, for når debuggeren er tilsluttet, synes den ikke at opstå. Nedenfor er koden.
Der er tale om en WCF-server i en Windows-tjeneste. Metoden NotifySubscribers kaldes af tjenesten, når der er en datahændelse (med tilfældige intervaller, men ikke særlig ofte - ca. 800 gange om dagen).
Når en Windows Forms-klient abonnerer, tilføjes abonnent-id'et til subscribers-ordbogen, og når klienten afmelder sig, slettes det fra ordbogen. Fejlen opstår, når (eller efter) en klient afmelder sig. Det ser ud til, at næste gang NotifySubscribers() metoden kaldes, mislykkes foreach() sløjfen med fejlen i emnelinjen. Metoden skriver fejlen ind i programloggen som vist i nedenstående kode. Når en debugger er tilsluttet, og en klient afmelder sig, udføres koden fint.
Kan du se et problem med denne kode? Er jeg nødt til at gøre ordbogen 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, der sandsynligvis sker, er, at SignalData indirekte ændrer abonnenternes ordbog under kølerhjelmen i løbet af løkken, hvilket fører til denne meddelelse. Du kan verificere dette ved at ændre
foreach(Subscriber s in subscribers.Values)
Til
foreach(Subscriber s in subscribers.Values.ToList())
Hvis jeg har ret, vil problemet forsvinde
Ved at kalde subscribers.Values.ToList() kopieres værdierne i subscribers.Values til en separat liste i starten af foreach. Intet andet har adgang til denne liste (den har ikke engang et variabelnavn!), så intet kan ændre den inde i løkken.
Når en abonnent afmelder sig, ændrer du indholdet af samlingen af abonnenter under opregningen.
Der er flere måder at løse dette på, bl.a. ved at ændre for-loop'en til at bruge en eksplicit .ToList()
:
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
En mere effektiv måde er efter min mening at have en anden liste, som du erklærer, at du lægger alt, der skal fjernes, på. Når du så er færdig med din hovedsløjfe (uden .ToList()), laver du endnu en sløjfe over "to be removed" listen, hvor du fjerner hver post efterhånden som det sker. Så i din klasse tilføjer du:
private List<Guid> toBeRemoved = new List<Guid>();
Derefter ændrer du det 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 kun løse dit problem, det vil forhindre dig i at skulle blive ved med at oprette en liste fra din ordbog, hvilket er dyrt, hvis der er mange abonnenter derinde. Hvis man antager, at listen over abonnenter, der skal fjernes ved en given iteration, er lavere end det samlede antal i listen, burde dette være hurtigere. Men du er naturligvis velkommen til at lave en profil for at være sikker på, at det er tilfældet, hvis der er tvivl i din specifikke brugssituation.