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


911

デバッガーが接続されていると発生しないように見えるため、このエラーの原因を突き止めることはできません。以下はコードです。

これは、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);
        }
    }
}

私の場合、プロセス中に変更されたいくつかの.Include( "table")を使用していたため、これは付随的な影響でした。コードを読んだときはあまりわかりませんでした。ただし、これらのインクルードが必要ないことは幸運でした(ええ!古い、保守されていないコード)。それらを削除するだけで問題を解決しました
Adi

回答:


1631

起こっている可能性が高いのは、SignalDataがループ中に内部のサブスクライバーディクショナリを間接的に変更し、そのメッセージにつながることです。これを確認するには、

foreach(Subscriber s in subscribers.Values)

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

私が正しければ、問題は消えます

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


14
BTW .ToList()は、.NET 2.0アプリケーションと互換性のないSystem.Core dllに存在します。したがって、ターゲットアプリを.Net 3.5に変更する必要があるかもしれません
mishal153

60
なぜあなたがToListをしたのか、なぜそれがすべてを修正するのか分かりません
PositiveGuy

204
@CoffeeAddict:問題はsubscribers.Valuesforeachループ内で変更されていることです。を呼び出すとsubscribers.Values.ToList()、の値がsubscribers.Valuesの先頭にある別のリストにコピーされますforeach。他には何もこのリストにアクセスできません(変数名すらありません!)ため、ループ内では何も変更できません。
BlueRaja-Danny Pflughoeft 2012

31
の実行ToList中にコレクションが変更された場合にもスローされる可能性があることに注意してくださいToList
Sriram Sakthivel、2015年

16
私は問題が修正されないと思いますが、再現を難しくしています。ToListはアトミック操作ではありません。さらにToListおもしろいのは、基本foreach的に内部で独自にアイテムを新しいリストインスタンスにコピーすることです。つまりforeach、(より高速ではありますが)foreach反復を追加して問題を修正しました。
Groo

113

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

これを修正する方法はいくつかあります。1つはforループを変更して明示的に使用する方法.ToList()です。

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

64

私の意見では、より効率的な方法は、「削除する」ものは何でも入れることを宣言する別のリストを用意することです。次に、メインループ(.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 );
}

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


9
より大きなコレクションで作業している場合は、検討する価値があると思います。もしそれが小さければ、たぶんToListだけで次に進むでしょう。
Karl Kieninger、2015年

42

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

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

2
これは完全な例ですか?MarkerFrequenciesという名前の一般的なDictionary <string、int>を含むクラス(以下の_dictionary obj)がありますが、これを行ってもすぐにはクラッシュが解決しませんでした:lock(_dictionary.MarkerFrequencies){foreach(KeyValuePair <string、int> _dictionary.MarkerFrequencies){...}}
Jon Coombs 14

4
@JCoombs変更している可能性があります。MarkerFrequenciesロック自体の中に辞書を再割り当てしている可能性があります。つまり、元のインスタンスはロックされていません。また、のfor代わりにを使用してみてforeachください。これこれを参照してください。それで解決するかどうか教えてください。
Mohammad Sepahvand 2014

1
さて、ようやくこのバグの修正に取り掛かり、これらのヒントは役に立ちました-ありがとう!私のデータセットは小さく、この操作は単なるUI表示の更新だったので、コピーを反復するのが最善のようでした。(両方のスレッドでMarkerFrequenciesをロックした後、foreachの代わりにforを使用して実験しました。これにより、クラッシュは防止されましたが、さらにデバッグ作業が必要と思われました。さらに複雑さをもたらしました。ここで2つのスレッドを使用する唯一の理由は、ユーザーがキャンセルできるようにすることですちなみに操作。UIはそのデータを直接変更しません。)
Jon Coombs

9
問題は、大規模なアプリケーションの場合、ロックがパフォーマンスに大きな影響を与える可能性があることSystem.Collections.Concurrentです。名前空間でコレクションを使用するほうがよいでしょう。
BrainSlugs83 14

28

なぜこのエラーなのか?

一般に、.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;
}

ここにあるブログの記事このソリューションについては。

StackOverflowの詳細:このエラーが発生する理由


ありがとうございました!なぜそれが発生するのかを明確に説明し、アプリケーションでこれを解決する方法をすぐに教えてくれました。
Kim Crosser

5

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

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


