Форум программистов, компьютерный форум, киберфорум
C# для начинающих
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
Рейтинг 4.67/15: Рейтинг темы: голосов - 15, средняя оценка - 4.67
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234

System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."

30.03.2019, 03:25. Показов 3289. Ответов 16
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Имеется две вебсокет подписки, где data1 и data2 - часто приходящие данные типа SortedDictionary<decimal, decimal>. Во второй подписке обращаясь к первому элементу словаря иногда ловлю ошибку System.InvalidOperationException. Конструкция lock почему-то не синхронизирует доступ к dic. Почему такое случается? Что делаю не так?

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public partial class Form1 : Form
    {
        static object locker = new object();
        SortedDictionary<decimal, decimal> dic1 = new SortedDictionary<decimal, decimal>();
 
        decimal temp = 0;
 
        public Form1()
        {
            InitializeComponent();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            var sub1 = subscribe1(data1 =>
            {
                lock (locker)
                    dic1 = data1;
            });
 
            var sub2 = subscribe2(data2 =>
            {
                lock (locker)
                    temp = dic1.First().Key; // System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."
 
                decimal result = data2.First().Key * temp;
            });
        }
    }
0
Лучшие ответы (1)
Programming
Эксперт
39485 / 9562 / 3019
Регистрация: 12.04.2006
Сообщений: 41,671
Блог
30.03.2019, 03:25
Ответы с готовыми решениями:

System.InvalidOperationException: 'Коллекция была изменена после создания экземпляра перечислителя.'
Почему обращаясь к скопированному объекту (в конструкции Lock()), я ловлю ошибку &quot;Коллекция была изменена после создания экземпляра...

Коллекция была изменена после создания экземпляра перечислителя
foreach (var entry in new SortedDictionary&lt;string, Macro&gt;(macros)) В почему в цикле foreach возникает эта ошибка, ведь я создаю...

