Решение проблемы параллелизма вокруг коллекций - PullRequest
2 голосов
/ 05 марта 2010

У меня проблемы с одновременным использованием общей коллекции в многопользовательской синхронной игре, над которой я работаю. Я немного покопался и нашел аккуратную поточно-ориентированную реализацию IEnumerator / IList в посте Алексея Дробышевского о проекте кода здесь:

http://www.codeproject.com/KB/cs/safe_enumerable.aspx

После принятия его реализации я даже заменил все запросы Linq в общей коллекции на циклы for / foreach, потому что запросы Linq все еще использовали небезопасный IEnumerable под ним.

Вот моя реализация SafeList, и сам список представляется как ReadOnlyCollection для потребляющих классов.

http://theburningmonk.com/2010/03/thread-safe-enumeration-in-csharp/

После перехода к этому списку SafeList я вижу гораздо меньше проблем, но при большой нагрузке (80+ потоков, все из которых читают / пишут из и в список в разных точках), я по-прежнему вижу исключение InvalidOperationException:

Список элементов изменился. Не удалось продолжить операцию перечисления

Я даже пытался использовать ReadWriterLockSlim вместо объекта блокировки в моей реализации SafeList, но это также оказалось бесплодным. Единственное другое предложение, которое у меня было до сих пор, - это клонирование списка каждый раз, когда потоки должны пройти по нему. Я надеюсь избежать клонирования списка каждый раз, так как список используется слишком часто, это может привести к снижению производительности и появлению других ошибок, которые трудно обнаружить.

Учитывая временные ограничения, я должен быть прагматичным в этом вопросе, и если клонирование является самым безопасным и быстрым способом решения этой проблемы, то я в порядке, но перед тем, как прибегнуть к последней попытке рва, я ' Мне просто интересно, если кто-нибудь сталкивался с чем-то подобным, может дать какой-то совет.

Заранее большое спасибо!

[РЕДАКТИРОВАТЬ] Вот немного больше информации о проблеме, которую я вижу по запросу:

Для одной «игры» может быть подключено до 100 или более синхронных клиентов, и игра должна сообщать каждому подключенному клиенту обновления каждые несколько секунд, и поэтому каждые несколько секунд этой игре необходимо перебирать общий список игроков. , Когда игрок присоединяется или уходит, список необходимо обновить соответствующим образом, чтобы отразить изменения. Чтобы добавить к этому, игроки могут взаимодействовать с игрой и общаться в чате с другими игроками, и каждый раз, когда от игрока поступает сообщение, игре снова приходится повторять один и тот же список и выполнять трансляцию. Исключения обычно генерируются, когда игра пытается передавать сообщения игрокам (операция чтения) в то же время, когда многие игроки покидают / присоединяются (операция записи) одновременно.

Ответы [ 2 ]

6 голосов
/ 05 марта 2010

Учитывая ваше описание игровой структуры, учитывая наличие единственного потока, который является единственным потоком, который может напрямую получить доступ к списку игроков. Сделайте список по-настоящему приватным для этого потока.

Способ, которым любые другие потоки получают доступ к списку, - отправка сообщений в поток менеджера списка. Так что у этого потока есть очередь сообщений, которую он ожидает. Пока очередь не пустая, она просматривает сообщения, следуя их инструкциям. Они могут сказать «Добавить нового игрока», или «Удалить игрока», или «Обновить статус этого игрока до« несчастный »».

Этот поток может также периодически сканировать список для отправки обновлений клиентам, или он может (даже лучше) использовать тот факт, что он имеет известный поток изменений, которые происходят в списке, и пересылать только эти изменения в клиенты.

Основной принцип: сделать данные частными для одного потока и обеспечить взаимодействие потоков через очереди сообщений.

Ваша основная структура данных - это класс очереди, ориентированный на многопотоковое исполнение. Уже есть десятки примеров на SO. (И держитесь подальше от любых, которые утверждают, что они «свободны от блокировки» и все же безопасны для потоков. Это просто не стоит риска.)

1 голос
/ 05 марта 2010

Несмотря на то, что другие предложения по рефакторингу - это путь, одна ошибка в классе 'thread-safe' выдается (может быть больше):

IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
    // instead of returning an usafe enumerator,
    // we wrap it into our thread-safe class
    return new SafeEnumerator<T>(_inner.GetEnumerator(), _lock);
}

Вы создаете перечислитель из _inner.GetEnumerator ДО запуска конструктора, и, таким образом, любой поток может модифицировать коллекцию до тех пор, пока вы не заблокируете конструктор. Это небольшой временной интервал, но с 80 потоками это произойдет. Вам нужно заблокировать оператор return .., чтобы защитить перечислитель.

РЕДАКТИРОВАТЬ: А также в других местах, где вы используете тот же шаблон.

...