それがどのように違いを生むかわかりませんか?リストの中央にある要素を削除しても、アクセスしようとしているアイテムが見つからないため、エラーが発生しますか?
Zapnologica 14

4
@Zapnologicaの違いは、リストを列挙するのではなく、for / eachを実行する代わりに、for / nextを実行して整数でアクセスすることです。リスト内のリストを間違いなく変更できます。以下のための/次のループが、決して用/各ループ(のために/各ので、中を列挙) -あなたはまた、それがために/次に前方に行く行うことができます、あなたがなど、あなたのカウンターを、調整するための余分なロジックを持って提供
BrainSlugs83

4

InvalidOperationException- InvalidOperationExceptionが発生しました。これは、foreachループで「コレクションが変更された」と報告します

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

例:

ArrayList list = new ArrayList(); 

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

リストに削除する必要のあるアイテムが最大で1つあることがわかっている場合は、適切でシンプルなソリューションです。
タワブワキル

3

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

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

11
このコードは間違っており、要素が削除されると、コレクション内の一部のアイテムがスキップされます。たとえば、var arr = ["a"、 "b"、 "c"]があり、最初の反復(i = 0)で、位置0の要素(要素 "a")を削除します。この後、すべての配列要素が1つ上の位置に移動し、配列は["b"、 "c"]になります。したがって、次の反復(i = 1)では、位置1の要素をチェックします。これは、「b」ではなく「c」になります。これは間違っています。それを修正するには、下から上に移動する必要があります
Kaspars Ozols

3

さて、私を助けたのは逆方向の反復でした。リストからエントリを削除しようとしましたが、エントリがもう存在しないため、上方向に反復するとループが失敗しました。

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

                            myList.RemoveAt(x);

                        }

2

私はこれについて多くのオプションを見てきましたが、私にとってこれが最高でした。

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

このコードは、重複がデータベースに入力されるのをどのように防止しますか。私は似たようなものを書いており、リストボックスから新しいユーザーを追加するときに、すでにリストにあるものを誤って選択したままにしておくと、重複したエントリが作成されます。@Mikeの提案はありますか?
ジェイミー

1

受け入れられた回答は不正確であり、最悪の場合は不正確です。ToList()変更を加えた場合でも、エラーが発生する可能性があります。さらにlock、パブリックメンバーがいる場合、どのパフォーマンスとスレッドセーフを考慮する必要があるかは、不変の型を使用することで適切に解決できます

一般に、不変タイプは、一度作成されるとその状態を変更できないことを意味します。したがって、コードは次のようになります。

public class SubscriptionServer : ISubscriptionServer
{
    private static ImmutableDictionary<Guid, Subscriber> subscribers = ImmutableDictionary<Guid, Subscriber>.Empty;
    public void SubscribeEvent(string id)
    {
        subscribers = subscribers.Add(Guid.NewGuid(), new Subscriber());
    }
    public void NotifyEvent()
    {
        foreach(var sub in subscribers.Values)
        {
            //.....This is always safe
        }
    }
    //.........
}

これは、パブリックメンバーがいる場合に特に役立ちます。他のクラスはforeach、コレクションの変更を心配することなく、常に不変の型を使用できます。


1

以下は、特殊なアプローチを保証する特定のシナリオです。

  1. Dictionary頻繁に列挙されています。
  2. Dictionaryあまり頻繁に変更されます。

このシナリオでは、すべての列挙の前にDictionary(またはDictionary.Values)のコピーを作成すると、かなりのコストがかかる可能性があります。この問題を解決するための私の考えは、同じキャッシュされたコピーを複数の列挙で再利用しIEnumerator、元Dictionaryのの例外を監視することです。列挙子はコピーされたデータと共にキャッシュされ、新しい列挙を開始する前に問い合わせられます。例外が発生した場合、キャッシュされたコピーは破棄され、新しいコピーが作成されます。これがこのアイデアの私の実装です:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

public class EnumerableSnapshot<T> : IEnumerable<T>, IDisposable
{
    private IEnumerable<T> _source;
    private IEnumerator<T> _enumerator;
    private ReadOnlyCollection<T> _cached;

    public EnumerableSnapshot(IEnumerable<T> source)
    {
        _source = source ?? throw new ArgumentNullException(nameof(source));
    }