Max() ругается что коллекция была изменена
Добрый день! Выскакивает исключение ValueTradeList.Max(); Коллекция была изменена. public class Strategy { ...

16
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16140 / 11264 / 2888
Регистрация: 21.04.2018
Сообщений: 33,109
Записей в блоге: 2
30.03.2019, 11:09
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Конструкция lock почему-то не синхронизирует доступ к dic. Почему такое случается? Что делаю не так?
lock не синхронизирует, а блокирует доступ к переменной для других потоков.
Действие конструкции распространяется только на её тело.
А у Вас в теле lock только запись выражения LINQ по коллекции dic1.
Эта коллекция создаётся в другом lock.
Блокировка lock создаёт очередь из потоков обращающихся к переменной.
Если за время обработки sub1 произойдёт обращение из другого потока, то он будет ждать в очереди.
После разблокировки доступ получит следующий из очереди. А при повторной блокировке для sub2 этот поток станет в очередь последним. Все потоки которые его ждали будут выполнены до него.

Что бы такого не было sub1 и sub2 должны вычисляться в одном lock.
1
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
30.03.2019, 14:57  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
А у Вас в теле lock только запись выражения LINQ по коллекции dic1.
Не понял этот момент - разве конструкция
C#
1
2
3
4
lock (locker)
{
    temp = dic1.First().Key;
}
не блокирует содержимое (доступ к dic1)?

Т.е., я думал, что когда программа "зашла" в тело блокировки (в подписке sub2), то в другом потоке (в подписке sub1) программа "не зайдет" в конструкцию
C#
1
2
3
4
lock (locker)
{
       dic1 = data1;
}
до тех пор, пока программа не выйдет из тела блокировки (в подписке sub2).
На счёт очередности обращения вроде всё понимаю.

Цитата Сообщение от Элд Хасп Посмотреть сообщение
Что бы такого не было sub1 и sub2 должны вычисляться в одном lock.
Этот момент тоже не понял.. Я только знаю что необходимо использовать один и тот же экземпляр для блокировки (в моём случае статическая переменная locker типа object), что гарантирует очередность обращения к переменной из разных потоков.
0
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
30.03.2019, 15:28  [ТС]
Накидал небольшой пример, повторяющий мою проблему в целом. Что-то где-то я недопонимаю.. Подскажите, пожалуйста где мои действия неверны.
Вложения
Тип файла: rar LockTest.rar (128.8 Кб, 4 просмотров)
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16140 / 11264 / 2888
Регистрация: 21.04.2018
Сообщений: 33,109
Записей в блоге: 2
30.03.2019, 16:21
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Т.е., я думал, что когда программа "зашла" в тело блокировки (в подписке sub2), то в другом потоке (в подписке sub1) программа "не зайдет" в конструкцию
Нет! Блокировка действует только внутри lock
C#
1
2
3
4
5
6
7
8
9
10
lock (locker) // locker заблокирован
{
       dic1 = data1;
}// locker разблокирован. 
// Если в очереди на блокировку стоит другой поток
// то теперь он блокирует
lock (locker) // locker заблокирован, но по очереди последним
{
    temp = dic1.First().Key;
}// locker разблокирован
Добавлено через 1 минуту
Правильно так (если позволяет алгоритм)
C#
1
2
3
4
5
lock (locker)
{
    dic1 = data1;
    temp = dic1.First().Key;
}
1
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
30.03.2019, 16:36  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Нет! Блокировка действует только внутри lock
Именно так я и представлял этот процесс.. Но всё равно не понимаю почему программу работает не так, как ожидается..
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16140 / 11264 / 2888
Регистрация: 21.04.2018
Сообщений: 33,109
Записей в блоге: 2
30.03.2019, 16:59
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Накидал небольшой пример, повторяющий мою проблему в целом.
Я по Вашему коду не совсем понял его смысла.
Ошибки в нём не возникает.
Насколько понял проблема в этом участке
C#
22
23
24
25
26
27
28
29
30
31
32
33
34
35
            var sub1 = subscribe(data1 =>
            {
                lock (locker) // блокируется locker только для операции присвоения ссылки
                    dic1 = data1; // такая блокировка бессмысленна
            });
 
            var sub2 = subscribe(data2 =>
            {
                lock (locker) // блокируется locker только для получения значения
                    temp = dic1.Bids.Last().Key; // System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."
 
                decimal result = data2.Bids.Last().Key * temp;
                Console.WriteLine(result);
            });
В строке 31 перечислитель создаётся перед методом Last()

Попробуйте так:
C#
22
23
24
25
26
27
28
29
30
31
32
33
34
35
            var sub1 = subscribe(data1 =>
            {
             //   lock (locker)
                    dic1 = data1;
            });
 
            var sub2 = subscribe(data2 =>
            {
                lock (dic1.Bids /*locker*/)
                    temp = dic1.Bids.Last().Key; // System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."
 
                decimal result = data2.Bids.Last().Key * temp;
                Console.WriteLine(result);
            });
Если не поможет - разъясните что делает у Вас метод subscribe.
Если пойму его смысл, возможно, найду другое решение.

Добавлено через 6 минут
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Именно так я и представлял этот процесс.. Но всё равно не понимаю почему программу работает не так, как ожидается..
Вы переменную locker нигде не используете.
А другой поток останавливается в очереди только если он хочет обратиться к этой переменной.
Хотя бы вот так делайте
C#
1
2
3
4
5
6
7
8
9
10
lock (locker)
{
    locker = new object();
    dic1 = data1;
}
lock (locker)
{
    locker = new object();
    temp = dic1.First().Key;
}
Добавлено через 3 минуты
То есть должно быть какое-то обращение к блокированной переменной. Так как очередь блокировки возникает именно при обращении к этой переменной.

Добавлено через 39 секунд
Допустим попробуйте так
C#
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
        static void Main(string[] args)
        {
            dictionaries dic1 = new dictionaries();
            dic1.Asks.Add(0, 0);
            dic1.Bids.Add(0, 0);
 
            decimal temp = 0;
 
            var sub1 = subscribe(data1 =>
            {
                lock (locker)
                {
                    locker.GetType();
                    dic1 = data1;
                }
            });
 
            var sub2 = subscribe(data2 =>
            {
                lock (locker)
                {
                    locker.GetType();
                    temp = dic1.Bids.Last().Key; // System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."
                }
 
                decimal result = data2.Bids.Last().Key * temp;
                Console.WriteLine(result);
            });
 
            Console.ReadKey();
        }
Добавлено через 1 минуту
Но первая блокировка смысла не имеет. Присвоение ссылки другой поток никак нарушить не может.
1
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
30.03.2019, 22:05  [ТС]
Я совсем запутался теперь..

Вами предложенное решение делать так:
C#
1
2
3
4
5
lock (locker)
{
    dic1 = data1;
    temp = dic1.First().Key;
}
в принципе частично решает мою проблему. Теперь в других потоках я обращаюсь к уже готовой переменной temp, а не извлекаю её путём небезопасного обращения dic1.First().Key там где нужно это значение. Спасибо что натолкнули на такое решение) Но это больше костыль, и мне всё же хочется разобраться в этой теме.

