デバッガーを装着しても、このエラーは発生しないようなので、真相がわかりません。以下はコードです。
これは、WindowsサービスのWCFサーバーです。データイベントがあるたびに、メソッドNotifySubscribersがサービスから呼び出されます(ランダムな間隔で、あまり頻繁ではありませんが、1日に800回程度)。
Windowsフォームクライアントが購読すると、購読者IDが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がループ中にフードの下で間接的にSubscribers辞書を変更しているために、このようなメッセージが表示されていると考えられます。 これを確認するには
foreach(Subscriber s in subscribers.Values)
を
foreach(Subscriber s in subscribers.Values.ToList())
私が正しければ、問題は解消されます。
subscribers.Values.ToList()を呼び出すと、subscribers.Valuesの値がforeachの開始時に別のリストにコピーされます。このリストには他の誰もアクセスできないので(変数名すらない!)、ループ内では何も変更できません。
加入者が脱退すると、列挙中の加入者のコレクションの内容が変更されます。
この問題を解決するにはいくつかの方法がありますが、1つはforループで明示的に .ToList()
を使用するように変更することです。
public void NotifySubscribers(DataRecord sr)
{
foreach(Subscriber s in subscribers.Values.ToList())
{
^^^^^^^^^
...
私の意見では、より効率的な方法は、"to be removed"であるものを入れると宣言した別のリストを持つことです。 そして、(.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 );
}
これにより、問題が解決するだけでなく、辞書からリストを作成し続ける必要がなくなり、多くの購読者がいる場合はコストがかかります。 任意の繰り返しで削除される購読者のリストが、リストの総数よりも少ないと仮定すれば、これはより速くなるはずです。 もちろん、あなたの使用状況に疑問がある場合は、念のためプロファイルを作成してみてください。