コレクションが変更されました。列挙操作が実行されない可能性があります

StackOverflow https://stackoverflow.com/questions/604831

質問

このエラーの一番下に到達することはできません。なぜなら、デバッガーがアタッチされると、エラーは発生しないようだからです。以下にコードを示します。

これは、WindowsサービスのWCFサーバーです。 NotifySubscribersメソッドは、データイベントが発生するたびにサービスによって呼び出されます(ランダムな間隔ですが、あまり頻繁ではありません-1日あたり約800回)。

Windowsフォームクライアントがサブスクライブすると、サブスクライバーIDがサブスクライバーディクショナリに追加され、クライアントがサブスクライブ解除すると、ディクショナリから削除されます。このエラーは、クライアントがサブスクライブを解除したとき(またはその後)に発生します。次回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)

宛先

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

私が正しい場合、問題は消えます

subscribers.Values.ToList()を呼び出すと、subeachers.Valuesの値がforeachの開始時に別のリストにコピーされます。このリストにアクセスできるものは他にないため(変数名さえありません!)、ループ内で変更することはできません。

他のヒント

サブスクライバーがサブスクリプションを解除すると、列挙中にサブスクライバーのコレクションの内容が変更されます。

これを修正するにはいくつかの方法があり、1つは明示的な .ToList()を使用するようにforループを変更します:

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

より効率的な方法は、私の意見では、「削除する」ものをすべて置くことを宣言する別のリストを作成することです。に。次に、メインループ(.ToList()なし)を終了した後、「削除対象」に対して別のループを実行します。リスト、発生した各エントリを削除します。クラスに追加します:

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

これは問題を解決するだけでなく、辞書からリストを作成し続ける必要がなくなります。リストに登録者が多い場合は費用がかかります。特定の反復で削除されるサブスクライバーのリストがリスト内の総数よりも少ないと仮定すると、これはより高速になります。ただし、特定の使用状況に疑いがある場合は、プロファイルを自由に作成してください。

サブスクライバー辞書をロックして、ループされるたびに変更されないようにすることもできます。

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }

このエラーの理由

一般に、.Netコレクションは、列挙と変更を同時にサポートしていません。列挙中にコレクションリストを変更しようとすると、例外が発生します。したがって、このエラーの背後にある問題は、同じものをループしている間はリスト/辞書を変更できないということです。

ソリューションの1つ

キーのリストを使用してディクショナリを反復する場合、キーコレクションを反復処理するため、ディクショナリオブジェクトを並行して変更できます。 辞書ではありません(およびキーコレクションを繰り返します)。

//get key collection from dictionary into a list to loop through
List<int> keys = new List<int>(Dictionary.Keys);

// iterating key collection using a simple for-each loop
foreach (int key in keys)
{
  // Now we can perform any modification with values of the dictionary.
  Dictionary[key] = Dictionary[key] - 1;
}

<このソリューションに関するem>ブログ投稿

そしてStackOverflowの詳細については、このエラーが発生する理由

実際には、リストから要素を削除し、何も起こらなかったかのようにリストを読み続けることを期待しているように思えます。

本当に必要なのは、最後から始めて最初に戻ることです。リストから要素を削除しても、それを読み続けることができます。

InvalidOperationException- InvalidOperationExceptionが発生しました。 「コレクションが変更されました」と報告されます。 foreach-loopで

オブジェクトが削除されたら、breakステートメントを使用します。

例:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}

同じ問題があり、 foreach の代わりに for ループを使用すると解決しました。

// foreach (var item in itemsToBeLast)
for (int i = 0; i < itemsToBeLast.Count; i++)
{
    var matchingItem = itemsToBeLast.FirstOrDefault(item => item.Detach);

   if (matchingItem != null)
   {
      itemsToBeLast.Remove(matchingItem);
      continue;
   }
   allItems.Add(itemsToBeLast[i]);// (attachDetachItem);
}

これには多くのオプションがありますが、私にとってはこれが最高でした。

ListItemCollection collection = new ListItemCollection();
        foreach (ListItem item in ListBox1.Items)
        {
            if (item.Selected)
                collection.Add(item);
        }

その後、コレクションをループします。

ListItemCollectionには重複が含まれることがあることに注意してください。デフォルトでは、コレクションへの重複の追加を妨げるものはありません。重複を避けるためにこれを行うことができます:

ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }

さて、私を助けたのは、逆方向に繰り返すことです。リストからエントリを削除しようとしていましたが、上向きに繰り返して、エントリがもう存在しなかったためループを台無しにしました:

for (int x = myList.Count - 1; x > -1; x--)
                        {

                            myList.RemoveAt(x);

                        }

サブスクライバ辞書オブジェクトを同じタイプの一時辞書オブジェクトにコピーしてから、foreachループを使用して一時辞書オブジェクトを反復処理できます。

この問題を解決する別の方法は、要素を削除する代わりに新しい辞書を作成し、削除したくない要素のみを追加してから、元の辞書を新しい辞書に置き換えます。構造を反復処理する回数が増えないため、これはあまり効率的な問題ではないと思います。

非常に詳細に説明されたリンクが1つあります。解決策も示されています。 適切な解決策が得られたら、他の人が理解できるようにここに投稿してください。 他の人がこれらの解決策を試すことができるように、与えられた解決策は投稿のようにOKです。

元のリンクを参照するには:- https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

.Net Serializationクラスを使用して、その定義にEnumerable型が含まれるオブジェクト、つまり コレクションの場合、&quot; Collection was modified;と言ったInvalidOperationExceptionを簡単に取得できます。 列挙操作が実行されない可能性があります&quot;コーディングがマルチスレッドシナリオの下にある場合。 根本的な原因は、シリアル化クラスが列挙子を介してコレクションを反復処理するためです。 問題は、コレクションを変更しながら反復処理しようとすることです。

最初の解決策は、単にロックを同期化ソリューションとして使用して、 Listオブジェクトに対する操作は、一度に1つのスレッドからのみ実行できます。  明らかに、パフォーマンスが低下します そのオブジェクトのコレクションをシリアル化する場合は、それぞれに対してロックが適用されます。

まあ、マルチスレッドのシナリオを簡単に処理できる.Net 4.0。 このコレクションフィールドのシリアル化の問題については、ConcurrentQueue(Check MSDN)クラスを利用するだけでよいことがわかりました。 これはスレッドセーフでFIFOのコレクションであり、コードをロックフリーにします。

このクラスを使用すると、コードの修正が必要なものがシンプルになり、Collection型がこのクラスに置き換えられます。 Enqueueを使用してConcurrentQueueの最後に要素を追加し、それらのロックコードを削除します。 または、作業中のシナリオでListなどのコレクションが必要な場合は、ConcurrentQueueをフィールドに適合させるためのコードがさらに必要になります。

ところで、ConcurrentQueueには、コレクションのアトミックなクリアを許可しない基になるアルゴリズムのため、Clearメソッドがありません。 自分でやらなければならないので、最速の方法は、新しい空のConcurrentQueueを再作成して再作成することです。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top