Я также обращаюсь из других потоков к переменной dic1 в цикле foreach таким образом:
C#
1
2
3
foreach (var item in dic1.Bids.ToArray())
{
}
Проблем не возникает (данное решение ".ToArray()" вычитал на stackoverflow, помогло, в отличие от обычного перебора коллекции foreach (var item in dic1.Bids), где возникает ошибка System.InvalidOperationException)

Суть: Имеется биржа. subscribe - это websocket подписка на обновление ордеров в стакане (удаление, замена, запись и т.д.) Это я симитировал в коде
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
            
Task.Run(() =>
            {
                Random rnd = new Random();
                int countIteration = 0;
                dictionaries newDic = new dictionaries(); ;
                while (true)
                {
                    countIteration = rnd.Next(1, 20);
 
                    if (countIteration == 10)
                        newDic = new dictionaries();
 
                    for (int i = 0; i < countIteration; i++)
                    {
                        if (newDic.Asks.ContainsKey(i))
                            newDic.Asks.Remove(i);
                        else
                            newDic.Asks.Add(i, i);
 
                        if (newDic.Bids.ContainsKey(i))
                            newDic.Bids.Remove(i);
                        else
                            newDic.Bids.Add(i, i);
                    }
 
                    update.Invoke(newDic);
 
                    Thread.Sleep(10);
                }
            });
Как только приходит обновление с сервера (удалился ордер, или новый появился и т.д. в этом участке кода формирую стакан с актуальными данными в виде сортированного словаря и с помощью Invoke передаю эти данные в блоки subscribe)

Имеется несколько потоков, которые пользуются этими данными (Нужно вытащить актуальную цену, посчитать объем и т.д.) Это как раз я и пытался сделать с помощью
C#
1
2
3
4
lock (locker)
{
    temp = dic1.First().Key;
}
Только у меня это получилось не потокобезопасно, в отличие от данного подхода
C#
1
foreach (var item in dic1.Bids.ToArray())
Миниатюры
System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."   System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."  
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16140 / 11264 / 2888
Регистрация: 21.04.2018
Сообщений: 33,109
Записей в блоге: 2
31.03.2019, 01:42
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Имеется несколько потоков, которые пользуются этими данными (Нужно вытащить актуальную цену, посчитать объем и т.д.) Это как раз я и пытался сделать с помощью
Реализация зависит от того какой объём вычислений за этим стоит.
Потокобезопасно, не потококобезопасно - разницы нет, если, в принципе, объём вычислений такой, что обработка не успевает за сообщениями сервера.

Сервер биржи присылает сообщения. В каждом сообщении (если оно с данными) есть штамп времени.

Если сообщений много, то учтите, что вполне возможен вариант когда старые сообщения приходят позже новых.
Так как каждое сообщение имеет случайное время прохождения по интернету.
Я тоже сейчас пытаюсь с BitMex разобраться. Так там порой разница в задержке бывает до 50 миллисекунд.
А штамп времени порой вообще у 2-5 сообщений одинаковый.
Приходится делать промежуточный буфер сообщений сервера с накоплением в 50 мс.
А из буфера выбирать сообщения для обработки анализируя их тип и штамп времени.

После того как сообщения выстроены в том порядке как создавались, их надо обрабатывать последовательно. Ни каких параллельных потоков здесь не будет.

Пример.
У Вас список Ордеров. Приходит метод iserpt - вставка нового Ordera. После него сразу (иногда с тем же штампом времени) метод update - изменяющий этот Order.
Вот теперь представьте, если обрабатывать это в параллельных потоках. Скорость каждого потока не предсказуема. Поэтому даже, если Вы создадите поток для update позже чем поток для iserpt, всё равно, update может выполниться раньше. А что update может изменить в коллекции ордеров? Ведь ордера, который он должен изменить, в коллекции ещё не создано!
Будете ловить непредсказуемые случайные баги.

Я каждую подписку (то есть фактически каждый набор данных) обрабатываю в одном потоке. Вернее в очереди потоков - потоков много, но выполняются они последовательно друг за другом. А разные подписки могут обрабатываться в разных параллельных потоках, так как наборы данных у них независимы.

Это только Локальная Модель. А потом ещё всё это надо синхронизировать в ViewModel и вывести в основном потоке UI элементов.

Если же биржа большая и книга ордеров большая, то изменения очень частые и большие. Это легко может превысить возможности типичного домашнего или офисного компьютера. В сообщениях от сервера может быть много ненужной информации её можно полностью или частично отсеивать каким-нибудь простым быстроработающим фильтром.

Но конкретных рекомендаций быть не может. Это всё очень индивидуально и зависит от очень многих причин.

Добавлено через 25 минут
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Только у меня это получилось не потокобезопасно, в отличие от данного подхода
foreach по массиву dic1.Bids.ToArray() тоже не потокобезопасно.
Метод .ToArray() - это тоже метод расширения LINQ.
Есть маленькая вероятность, что во время его исполнения коллекция измениться и получите баг. Причём баг может быть "неуловимый" и "неопределяемый". То есть данные у не правильные, но приложение этого не замечает и работает с неверными данными.

А Вы создаёте приложение для Биржи - для работы с реальными деньгами! Такие баги могут быть очень опасны, так как пользователь может попасть на реальные деньги.

Как минимум надо массив получать в заблокированном состоянии.
Что-то в таком духе
C#
1
2
3
4
5
6
7
KeyValuePair<decimal, decimal>[] bids;
lock(dic1.Bids)
    bids=dic1.Bids.ToArray();
foreach (var item in bids)
{
     // Тело цикла
}
Таким образом можно зафиксировать коллекцию, но не сами элементы коллекции.
Но это надо по алгоритму обработки смотреть может ли быть изменение элемента коллекции вол время цикла и может ли это привести к неправильным результатам.

В данном случае KeyValuePair - это структура, то есть значимый тип. При таком создании дубля каждый элемент KeyValuePair тоже скопируется по значению. И экземпляры в массиве будут хоть и копиями, но не теми же что в исходном словаре.

Но в другом случае, если в исходной коллекции классы, то экземпляры, при таком копировании, будут одни и те же. Скопированы будут только ссылки на них. И изменение экземпляра в параллельном потоке моет привести к неверным результатам.

Добавлено через 9 минут
DEMON_RUS, и я не получаю бага как на Ваших скринах.
Но и на консоль ничего приложение не выводит.
Может Вы что-то не выложили в тему?

Добавлено через 4 минуты
Странно....
Прогнал в пошаговой отладке - вроде заработало...

Добавлено через 5 минут
Выдаёт набор чисел, но не всегда. В трети случаев (примерно) - пустая консоль.
Запускал раз двадцать. Баг ни разу не выходил.
1
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
31.03.2019, 07:36  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Выдаёт набор чисел, но не всегда. В трети случаев (примерно) - пустая консоль.
Запускал раз двадцать. Баг ни разу не выходил.
Хмм.. проект предоставил как есть, у меня через 5-10 секунд строго выкидывает исключение. Может характеристики ПК решают? Попробуйте убрать либо ещё уменьшить задержку после update.Invoke(newDic); (Там стоит Thread.Sleep(10); ) Напротив, увеличив задержку до 100 млсек исключение уже не так часто бросается.
Набор чисел - это некая имитация выдаваемых сформированных ордерБуков из вебсокета.
Раз Вы в теме, накидаю аналогичный пример для бинанса, чтоб показать, какого результата я хочу добиться.

На счёт ужаса, который творится на BitMex - на других биржах я вроде такого благо не встречал (бинанс, гейт, кукоин). Для этих бирж использую хороший пример с официальной документации бинанса Только немного его переделав для удобства использования.

Добавлено через 45 секунд
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Как минимум надо массив получать в заблокированном состоянии.
Думаю это мне пригодится, спасибо)
0
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
31.03.2019, 09:07  [ТС]
Накидал пример на основе бинанса, там приведены все мои проблемы, с которыми не могу разобраться..

Основная суть - в сторонних задачах (привел примеры в коде) мне необходимо всячески пользоваться ордерБуками, сформированными в методе BuildLocalDepthCache (ордерБуки реалтайм обновляются сокетами).
Подскажите, пожалуйста, какие ошибки я допускаю при таких подходах?
Вложения
Тип файла: rar BinanceDotNet-master.rar (12.50 Мб, 2 просмотров)
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16140 / 11264 / 2888
Регистрация: 21.04.2018
Сообщений: 33,109
Записей в блоге: 2
31.03.2019, 14:22
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Может характеристики ПК решают?
В этом и проблема отлавливания багов многопоточности. На это очень много что влияет: конфигурация компьютера, ОС, версии, апгрейты, приложения работающие в фоне и т.д.
Надо приложение делать сразу с учётом возможного наложения потоков, а не просто реагировать на проявление того или иного бага.

Работая с перечислителем (а это и foreach, и LINQ) надо учитывать
Цитатаhttps://msdn.microsoft.com/ru-... 2147217396

Оператор foreach языка C# (for each в C++, For Each в Visual Basic) позволяет скрыть сложный механизм перечисления. Поэтому рекомендуется вместо непосредственного использования перечислителя применять ключевое слово foreach.

Перечислители могут использоваться для чтения данных коллекции; они не могут использоваться для изменения коллекции.

Изначально перечислитель располагается перед первым элементом коллекции. В этой позиции значение Current не определено. Поэтому необходимо вызвать метод MoveNext, чтобы переместить перечислитель к первому элементу коллекции до считывания значения свойства Current.

Метод Current возвращает один и тот же объект до тех пор, пока не будет вызван метод MoveNext. Метод MoveNext присваивает свойству Current следующий элемент.

Если метод MoveNext достигает конца коллекции, перечислитель располагается после последнего элемента коллекции, а MoveNext возвращает значение false. Когда перечислитель находится в данном месте, последующие вызовы метода MoveNext также возвращают false. Если при последнем вызове метода MoveNext было возвращено значение false, свойство Current не будет определено. Значением Current не может снова стать первый элемент коллекции; вместо этого следует создать новый экземпляр перечислителя.

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

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

Используемые по умолчанию реализации коллекций в пространстве имен System.Collections.Generic не синхронизированы.

То есть надо обеспечить неизменность коллекции перечислитель которой используется. Для этого надо либо блокировать коллекцию от изменений (ReaderWriterLockSlim), либо делать копию коллекции (в том числе через LINQ метод ToArray).

Добавлено через 8 минут
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
На счёт ужаса, который творится на BitMex
Это проблема ни самой биржи.
Она-то нормально выдаёт сообщения. Но сообщения идут через инет, через чёрт знает сколько промежуточных узлов. И у каждого сообщения возникает индивидуальная задержка. Задержка эта случайна (в определённых рамках, конечно).
У меня реально разница, хоть и редко, порой доходит до нескольких десятков миллисекунд.
Между соседними сообщениями (они идут по одному пути) несколько единиц миллисекунд.

Добавлено через 49 минут
По коду первого Вашего теста.
Ещё раз объясню, может не ясно выразил:

1) Блокировать locker бессмысленно. Потоки останавливаются при попытке доступа к заблокированному объекту. Обратите внимание ОБЪЕКТУ, а не ССЫЛКЕ! Объект (экземпляр) locker это пустой new object(). Он ни где не используется. Поэтому никакой блокировки по locker быть не может. Считайте что этих строк у Вас нет, они игнорируются при исполнении.

2) В первом sub1 = subscribe.... у Вас происходит изменение ССЫЛКИ, а не ОБЪЕКТА здесь тоже ни какой блокировки в принципе не может быть. Поэтому здесь блокировка, вообще, не нужна.

3) В методе subscribe у Вас первым идёт оператор Random rnd = new Random();. Разница (во времени) между двумя вызовами subscribe очень мала. А особенность new Random() это, то что он при вызовах подряд будет выдавать одинаковую последовательность. Вы, по-моему, этого не учитывали. Или у Ваш комп чем-то перегружен и между вызовами subscribe проходит достаточно времени для разной инициализации Random. Или это не важно для вашего примера.
Время, точно не помню, между вызовами должно быть > 20 мс.

4)Во втором Action Вы используете коллекцию Bids. Блокировать надо именно её. И не только здесь, но и в потоке где она изменяется, то есть в методе subscribe.
Так же перед получением Last надо проверять коллекцию Bids на пустоту иначе возможно исключение (у меня вылетает иногда по этой причине) или гарантировать, что она заполнена. Пустой она может быть если последний элемент удалён в методе subscribe.

5) После моих корректировок вот полученный код:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
 
namespace LockTest
{
    class Program
    {
        static object locker = new object();
 
        static void Main(string[] args)
        {
            dictionaries dic1 = new dictionaries();
            dic1.Asks.Add(0, 0);
            dic1.Bids.Add(0, 0);
 
            decimal temp = 0;
 
            var sub1 = subscribe(data1 =>
            {
                //lock (locker)
                dic1 = data1;
            });
 
            Thread.Sleep(100);
 
            var sub2 = subscribe(data2 =>
            {
                //lock (locker)
                lock (dic1.Bids)
                {
                    if (dic1.Bids.Any())
                        temp = dic1.Bids.Last().Key; // System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."
                    else
                        temp = -1;
                }
 
                decimal result;
                if (data2.Bids.Any())
                    result = data2.Bids.Last().Key * temp;
                else
                    result = 1000;
                Console.WriteLine(result);
            });
 
            Console.ReadKey();
        }
 
