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

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

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

Author24 — интернет-сервис помощи студентам
Имеется две вебсокет подписки, где 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...

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

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

16
Модератор
Эксперт .NET
15859 / 11006 / 2855
Регистрация: 21.04.2018
Сообщений: 32,349
Записей в блоге: 2
30.03.2019, 11:09 2
Цитата Сообщение от 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  [ТС] 3
Цитата Сообщение от Элд Хасп Посмотреть сообщение
А у Вас в теле 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  [ТС] 4
Накидал небольшой пример, повторяющий мою проблему в целом. Что-то где-то я недопонимаю.. Подскажите, пожалуйста где мои действия неверны.
Вложения
Тип файла: rar LockTest.rar (128.8 Кб, 4 просмотров)
0
Модератор
Эксперт .NET
15859 / 11006 / 2855
Регистрация: 21.04.2018
Сообщений: 32,349
Записей в блоге: 2
30.03.2019, 16:21 5
Цитата Сообщение от 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  [ТС] 6
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Нет! Блокировка действует только внутри lock
Именно так я и представлял этот процесс.. Но всё равно не понимаю почему программу работает не так, как ожидается..
0
Модератор
Эксперт .NET
15859 / 11006 / 2855
Регистрация: 21.04.2018
Сообщений: 32,349
Записей в блоге: 2
30.03.2019, 16:59 7
Цитата Сообщение от 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  [ТС] 8
Я совсем запутался теперь..

Вами предложенное решение делать так:
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
15859 / 11006 / 2855
Регистрация: 21.04.2018
Сообщений: 32,349
Записей в блоге: 2
31.03.2019, 01:42 9
Цитата Сообщение от 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  [ТС] 10
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Выдаёт набор чисел, но не всегда. В трети случаев (примерно) - пустая консоль.
Запускал раз двадцать. Баг ни разу не выходил.
Хмм.. проект предоставил как есть, у меня через 5-10 секунд строго выкидывает исключение. Может характеристики ПК решают? Попробуйте убрать либо ещё уменьшить задержку после update.Invoke(newDic); (Там стоит Thread.Sleep(10); ) Напротив, увеличив задержку до 100 млсек исключение уже не так часто бросается.
Набор чисел - это некая имитация выдаваемых сформированных ордерБуков из вебсокета.
Раз Вы в теме, накидаю аналогичный пример для бинанса, чтоб показать, какого результата я хочу добиться.

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

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

Основная суть - в сторонних задачах (привел примеры в коде) мне необходимо всячески пользоваться ордерБуками, сформированными в методе BuildLocalDepthCache (ордерБуки реалтайм обновляются сокетами).
Подскажите, пожалуйста, какие ошибки я допускаю при таких подходах?
Вложения
Тип файла: rar BinanceDotNet-master.rar (12.50 Мб, 2 просмотров)
0
Модератор
Эксперт .NET
15859 / 11006 / 2855
Регистрация: 21.04.2018
Сообщений: 32,349
Записей в блоге: 2
31.03.2019, 14:22 12
Цитата Сообщение от 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  [ТС] 13
Цитата Сообщение от Элд Хасп Посмотреть сообщение
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
15859 / 11006 / 2855
Регистрация: 21.04.2018
Сообщений: 32,349
Записей в блоге: 2
02.04.2019, 01:03 14
Лучший ответ Сообщение было отмечено 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  [ТС] 15
Вроде стало понятнее всё, разобрался, спасибо)
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Поэтому большую часть информации надо получать по websocket.
К сожалению (или наоборот) не все биржи поддерживают websocket.
0
Модератор
Эксперт .NET
15859 / 11006 / 2855
Регистрация: 21.04.2018
Сообщений: 32,349
Записей в блоге: 2
02.04.2019, 15:27 16
Цитата Сообщение от 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 17
Элд Хасп, Тут очень грамотно реализована система. https://github.com/Marfusios/b... -websocket
1
04.04.2019, 21:56
inter-admin
Эксперт
29715 / 6470 / 2152
Регистрация: 06.03.2009
Сообщений: 28,500
Блог
04.04.2019, 21:56
Помогаю со студенческими работами здесь

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

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

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