    public IEnumerator<T> GetEnumerator()
    {
        if (_source == null) throw new ObjectDisposedException(this.GetType().Name);
        if (_enumerator == null)
        {
            _enumerator = _source.GetEnumerator();
            _cached = new ReadOnlyCollection<T>(_source.ToArray());
        }
        else
        {
            var modified = false;
            if (_source is ICollection collection) // C# 7 syntax
            {
                modified = _cached.Count != collection.Count;
            }
            if (!modified)
            {
                try
                {
                    _enumerator.MoveNext();
                }
                catch (InvalidOperationException)
                {
                    modified = true;
                }
            }
            if (modified)
            {
                _enumerator.Dispose();
                _enumerator = _source.GetEnumerator();
                _cached = new ReadOnlyCollection<T>(_source.ToArray());
            }
        }
        return _cached.GetEnumerator();
    }

    public void Dispose()
    {
        _enumerator?.Dispose();
        _enumerator = null;
        _cached = null;
        _source = null;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public static class EnumerableSnapshotExtensions
{
    public static EnumerableSnapshot<T> ToEnumerableSnapshot<T>(
        this IEnumerable<T> source) => new EnumerableSnapshot<T>(source);
}

使用例:

private static IDictionary<Guid, Subscriber> _subscribers;
private static EnumerableSnapshot<Subscriber> _subscribersSnapshot;

//...(in the constructor)
_subscribers = new Dictionary<Guid, Subscriber>();
_subscribersSnapshot = _subscribers.Values.ToEnumerableSnapshot();

// ...(elsewere)
foreach (var subscriber in _subscribersSnapshot)
{
    //...
}

残念ながら、このアイデアは、クラスで現在使用することはできませんDictionaryので、.NETのコア3.0でこのクラスがスローされませんコレクションが変更された例外を列挙するときや方法RemoveClear呼び出されます。チェックした他のすべてのコンテナは一貫して動作しています。:私は体系的にこれらのクラスを確認し List<T>Collection<T>ObservableCollection<T>HashSet<T>SortedSet<T>Dictionary<T,V>SortedDictionary<T,V>Dictionary.NET Core のクラスの前述の2つのメソッドのみが列挙を無効にしていません。


更新:キャッシュされたコレクションと元のコレクションの長さを比較することで、上記の問題を修正しました。この修正は、辞書が引数としてEnumerableSnapshotコンストラクタに直接渡され、そのアイデンティティが(たとえば)次のようなプロジェクションによって隠されないことを前提としていますdictionary.Select(e => e).ΤοEnumerableSnapshot()


重要:上記のクラスはスレッドセーフではありません。シングルスレッドで排他的に実行されるコードから使用することを目的としています。


0

サブスクライバーディクショナリオブジェクトを同じタイプの一時ディクショナリオブジェクトにコピーし、foreachループを使用して一時ディクショナリオブジェクトを反復できます。


(この投稿は質問に対する質の高い回答を提供していないようです。回答を編集するか、質問へのコメントとして投稿してください)。
sɐunıɔןɐqɐp

0

したがって、この問題を解決する別の方法は、要素を削除する代わりに新しいディクショナリを作成し、削除したくない要素のみを追加して、元のディクショナリを新しいものに置き換えることです。構造を反復する回数が増えることはないので、これはあまり効率の問題ではないと思います。


0

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

元のリンクを参照してください:- https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

.Net Serializationクラスを使用して、その定義にEnumerableタイプ、つまりコレクションが含まれるオブジェクトをシリアル化すると、コーディングがマルチスレッドシナリオである場合に、「コレクションが変更されました。列挙操作が実行されない可能性があります」というInvalidOperationExceptionが簡単に発生します。一番下の原因は、シリアル化クラスが列挙子を介してコレクションを反復処理するため、コレクションを変更しながらコレクションを反復処理しようとすると問題が発生することです。

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

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

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

ところで、コレクションのアトミックなクリアを許可しない基本的なアルゴリズムのために、ConcurrentQueueにはClearメソッドがありません。したがって、自分で行う必要があります。最も速い方法は、新しい空のConcurrentQueueを再作成して交換することです。


-1

私は最近、Linqの.Remove(item)を使用したオープンdbcontextにある不慣れなコードでこれに遭遇しました。最初の反復時にアイテムが削除されたため、エラーデバッグを見つけるのに時間がかかりました。同じコンテキストにいるため、前に戻ってステップオーバーしようとすると、コレクションが空でした。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.