Исключение SynchronizedCollection InvalidOperationException/System.Исключение ArgumentException

#c# #list #asynchronous #concurrency #synchronization

#c# #Список #асинхронный #параллелизм #синхронизация

Вопрос:

Я написал несколько классов для тестирования многопоточности с помощью SynchronizedCollection .

 class MultithreadTesting
{
    public readonly SynchronizedCollection<int> testlist = new SynchronizedCollection<int>();
    public SynchronizedReadOnlyCollection<int> pubReadOnlyProperty
    {
        get
        {
            return new SynchronizedReadOnlyCollection<int>(testlist.SyncRoot, testlist);
        }
    }

    public void Test()
    {
        int numthreads = 20;
        Thread[] threads = new Thread[numthreads];
        List<Task> taskList = new List<Task>();
        for (int i = 0; i < numthreads / 2; i  )
        {
            taskList.Add(Task.Factory.StartNew(() =>
            {
                for (int j = 0; j < 100000; j  )
                {
                    testlist.Add(42);
                }
            }));
        }

        for (int i = numthreads / 2; i < numthreads; i  )
        {
            taskList.Add(Task.Factory.StartNew(() =>
            {
                var sum = 0;
                foreach (int num in pubReadOnlyProperty)
                {
                    sum  = num;
                }
            }));
        }
        Task.WaitAll(taskList.ToArray());
        testlist.Clear();
    }
}
  

для его запуска я использую

     MultithreadTesting test = new MultithreadTesting();
    while (true)
        test.Test();
  

Но код бросает меня System.ArgumentException: 'Destination array was not long enough. Check destIndex and length, and the array's lower bounds.'

Если я попытаюсь использовать testlist в foreach , я получу

 System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'
  

Однако MSDN сообщает нам

Класс SynchronizedReadOnlyCollection

Предоставляет потокобезопасную коллекцию, доступную только для чтения, которая содержит объекты типа, указанного универсальным параметром в качестве элементов.

Ответ №1:

Основная причина ошибки заключается в том, что List<T> построение не является потокобезопасным.

Давайте посмотрим, что происходит при создании нового SynchronizedReadOnlyCollection . Исключение возникает в следующей строке:

 return new SynchronizedReadOnlyCollection<int>(testlist.SyncRoot, testlist);
  

Как сообщает нам трассировка стека исключений, в процессе построения List<T>..ctor участвует:

 at System.Collections.Generic.SynchronizedCollection`1.CopyTo(T[] array, Int32 index)
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Collections.Generic.SynchronizedReadOnlyCollection`1..ctor(Object syncRoot, IEnumerable`1 list)
  

Следующий фрагмент из List<T> конструктора показывает, где происходит ошибка. Код скопирован из MS reference source, я убрал ненужные части кода для облегчения чтения. Пожалуйста, обратите внимание, что между комментариями (1) и (2) есть другие потоки, манипулирующие сбором:

 public List(IEnumerable<T> collection) {
    ICollection<T> c = collection as ICollection<T>;
    // (1) count is now current Count of collection
    int count = c.Count;
    // other threads can modify collection meanwhile
    if (count == 0)
    {
        _items = _emptyArray;
    }
    else {
        _items = new T[count];
        // (2) SynchronizedCollection.CopyTo is called (which itself is thread-safe)
        // Collection can still be modified between (1) and (2) 
        // No when _items.Count != c.Count -> Exception is raised.
        c.CopyTo(_items, 0);
        _size = count;
    }
}
  

Решение

Проблема может быть легко устранена с помощью testlist модификации блокировки при создании нового SynchronizedReadOnlyCollection .

 public SynchronizedReadOnlyCollection<int> pubReadOnlyProperty
{
    get
    {
        lock (testlist.SyncRoot)
        {
            return new SynchronizedReadOnlyCollection<int>(testlist.SyncRoot, testlist);
        }
    }
}