Program Tip

컬렉션이 수정되었습니다.

programtip 2020. 9. 28. 09:58
반응형

컬렉션이 수정되었습니다. 열거 작업이 실행되지 않을 수 있습니다.


디버거가 연결되면 발생하지 않는 것 같기 때문에이 오류의 하단에 도달 할 수 없습니다. 아래는 코드입니다.

이것은 Windows 서비스의 WCF 서버입니다. NotifySubscribers 메서드는 데이터 이벤트가있을 때마다 서비스에서 호출됩니다 (임의 간격으로, 자주는 아니지만 하루에 약 800 회).

Windows Forms 클라이언트가 구독하면 구독자 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 ()를 호출하면 subscribers.Values의 값이 foreach 시작 부분에 별도의 목록으로 복사됩니다. 다른 어떤 것도이 목록에 액세스 할 수 없으므로 (변수 이름도 없습니다!) 루프 내에서 수정할 수있는 것은 없습니다.


구독자가 구독을 취소하면 열거하는 동안 구독자 컬렉션의 내용이 변경됩니다.

이 문제를 해결하는 방법에는 여러 가지가 있습니다. 하나는 명시 적 사용을 위해 for 루프를 변경하는 것입니다 .ToList().

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

This will not only solve your problem, it will prevent you from having to keep creating a list from your dictionary, which is expensive if there are a lot of subscribers in there. Assuming the list of subscribers to be removed on any given iteration is lower than the total number in the list, this should be faster. But of course feel free to profile it to be sure that's the case if there's any doubt in your specific usage situation.


You can also lock your subscribers dictionary to prevent it from being modified whenever its being looped:

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

Why this error?

In general .Net collections do not support being enumerated and modified at the same time. If you try to modify the collection list during enumeration, it raises an exception. So the issue behind this error is, we can not modify the list/dictionary while we are looping through the same.

One of the solutions

If we iterate a dictionary using a list of its keys, in parallel we can modify the dictionary object, as we are iterating through the key-collection and not the dictionary(and iterating its key collection).

Example

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

Here is a blog post about this solution.

And for a deep dive in StackOverflow: Why this error occurs?


Actually the problem seems to me that you are removing elements from the list and expecting to continue to read the list as if nothing had happened.

What you really need to do is to start from the end and back to the begining. Even if you remove elements from the list you will be able to continue reading it.


InvalidOperationException- An InvalidOperationException has occurred. It reports a "collection was modified" in a foreach-loop

Use break statement, Once the object is removed.

ex:

ArrayList list = new ArrayList(); 

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

I had the same issue, and it was solved when I used a for loop instead of 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);
}

I've seen many options for this but to me this one was the best.

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

Then simply loop through the collection.

Be aware that a ListItemCollection can contain duplicates. By default there is nothing preventing duplicates being added to the collection. To avoid duplicates you can do this:

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

Okay so what helped me was iterating backwards. I was trying to remove an entry from a list but iterating upwards and it screwed up the loop because the entry didn't exist anymore:

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

                            myList.RemoveAt(x);

                        }

You can copy subscribers dictionary object to a same type of temporary dictionary object and then iterate the temporary dictionary object using foreach loop.


So a different way to solve this problem would be instead of removing the elements create a new dictionary and only add the elements you didnt want to remove then replace the original dictionary with the new one. I don't think this is too much of an efficiency problem because it does not increase the number of times you iterate over the structure.


There is one link where it elaborated very well & solution is also given. Try it if you got proper solution please post here so other can understand. Given solution is ok then like the post so other can try these solution.

for you reference original link :- https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

When we use .Net Serialization classes to serialize an object where its definition contains an Enumerable type, i.e. collection, you will be easily getting InvalidOperationException saying "Collection was modified; enumeration operation may not execute" where your coding is under multi-thread scenarios. The bottom cause is that serialization classes will iterate through collection via enumerator, as such, problem goes to trying to iterate through a collection while modifying it.

First solution, we can simply use lock as a synchronization solution to ensure that the operation to the List object can only be executed from one thread at a time. Obviously, you will get performance penalty that if you want to serialize a collection of that object, then for each of them, the lock will be applied.

Well, .Net 4.0 which makes dealing with multi-threading scenarios handy. for this serializing Collection field problem, I found we can just take benefit from ConcurrentQueue(Check MSDN)class, which is a thread-safe and FIFO collection and makes code lock-free.

Using this class, in its simplicity, the stuff you need to modify for your code are replacing Collection type with it, use Enqueue to add an element to the end of ConcurrentQueue, remove those lock code. Or, if the scenario you are working on do require collection stuff like List, you will need a few more code to adapt ConcurrentQueue into your fields.

BTW, ConcurrentQueue doesnât have a Clear method due to underlying algorithm which doesnât permit atomically clearing of the collection. so you have to do it yourself, the fastest way is to re-create a new empty ConcurrentQueue for a replacement.


The accepted answer is imprecise and incorrect in the worst case . If changes are made during ToList(), you can still end up with an error. Besides lock, which performance and thread-safety needs to be taken into consideration if you have a public member, a proper solution can be using immutable types.

In general, an immutable type means that you can't change the state of it once created. So your code should look like:

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
        }
    }
    //.........
}

This can be especially useful if you have a public member. Other classes can always foreach on the immutable types without worrying about the collection being modified.

참고URL : https://stackoverflow.com/questions/604831/collection-was-modified-enumeration-operation-may-not-execute

반응형