Коллекция была изменена; невозможно выполнить операцию перечисления
foreach (VigruzkaSpecZvan s in massiv._VigruzkaSpecZvan) { ...

Коллекция была изменена; невозможно выполнить операцию перечисления
Подскажите пожалуйста, почему так происходит. Выполняю такой код: foreach (Signal signal in...

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


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

Или воспользуйтесь поиском по форуму:
17
Ответ Создать тему
Новые блоги и статьи
Книги и учебные ресурсы по C#
InfoMaster 08.01.2025
Базовые учебники и руководства Одной из лучших книг для начинающих является "C# 10 и . NET 6 для начинающих" Эндрю Троелсена и Филиппа Джепикса . Книга последовательно раскрывает основные концепции. . .
Что такое NullReferenceEx­­­ception и как исправить?
InfoMaster 08.01.2025
NullReferenceException - одно из самых распространенных исключений, с которым сталкиваются разработчики на C#. Это исключение возникает при попытке обратиться к членам объекта (методам, свойствам или. . .
Что такое Null Pointer Exception (NPE) и как это исправить?
InfoMaster 08.01.2025
Null Pointer Exception (NPE) - это одно из самых распространенных исключений в Java, которое возникает при попытке использовать ссылку на объект, значение которой равно null. Это исключение относится. . .
Русский язык в консоли C++
InfoMaster 08.01.2025
При разработке программ на C++ одной из частых проблем, с которой сталкиваются русскоязычные программисты, является корректное отображение кириллицы в консольных приложениях. Эта проблема особенно. . .
Telegram бот на C#
InfoMaster 08.01.2025
Разработка ботов для Telegram стала неотъемлемой частью современной экосистемы мессенджеров. C# предоставляет мощный и удобный инструментарий для создания разнообразных ботов, от простых. . .
Использование GraphQL в Go (Golang)
InfoMaster 08.01.2025
Go (Golang) является одним из наиболее популярных языков программирования, используемых для создания высокопроизводительных серверных приложений. Его архитектурные особенности и встроенные. . .
Что лучше использовать при создании класса в Java: сеттеры или конструктор?
Alexander-7 08.01.2025
Вопрос подробнее: На вопрос: «Когда одновременно создаются конструктор и сеттеры в классе – это нормально?» куратор уточнил: «Ваш класс может вообще не иметь сеттеров, а только конструктор и геттеры. . .
Как работать с GraphQL на TypeScript
InfoMaster 08.01.2025
Введение в GraphQL и TypeScript В современной разработке веб-приложений GraphQL стал мощным инструментом для создания гибких и эффективных API. В сочетании с TypeScript, эта технология. . .
Счётчик на базе сумматоров + регистров и генератора сигналов согласования.
Hrethgir 07.01.2025
Создан с целью проверки скорости асинхронной логики: ранее описанного сумматора и предополагаемых fast регистров. Регистры созданы на базе ранее описанного, предполагаемого fast триггера. То-есть. . .
Как перейти с Options API на Composition API в Vue.js
BasicMan 06.01.2025
Почему переход на Composition API актуален В мире современной веб-разработки фреймворк Vue. js продолжает эволюционировать, предлагая разработчикам все более совершенные инструменты для создания. . .
Архитектура современных процессоров
inter-admin 06.01.2025
Процессор (центральный процессор, ЦП) является основным вычислительным устройством компьютера, которое выполняет обработку данных и управляет работой всех остальных компонентов системы. Архитектура. . .
История создания реляционной модели баз данных, правила Кодда
Programming 06.01.2025
Предпосылки создания реляционной модели В конце 1960-х годов компьютерная индустрия столкнулась с серьезными проблемами в области управления данными. Существовавшие на тот момент модели данных -. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru