개요
- 주요 자료구조로 사용되는 List, Dictionary에 대해, 어느 상황에서 Collection was modified가 발생하는지 확인
- 그리고 어떻게 해결할 수 있는지 확인
코드
public void CollectionModification()
{
var dictionary = new Dictionary<int, int>() { { 0, 1 }, { 1, 1 }, { 2, 1 } };
DictionaryAdd(dictionary);
DictionaryRemove(dictionary);
DictionaryUpdate(dictionary);
var list = new List<int>() {0, 1, 2};
ListAdd(list);
ListRemove(list);
}
public void DictionaryAdd(Dictionary<int, int> dictionary)
{
try
{
foreach (var kvp in dictionary)
{
dictionary.Add(3, 1);
}
}
catch (Exception e)
{
Console.WriteLine($"Message: {e.Message}, DictionaryAdd");
}
}
public void DictionaryRemove(Dictionary<int, int> dictionary)
{
try
{
foreach (var kvp in dictionary)
{
dictionary.Remove(0);
}
}
catch (Exception e)
{
Console.WriteLine($"Message: {e.Message}, DictionaryRemove");
}
}
public void DictionaryUpdate(Dictionary<int, int> dictionary)
{
try
{
foreach (var kvp in dictionary)
{
dictionary[1] = 3;
}
}
catch (Exception e)
{
Console.WriteLine($"Message: {e.Message}, DictionaryUpdate");
}
}
public void ListAdd(List<int> list)
{
try
{
foreach (var item in list)
{
list.Add(1);
}
}
catch (Exception e)
{
Console.WriteLine($"Message: {e.Message}, ListAdd");
}
}
public void ListRemove(List<int> list)
{
try
{
foreach (var item in list)
{
list.Remove(1);
}
}
catch (Exception e)
{
Console.WriteLine($"Message: {e.Message}, ListRemove");
}
}
출력
Message: Collection was modified; enumeration operation may not execute., DictionaryAdd
Message: Collection was modified; enumeration operation may not execute., ListAdd
Message: Collection was modified; enumeration operation may not execute., ListRemove
- Dictionary의 경우 Add, List의 경우 Add, Remove에서 발생
해결?
- Dictionary.Add, List.Add에 대해 Concurrent 자료구조를 사용
- Exception은 발생하지 않지만, Count가 계속 증가하기 때문에 무한루프 발생
public void DictionaryAddSafe(Dictionary<int, int> dictionary)
{
try
{
var temp = 3;
var concurrentDictionary = new ConcurrentDictionary<int, int>(dictionary);
foreach (var kvp in concurrentDictionary)
{
concurrentDictionary.TryAdd(temp, 1);
temp++;
}
}
catch (Exception e)
{
Console.WriteLine($"Message: {e.Message}, DictionaryAddSafe");
}
}
public void ListAddSafe(List<int> list)
{
try
{
var concurrentBag = new ConcurrentBag<int>(list);
foreach (var item in concurrentBag)
{
list.Add(1);
}
}
catch (Exception e)
{
Console.WriteLine($"Message: {e.Message}, ListAddSafe");
}
}
해결
- List.Remove의 경우 for 루프로 변경
- 역순으로 순회해서 index가 밀리지 않음
public void ListRemoveSafe(List<int> list)
{
try
{
for (var i = list.Count-1; i >= 0 ; i--)
{
list.Remove(1);
}
}
catch (Exception e)
{
Console.WriteLine($"Message: {e.Message}, ListRemoveSafe");
}
}
멀티쓰레드 환경이라면?
- lock 또는 Concurrent 자료구조 사용
public void DictionaryAddMutiThreadSafe1(Dictionary<int, int> dictionary)
{
Task.Run(() =>
{
Thread.Sleep(500);
lock (dictionary)
{
dictionary.Add(3, 1);
}
});
lock (dictionary)
{
foreach (var kvp in dictionary)
{
Thread.Sleep(1000);
}
}
}
public void DictionaryAddMutiThreadSafe2(Dictionary<int, int> dictionary)
{
var concurrentDictionary = new ConcurrentDictionary<int, int>(dictionary);
Task.Run(() =>
{
Thread.Sleep(500);
concurrentDictionary.TryAdd(3, 1);
});
foreach (var kvp in concurrentDictionary)
{
Thread.Sleep(1000);
}
}
결론
- Add의 경우 Concurrent 자료구조로 Exception은 막을 수 있지만, 무한루프가 발생하기 때문에 for, foreach 내부에서는 Add를 안하는게 바람직해 보임