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


910

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

이것은 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);
        }
    }
}

내 경우에는 프로세스 중에 수정 된 몇 가지 .Include ( "table")을 사용했기 때문에 부수 효과였습니다. 코드를 읽을 때별로 눈에 띄지 않았습니다. 그러나 나는 그 필요하지 않았다 포함 운이 좋았다 (예 이전, 관리되지 않는 코드!) 난 그냥 그들을 제거하여 문제가 해결되었습니다
안녕

답변:


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에 있습니다. 당신은 닷넷 3.5 대상 응용 프로그램을 변경해야 할 수도 있습니다 그래서
mishal153

60
나는 왜 당신이 ToList를했는지 그리고 왜 모든 것이 고쳐 지는지 이해하지 못합니다
PositiveGuy

204
@CoffeeAddict : 문제는 루프 subscribers.Values내에서 수정되고 있다는 것 입니다 foreach. 호출 하면의 시작 부분에서 subscribers.Values.ToList()값을 subscribers.Values별도의 목록으로 복사 합니다 foreach. 다른 사람은이 목록에 액세스 할 수 없으며 (변수 이름조차 없습니다!) , 루프 내에서 아무것도 수정할 수 없습니다.
BlueRaja-대니 Pflughoeft

31
참고 ToList컬렉션 동안 수정 된 경우도 던질 수있는 ToList실행됩니다.
Sriram Sakthivel

16
나는 그것이 문제를 해결하지는 않지만 단지 재현하기 어렵게 만든다고 확신합니다. ToList원 자성 작업이 아닙니다. 더 재미있는 ToList것은 기본적으로 foreach내부적으로 항목을 새 목록 인스턴스에 복사하기 위해 자체적 으로 수행하므로 foreach추가 (더 빠른) foreach반복 을 추가하여 문제를 해결했습니다 .
Groo

113

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

이것을 수정하는 몇 가지 방법이 있습니다. 하나는 명시 적을 사용하도록 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

42

또한 구독자 사전을 잠글 때마다 수정되지 않도록 구독자 사전을 잠글 수도 있습니다.

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

2
이것이 완전한 예입니까? MarkerFrequencies라는 일반 Dictionary <string, int>가 포함 된 클래스 (_dictionary obj 아래)가 있지만 충돌을 즉시 해결하지 못했습니다. lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair <string, int> pair in _dictionary.MarkerFrequencies) {...}}
Jon Coombs

4
@JCoombs MarkerFrequencies잠금 자체 내부의 사전을 다시 할당하여 수정 할 수 있습니다. 즉, 원래 인스턴스가 더 이상 잠겨 있지 않습니다. 또한 사용하려고 for대신의를 foreach참조하십시오 . 그것이 해결되면 알려주세요.
Mohammad Sepahvand

1
글쎄, 마침내이 버그를 해결하기 위해 왔으며이 팁은 도움이되었습니다. 감사합니다! 내 데이터 세트는 작았 으며이 작업은 UI 디스플레이 업데이트 일뿐이므로 사본을 반복하는 것이 가장 좋았습니다. (두 스레드에서 MarkerFrequencies를 잠근 후 foreach 대신 for를 사용하여 실험했습니다. 충돌이 발생하지는 않았지만 추가 디버깅 작업이 필요한 것 같습니다. 더 복잡해졌습니다. 여기에 두 개의 스레드가있는 유일한 이유는 사용자가 취소 할 수 있도록하기위한 것입니다. UI는 그 데이터를 직접 수정하지 않습니다.)
Jon Coombs

9
문제는 대규모 응용 프로그램의 경우 잠금이 주요 성능 저하가 될 수 있다는 것 System.Collections.Concurrent입니다. 네임 스페이스 에서 컬렉션을 사용하는 것이 더 좋습니다 .
BrainSlugs83

28

왜이 오류가 발생합니까?

일반적으로 .Net 컬렉션은 동시에 열거 및 수정되는 것을 지원하지 않습니다. 열거 중에 컬렉션 목록을 수정하려고하면 예외가 발생합니다. 따라서이 오류의 문제는 동일한 루프를 반복하는 동안 목록 / 사전을 수정할 수 없다는 것입니다.

솔루션 중 하나

키 목록을 사용하여 사전을 반복하는 경우 사전이 아닌 키 수집을 반복하고 키 컬렉션을 반복하므로 사전 객체를 수정할 수 있습니다.

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

4
@Zapnologica 차이점은- 목록을 열거 하지 않을 것입니다-for / each를 수행하는 대신 for / next를 수행하고 정수로 액세스 할 것입니다. for / next 루프, for / each 루프에서는 절대 안됨 (for / each 열거로 인해 )-카운터를 조정할 수있는 추가 로직이있는 경우 for / next로 진행할 수도 있습니다.
BrainSlugs83

4

InvalidOperationException- InvalidOperationException이 발생했습니다. foreach-loop에서 "컬렉션이 수정되었습니다"라고보고합니다

일단 객체가 제거되면 break 문을 사용하십시오.

전의:

ArrayList list = new ArrayList(); 

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

목록에 최대 하나의 항목을 제거해야한다는 것을 알고있는 경우 좋고 간단한 해결책입니다.
Tawab Wakil

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")에서 요소를 제거합니다. 그런 다음 모든 배열 요소가 한 위치 위로 이동하고 배열은 [ "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 제안이 있습니까?
Jamie

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 이 클래스가 throw하지 않습니다 컬렉션이 수정 된 예외를 열거 할 때와 방법 RemoveClear호출됩니다. 내가 확인한 다른 모든 컨테이너는 일관되게 작동합니다. 나는 체계적으로 이러한 클래스를 확인 : List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>,Dictionary<T,V>SortedDictionary<T,V>. Dictionary.NET Core 에서 클래스 의 위에서 언급 한 두 가지 메소드 만 열거를 무효화하지 않습니다.


업데이트 : 캐시 된 컬렉션과 원본 컬렉션의 길이도 비교하여 위의 문제를 해결했습니다. 이 수정에서는 사전이 인수로 직접 전달된다고 가정합니다.EnumerableSnapshot 의 생성자에 되고 해당 ID가 다음과 같은 프로젝션에 의해 숨겨지지 않는다고 가정합니다 dictionary.Select(e => e).ΤοEnumerableSnapshot().


중요 : 위 클래스는 스레드 안전 이 아닙니다 . 단일 스레드에서 독점적으로 실행되는 코드에서 사용하도록 고안되었습니다.


0

가입자 사전 객체를 동일한 유형의 임시 사전 객체에 복사 한 다음 foreach 루프를 사용하여 임시 사전 객체를 반복 할 수 있습니다.


(이 게시물은 질문에 대한 양질의 답변 을 제공하지 않는 것 같습니다 . 답변 을 수정하거나 질문에 대한 의견으로 게시하십시오).
sɐunıɔ ןɐ qɐp 2016 년

0

따라서이 문제를 해결하는 다른 방법은 요소를 제거하는 대신 새 사전을 만들고 제거하지 않으려는 요소 만 추가 한 다음 원래 사전을 새 사전으로 바꾸는 것입니다. 나는 이것이 구조를 반복하는 횟수를 늘리지 않기 때문에 이것이 효율성 문제라고 생각하지 않습니다.


0

매우 정교하게 정리 된 하나의 링크가 있으며 솔루션도 제공됩니다. 적절한 해결책을 찾으면 다른 사람이 이해할 수 있도록 여기에 게시하십시오. 주어진 해결책은 게시물과 같으므로 다른 사람은이 해결책을 시도 할 수 있습니다.

당신은 원래 링크를 참조하십시오 :- https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

.Net Serialization 클래스를 사용하여 정의에 열거 가능한 유형 (예 : 컬렉션)이 포함 된 개체를 직렬화하면 코딩이 다중 스레드 시나리오에있는 경우 "컬렉션이 수정되었습니다. 열거 작업이 실행되지 않을 수 있습니다"라는 InvalidOperationException이 쉽게 발생합니다. 가장 큰 원인은 직렬화 클래스가 열거자를 통해 컬렉션을 반복하므로 문제는 컬렉션을 수정하는 동안 반복하는 것입니다.

첫 번째 솔루션은 잠금을 동기화 솔루션으로 사용하여 List 개체에 대한 작업을 한 번에 하나의 스레드에서만 실행할 수 있도록하는 것입니다. 분명히, 객체의 컬렉션을 직렬화하려는 경우 각 객체에 대해 잠금이 적용된다는 성능 저하가 발생합니다.

.Net 4.0은 멀티 스레딩 시나리오를 처리하는 데 편리합니다. 이 직렬화 Collection 필드 문제의 경우 스레드 안전 및 FIFO 수집이며 코드 잠금이없는 ConcurrentQueue (Check MSDN) 클래스의 이점을 얻을 수 있음을 알았습니다.

이 클래스를 사용하면 간단하게 코드에서 수정해야 할 항목이 Collection 유형을 대체하고 Enqueue를 사용하여 ConcurrentQueue 끝에 요소를 추가하고 해당 잠금 코드를 제거합니다. 또는 작업중인 시나리오에 List와 같은 수집 항목이 필요한 경우 필드에 ConcurrentQueue를 적용하려면 몇 가지 코드가 더 필요합니다.

BTW, ConcurrentQueue에는 기본 알고리즘으로 인해 Clear 메서드가 없으므로 컬렉션을 원자 적으로 지울 수 없습니다. 따라서 직접 수행해야하는 가장 빠른 방법은 교체를 위해 비어있는 새 ConcurrentQueue를 다시 만드는 것입니다.


-1

나는 Linq의 .Remove (item)와 함께 열린 dbcontext에있는 익숙하지 않은 코드로 최근 이것을 개인적으로 가로 질러 갔다. 항목이 처음 반복 될 때 제거 되었기 때문에 오류 디버깅을 찾는 데 시간이 많이 걸렸으며 뒤로 물러서서 다시 단계를 시도하면 동일한 컨텍스트에 있었기 때문에 컬렉션이 비어있었습니다!

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.