        public static bool subscribe(Action<dictionaries> update)
        {
            Task.Run(() =>
            {
                Random rnd = new Random();
                int countIteration = 0;
                dictionaries newDic = new dictionaries();
                while (true)
                {
                    countIteration = rnd.Next(1, 20);
 
                    if (countIteration == 10)
                        newDic = new dictionaries();
 
                    for (int i = 0; i < countIteration; i++)
                    {
                        lock (newDic.Asks)
                        {
                            if (newDic.Asks.ContainsKey(i))
                                newDic.Asks.Remove(i);
                            else
                                newDic.Asks.Add(i, i);
                        }
 
                        lock (newDic.Bids)
                        {
                            if (newDic.Bids.ContainsKey(i))
                                newDic.Bids.Remove(i);
                            else
                                newDic.Bids.Add(i, i);
                        }
                    }
 
                    update.Invoke(newDic);
 
                    Thread.Sleep(1);
                }
            });
            return true;
        }
 
        public class dictionaries
        {
            public SortedDictionary<decimal, decimal> Asks = new SortedDictionary<decimal, decimal>();
            public SortedDictionary<decimal, decimal> Bids = new SortedDictionary<decimal, decimal>();
        }
    }
}
Добавлено через 26 минут
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Подскажите, пожалуйста, какие ошибки я допускаю при таких подходах?
Ошибки те же, что в простом тесте. Если возможно изменение коллекции, то перед изменением/чтением надо её блокировать.
Оператор lock проще, но при большом количестве потоков, чтобы не создавать лишних "тормозов" лучше использовать ReaderWriterLockSlim.

Добавлено через 9 минут
И у Вас непривычный мне стиль программирования. Какие-то бесконечные циклы...
Должно быть так:
1) Часть приложения обрабатывающая сообщения от сервера. При внесении имения в данные вызывается событие извещающее об этом.
2) Часть приложения обрабатывающая имения в данных. Она подписана на событие в первой части. При возникновении события, проверяет что за изменения и вызывает, соответствующий, собственный метод.
3) Часть отвечающая за отображение данных и их подготовку к отображению. Она реагирует на изменения во второй части.

Первые две относятся к локальной Mодели. Их можно сделать в разных классах или в одном.
Третья часть это View и ViewModel.
1
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
01.04.2019, 15:56  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
1) Блокировать locker бессмысленно. Потоки останавливаются при попытке доступа к заблокированному объекту. Обратите внимание ОБЪЕКТУ, а не ССЫЛКЕ! Объект (экземпляр) locker это пустой new object(). Он ни где не используется. Поэтому никакой блокировки по locker быть не может. Считайте что этих строк у Вас нет, они игнорируются при исполнении.
Моё понимание о конструкции Lock было сформировано из данной статьи https://metanit.com/sharp/tutorial/11.4.php (в примере блокируется не "X", а экземпляр Locker) а также из видео данного ресурса https://itvdn.com/ru/video/csh... al/threads (Техника синхронизации доступа к ресурсу - таймкод 1:37:10) Речь идёт именно о блокировке секции, используя "пустой new object()".

Цитата Сообщение от Элд Хасп Посмотреть сообщение
А особенность new Random() это... Или это не важно для вашего примера.
Да, это не имело значения, использовалось лишь в качестве имитации потока случайных данных. На счёт ">20 мc" не знал, но сталкивался как-то с таким поведением. Теперь знаю, спасибо)

Цитата Сообщение от Элд Хасп Посмотреть сообщение
И у Вас непривычный мне стиль программирования. Какие-то бесконечные циклы...
Это тоже использовалось в качестве примера. (Хоть я и действительно использую циклы - в каждой итерации берутся актуальные данные (стаканы) из нескольких вебсокет подписок, и над ними делаются вычисления, на следующей итерации всё это повторяется, даже если стаканы не изменились). Событийная модель на начальном этапе даже в голову не приходила, когда на бирже присутствовали только REST Api. В итоге этот подход так и тянется за мной..) Правда с освоением websocket понемногу перехожу на работу с событиями)
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16140 / 11264 / 2888
Регистрация: 21.04.2018
Сообщений: 33,109
Записей в блоге: 2
02.04.2019, 01:03
Лучший ответ Сообщение было отмечено DEMON_RUS как решение

Решение

Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Моё понимание о конструкции Lock было сформировано из данной статьи https://metanit.com/sharp/tutorial/11.4.php (в примере блокируется не "X", а экземпляр Locker) а также из видео данного ресурса https://itvdn.com/ru/video/csh... al/threads (Техника синхронизации доступа к ресурсу - таймкод 1:37:10) Речь идёт именно о блокировке секции, используя "пустой new object()".
Но посмотрите как там сделано.
Перед тем как получить доступ к определённому участку кода - используется блокировка-заглушка.
А где в Вас доступ к данным?
Он у Вас у внутри метода subscribe, а заглушка стоит снаружи.
Если так уж хотите реализовать через заглушку, то Вам надо передать ссылку на неё внутрь метода и там реализовывать блокировку по заглушке.
На мой взгляд, "притянуто за уши".
Я сейчас попробую такой вариант накидать, но он мне концептуально очень не нравится. В данном случае, так как метод внутренний можно использовать приватное поле уровня класса. Но это по сути "костыль":
- А если надо вызвать метод из другого класса?
- Придётся блокировать полностью все потоки, а не только обращения к изменяемым данным.
- Невозможно будет сделать блокировку только для записи. Ведь читающие потоки смысла блокировать, в данном случае, нет.

Добавлено через 2 минуты
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
    class Program
    {
        static object locker = new object();
 
        static void Main(string[] args)
        {
            dictionaries dic1 = new dictionaries();
            dic1.Asks.Add(0, 0);
            dic1.Bids.Add(0, 0);
 
            decimal temp = 0;
 
            var sub1 = subscribe(data1 =>
            {
                //lock (locker)
                dic1 = data1;
            });
 
            Thread.Sleep(100);
 
            var sub2 = subscribe(data2 =>
            {
                lock (locker)
                //lock (dic1.Bids)
                {
                    if (dic1.Bids.Any())
                        temp = dic1.Bids.Last().Key; // System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."
                    else
                        temp = -1;
                }
 
                decimal result;
                if (data2.Bids.Any())
                    result = data2.Bids.Last().Key * temp;
                else
                    result = 1000;
                Console.WriteLine(result);
            });
 
            Console.ReadKey();
        }
 
        public static bool subscribe(Action<dictionaries> update)
        {
            Task.Run(() =>
            {
                Random rnd = new Random();
                int countIteration = 0;
                dictionaries newDic = new dictionaries();
                while (true)
                {
                    countIteration = rnd.Next(1, 20);
 
                    if (countIteration == 10)
                        newDic = new dictionaries();
 
                    for (int i = 0; i < countIteration; i++)
                    {
                        lock (locker) // Блокировка вся и всё! А нужно только Asks или Bids
                        //lock (newDic.Asks)
                        {
                            if (newDic.Asks.ContainsKey(i))
                                newDic.Asks.Remove(i);
                            else
                                newDic.Asks.Add(i, i);
                        //}
 
                        //lock (newDic.Bids)
                        //{
                            if (newDic.Bids.ContainsKey(i))
                                newDic.Bids.Remove(i);
                            else
                                newDic.Bids.Add(i, i);
                        }
                    }
 
                    update.Invoke(newDic);
 
                    Thread.Sleep(1);
                }
            });
            return true;
        }
 
        public class dictionaries
        {
            public SortedDictionary<decimal, decimal> Asks = new SortedDictionary<decimal, decimal>();
            public SortedDictionary<decimal, decimal> Bids = new SortedDictionary<decimal, decimal>();
        }
    }
Добавлено через 3 минуты
Видите, что получается?
У Вас блокировка нужна в двух методах: subscribe и в анонимном Action.

Добавлено через 4 минуты
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
Событийная модель на начальном этапе даже в голову не приходила, когда на бирже присутствовали только REST Api.
Обычно не REST запросы бывает ограничения. На BitMex допустим не более 300 в 5 минут. А информации надо получать много и часто. В лимите удержаться не получается.
Поэтому большую часть информации надо получать по websocket.
По REST идёт только отправка ордеров.
1
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
02.04.2019, 12:55  [ТС]
Вроде стало понятнее всё, разобрался, спасибо)
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Поэтому большую часть информации надо получать по websocket.
К сожалению (или наоборот) не все биржи поддерживают websocket.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16140 / 11264 / 2888
Регистрация: 21.04.2018
Сообщений: 33,109
Записей в блоге: 2
02.04.2019, 15:27
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
К сожалению (или наоборот) не все биржи поддерживают websocket.
Я даже не представляю как без websocket биржа может рассылать информацию.
REST очень накладный (для сервера) в этом отношении.
При более мене значимом количестве клиентов REST не справится.
Цитата Сообщение от DEMON_RUS Посмотреть сообщение
(Хоть я и действительно использую циклы - в каждой итерации берутся актуальные данные (стаканы) из нескольких вебсокет подписок, и над ними делаются вычисления, на следующей итерации всё это повторяется, даже если стаканы не изменились). Событийная модель на начальном этапе даже в голову не приходила, когда на бирже присутствовали только REST Api. В итоге этот подход так и тянется за мной..)
Даже если Вы делаете всё через REST, надо делать не через бесконечные циклы, а через таймеры. То есть всё равно событийная модель. Такая модель является основной для Net платформы.
0
10 / 1 / 0
Регистрация: 06.08.2018
Сообщений: 8
04.04.2019, 21:56
Элд Хасп, Тут очень грамотно реализована система. https://github.com/Marfusios/b... -websocket
1
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
inter-admin
Эксперт
29715 / 6470 / 2152
Регистрация: 06.03.2009
Сообщений: 28,500
Блог
04.04.2019, 21:56
Помогаю со студенческими работами здесь

Коллекция была изменена; невозможно выполнить операцию перечисления
Проблема очень простая, иду foreach 'ем по коллекции в которую в это же время добавляю элементы, как переделать чтобы ошибка не вылетала?

Невозможно выполнить операцию перечисления. Коллекция была изменена
Доброго времени суток! Возникла такая ошибка - при добавлении элемента в очередь (таймер постоянно запущен) компилятор выдает ошибку -...

Коллекция была изменена; невозможно выполнить операцию перечисления
Добрый день! Явственно не понимаю одну штуку. Ситуация такая: Есть поток, который собирает данные с сенсора и выводит их на график,...

Коллекция была изменена; невозможно выполнить операцию перечисления при запуске приложения
Здравствуйте, при запуске программы иногда вылетает такая ошибка &quot;Коллекция была изменена; невозможно выполнить операцию...

"Коллекция была изменена" - там, где она не была изменена
Непойму что за глюканы творятся... Код: while (sitemapUrls.Count != sitemapUrlsChecks.Count) { var...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
17
Ответ Создать тему
Новые блоги и статьи
[Owen Logic] Поддержание уровня воды в резервуаре количеством включённых насосов: моделирование и выбор регулятора
ФедосеевПавел 14.03.2026
Поддержание уровня воды в резервуаре количеством включённых насосов: моделирование и выбор регулятора ВВЕДЕНИЕ Выполняя задание на управление насосной группой заполнения резервуара,. . .
делаю науч статью по влиянию грибов на сукцессию
anaschu 13.03.2026
прикрепляю статью
SDL3 для Desktop (MinGW): Создаём пустое окно с нуля для 2D-графики на SDL3, Си и C++
8Observer8 10.03.2026
Содержание блога Финальные проекты на Си и на C++: hello-sdl3-c. zip hello-sdl3-cpp. zip Результат:
Установка CMake и MinGW 13.1 для сборки С и C++ приложений из консоли и из Qt Creator в EXE
8Observer8 10.03.2026
Содержание блога MinGW - это коллекция инструментов для сборки приложений в EXE. CMake - это система сборки приложений. Здесь описаны базовые шаги для старта программирования с помощью CMake и. . .
Как дизайн сайта влияет на конверсию: 7 решений, которые реально повышают заявки
Neotwalker 08.03.2026
Многие до сих пор воспринимают дизайн сайта как “красивую оболочку”. На практике всё иначе: дизайн напрямую влияет на то, оставит человек заявку или уйдёт через несколько секунд. Даже если у вас. . .
Модульная разработка через nuget packages
DevAlt 07.03.2026
Сложившийся в . Net-среде способ разработки чаще всего предполагает монорепозиторий в котором находятся все исходники. При создании нового решения, мы просто добавляем нужные проекты и имеем. . .
Модульный подход на примере F#
DevAlt 06.03.2026
В блоге дяди Боба наткнулся на такое определение: В этой книге («Подход, основанный на вариантах использования») Ивар утверждает, что архитектура программного обеспечения — это структуры,. . .
Управление камерой с помощью скрипта OrbitControls.js на Three.js: Вращение, зум и панорамирование
8Observer8 05.03.2026
Содержание блога Финальная демка в браузере работает на Desktop и мобильных браузерах. Итоговый код: orbit-controls-threejs-js. zip. Сканируйте QR-код на мобильном. Вращайте камеру одним пальцем,. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru