3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
|
||||||
1 | ||||||
System.InvalidOperationException: "Коллекция была изменена после создания экземпляра перечислителя."30.03.2019, 03:25. Показов 2946. Ответов 16
Метки нет (Все метки)
Имеется две вебсокет подписки, где data1 и data2 - часто приходящие данные типа SortedDictionary<decimal, decimal>. Во второй подписке обращаясь к первому элементу словаря иногда ловлю ошибку System.InvalidOperationException. Конструкция lock почему-то не синхронизирует доступ к dic. Почему такое случается? Что делаю не так?
0
|
30.03.2019, 03:25 | |
Ответы с готовыми решениями:
16
System.InvalidOperationException: 'Коллекция была изменена после создания экземпляра перечислителя.' Коллекция была изменена после создания экземпляра перечислителя Max() ругается что коллекция была изменена Коллекция была изменена; невозможно выполнить операцию перечисления Невозможно выполнить операцию перечисления. Коллекция была изменена |
Модератор
|
|
30.03.2019, 11:09 | 2 |
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 | ||||||||||
Не понял этот момент - разве конструкция
Т.е., я думал, что когда программа "зашла" в тело блокировки (в подписке sub2), то в другом потоке (в подписке sub1) программа "не зайдет" в конструкцию
На счёт очередности обращения вроде всё понимаю. Этот момент тоже не понял.. Я только знаю что необходимо использовать один и тот же экземпляр для блокировки (в моём случае статическая переменная locker типа object), что гарантирует очередность обращения к переменной из разных потоков.
0
|
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
|
|
30.03.2019, 15:28 [ТС] | 4 |
Накидал небольшой пример, повторяющий мою проблему в целом. Что-то где-то я недопонимаю.. Подскажите, пожалуйста где мои действия неверны.
0
|
Модератор
|
|||||||||||
30.03.2019, 16:21 | 5 | ||||||||||
Нет! Блокировка действует только внутри lock
Правильно так (если позволяет алгоритм)
1
|
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
|
|
30.03.2019, 16:36 [ТС] | 6 |
Именно так я и представлял этот процесс.. Но всё равно не понимаю почему программу работает не так, как ожидается..
0
|
Модератор
|
|||||||||||||||||||||
30.03.2019, 16:59 | 7 | ||||||||||||||||||||
Я по Вашему коду не совсем понял его смысла.
Ошибки в нём не возникает. Насколько понял проблема в этом участке
Попробуйте так:
Если пойму его смысл, возможно, найду другое решение. Добавлено через 6 минут Вы переменную locker нигде не используете. А другой поток останавливается в очереди только если он хочет обратиться к этой переменной. Хотя бы вот так делайте
То есть должно быть какое-то обращение к блокированной переменной. Так как очередь блокировки возникает именно при обращении к этой переменной. Добавлено через 39 секунд Допустим попробуйте так
Но первая блокировка смысла не имеет. Присвоение ссылки другой поток никак нарушить не может.
1
|
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
|
||||||||||||||||||||||||||
30.03.2019, 22:05 [ТС] | 8 | |||||||||||||||||||||||||
Я совсем запутался теперь..
Вами предложенное решение делать так:
Я также обращаюсь из других потоков к переменной dic1 в цикле foreach таким образом:
Суть: Имеется биржа. subscribe - это websocket подписка на обновление ордеров в стакане (удаление, замена, запись и т.д.) Это я симитировал в коде
Имеется несколько потоков, которые пользуются этими данными (Нужно вытащить актуальную цену, посчитать объем и т.д.) Это как раз я и пытался сделать с помощью
0
|
Модератор
|
||||||
31.03.2019, 01:42 | 9 | |||||
Реализация зависит от того какой объём вычислений за этим стоит.
Потокобезопасно, не потококобезопасно - разницы нет, если, в принципе, объём вычислений такой, что обработка не успевает за сообщениями сервера. Сервер биржи присылает сообщения. В каждом сообщении (если оно с данными) есть штамп времени. Если сообщений много, то учтите, что вполне возможен вариант когда старые сообщения приходят позже новых. Так как каждое сообщение имеет случайное время прохождения по интернету. Я тоже сейчас пытаюсь с BitMex разобраться. Так там порой разница в задержке бывает до 50 миллисекунд. А штамп времени порой вообще у 2-5 сообщений одинаковый. Приходится делать промежуточный буфер сообщений сервера с накоплением в 50 мс. А из буфера выбирать сообщения для обработки анализируя их тип и штамп времени. После того как сообщения выстроены в том порядке как создавались, их надо обрабатывать последовательно. Ни каких параллельных потоков здесь не будет. Пример. У Вас список Ордеров. Приходит метод iserpt - вставка нового Ordera. После него сразу (иногда с тем же штампом времени) метод update - изменяющий этот Order. Вот теперь представьте, если обрабатывать это в параллельных потоках. Скорость каждого потока не предсказуема. Поэтому даже, если Вы создадите поток для update позже чем поток для iserpt, всё равно, update может выполниться раньше. А что update может изменить в коллекции ордеров? Ведь ордера, который он должен изменить, в коллекции ещё не создано! Будете ловить непредсказуемые случайные баги. Я каждую подписку (то есть фактически каждый набор данных) обрабатываю в одном потоке. Вернее в очереди потоков - потоков много, но выполняются они последовательно друг за другом. А разные подписки могут обрабатываться в разных параллельных потоках, так как наборы данных у них независимы. Это только Локальная Модель. А потом ещё всё это надо синхронизировать в ViewModel и вывести в основном потоке UI элементов. Если же биржа большая и книга ордеров большая, то изменения очень частые и большие. Это легко может превысить возможности типичного домашнего или офисного компьютера. В сообщениях от сервера может быть много ненужной информации её можно полностью или частично отсеивать каким-нибудь простым быстроработающим фильтром. Но конкретных рекомендаций быть не может. Это всё очень индивидуально и зависит от очень многих причин. Добавлено через 25 минут foreach по массиву dic1.Bids.ToArray() тоже не потокобезопасно. Метод .ToArray() - это тоже метод расширения LINQ. Есть маленькая вероятность, что во время его исполнения коллекция измениться и получите баг. Причём баг может быть "неуловимый" и "неопределяемый". То есть данные у не правильные, но приложение этого не замечает и работает с неверными данными. А Вы создаёте приложение для Биржи - для работы с реальными деньгами! Такие баги могут быть очень опасны, так как пользователь может попасть на реальные деньги. Как минимум надо массив получать в заблокированном состоянии. Что-то в таком духе
Но это надо по алгоритму обработки смотреть может ли быть изменение элемента коллекции вол время цикла и может ли это привести к неправильным результатам. В данном случае 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 (ордерБуки реалтайм обновляются сокетами). Подскажите, пожалуйста, какие ошибки я допускаю при таких подходах?
0
|
Модератор
|
|||||||
31.03.2019, 14:22 | 12 | ||||||
В этом и проблема отлавливания багов многопоточности. На это очень много что влияет: конфигурация компьютера, ОС, версии, апгрейты, приложения работающие в фоне и т.д.
Надо приложение делать сразу с учётом возможного наложения потоков, а не просто реагировать на проявление того или иного бага. Работая с перечислителем (а это и foreach, и LINQ) надо учитывать То есть надо обеспечить неизменность коллекции перечислитель которой используется. Для этого надо либо блокировать коллекцию от изменений (ReaderWriterLockSlim), либо делать копию коллекции (в том числе через LINQ метод ToArray). Добавлено через 8 минут Это проблема ни самой биржи. Она-то нормально выдаёт сообщения. Но сообщения идут через инет, через чёрт знает сколько промежуточных узлов. И у каждого сообщения возникает индивидуальная задержка. Задержка эта случайна (в определённых рамках, конечно). У меня реально разница, хоть и редко, порой доходит до нескольких десятков миллисекунд. Между соседними сообщениями (они идут по одному пути) несколько единиц миллисекунд. Добавлено через 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) После моих корректировок вот полученный код:
Ошибки те же, что в простом тесте. Если возможно изменение коллекции, то перед изменением/чтением надо её блокировать. Оператор lock проще, но при большом количестве потоков, чтобы не создавать лишних "тормозов" лучше использовать ReaderWriterLockSlim. Добавлено через 9 минут И у Вас непривычный мне стиль программирования. Какие-то бесконечные циклы... Должно быть так: 1) Часть приложения обрабатывающая сообщения от сервера. При внесении имения в данные вызывается событие извещающее об этом. 2) Часть приложения обрабатывающая имения в данных. Она подписана на событие в первой части. При возникновении события, проверяет что за изменения и вызывает, соответствующий, собственный метод. 3) Часть отвечающая за отображение данных и их подготовку к отображению. Она реагирует на изменения во второй части. Первые две относятся к локальной Mодели. Их можно сделать в разных классах или в одном. Третья часть это View и ViewModel.
1
|
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
|
|
01.04.2019, 15:56 [ТС] | 13 |
Моё понимание о конструкции Lock было сформировано из данной статьи https://metanit.com/sharp/tutorial/11.4.php (в примере блокируется не "X", а экземпляр Locker) а также из видео данного ресурса https://itvdn.com/ru/video/csh... al/threads (Техника синхронизации доступа к ресурсу - таймкод 1:37:10) Речь идёт именно о блокировке секции, используя "пустой new object()".
Да, это не имело значения, использовалось лишь в качестве имитации потока случайных данных. На счёт ">20 мc" не знал, но сталкивался как-то с таким поведением. Теперь знаю, спасибо) Это тоже использовалось в качестве примера. (Хоть я и действительно использую циклы - в каждой итерации берутся актуальные данные (стаканы) из нескольких вебсокет подписок, и над ними делаются вычисления, на следующей итерации всё это повторяется, даже если стаканы не изменились). Событийная модель на начальном этапе даже в голову не приходила, когда на бирже присутствовали только REST Api. В итоге этот подход так и тянется за мной..) Правда с освоением websocket понемногу перехожу на работу с событиями)
0
|
Модератор
|
||||||
02.04.2019, 01:03 | 14 | |||||
Сообщение было отмечено DEMON_RUS как решение
Решение
Но посмотрите как там сделано.
Перед тем как получить доступ к определённому участку кода - используется блокировка-заглушка. А где в Вас доступ к данным? Он у Вас у внутри метода subscribe, а заглушка стоит снаружи. Если так уж хотите реализовать через заглушку, то Вам надо передать ссылку на неё внутрь метода и там реализовывать блокировку по заглушке. На мой взгляд, "притянуто за уши". Я сейчас попробую такой вариант накидать, но он мне концептуально очень не нравится. В данном случае, так как метод внутренний можно использовать приватное поле уровня класса. Но это по сути "костыль": - А если надо вызвать метод из другого класса? - Придётся блокировать полностью все потоки, а не только обращения к изменяемым данным. - Невозможно будет сделать блокировку только для записи. Ведь читающие потоки смысла блокировать, в данном случае, нет. Добавлено через 2 минуты
Видите, что получается? У Вас блокировка нужна в двух методах: subscribe и в анонимном Action. Добавлено через 4 минуты Обычно не REST запросы бывает ограничения. На BitMex допустим не более 300 в 5 минут. А информации надо получать много и часто. В лимите удержаться не получается. Поэтому большую часть информации надо получать по websocket. По REST идёт только отправка ордеров.
1
|
3 / 2 / 1
Регистрация: 13.05.2013
Сообщений: 234
|
|
02.04.2019, 12:55 [ТС] | 15 |
Вроде стало понятнее всё, разобрался, спасибо)
К сожалению (или наоборот) не все биржи поддерживают websocket.
0
|
Модератор
|
|
02.04.2019, 15:27 | 16 |
Я даже не представляю как без websocket биржа может рассылать информацию.
REST очень накладный (для сервера) в этом отношении. При более мене значимом количестве клиентов REST не справится. Даже если Вы делаете всё через 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 | |
04.04.2019, 21:56 | |
Помогаю со студенческими работами здесь
17
Коллекция была изменена; невозможно выполнить операцию перечисления Коллекция была изменена; невозможно выполнить операцию перечисления при запуске приложения "Коллекция была изменена" - там, где она не была изменена Коллекция была изменена; невозможно выполнить операцию перечисления Коллекция была изменена; невозможно выполнить операцию перечисления Коллекция была изменена; невозможно выполнить операцию перечисления Искать еще темы с ответами Или воспользуйтесь поиском по форуму: |
|
Новые блоги и статьи | |||||
Книги и учебные ресурсы по C#
InfoMaster 08.01.2025
Базовые учебники и руководства
Одной из лучших книг для начинающих является "C# 10 и . NET 6 для начинающих" Эндрю Троелсена и Филиппа Джепикса . Книга последовательно раскрывает основные концепции. . .
|
Что такое NullReferenceException и как исправить?
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-х годов компьютерная индустрия столкнулась с серьезными проблемами в области управления данными. Существовавшие на тот момент модели данных -. . .
|