Как я должен структурировать свои классы, чтобы учесть многопоточное моделирование?

8

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

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

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

Проблема зависимости заключается в том, что для вычисления значений мои вычисления зависят от значений других объектов.

Как лучше всего связать мой объект-арендатор в здании с моими расчетами? Жестко закодировать это в класс арендатора? Что такое хороший способ сделать «хранилище» алгоритмов, чтобы их можно было легко настроить?

Простым ленивым способом было бы объединить все в класс, который содержит все объекты, такие как участки земли (которые в свою очередь владеют зданиями и так далее). Этот класс также будет содержать состояние игры, такое как технология, доступная пользователю, пулы объектов для таких вещей, как спрайты. Но это ленивый и опасный способ, верно?

Редактировать: я смотрел на Dependency Injection, но насколько хорошо он справляется с классом, который содержит другие объекты? т. е. мой участок земли, со зданием, в котором есть арендатор и множество других ценностей. DI выглядит как боль в заднице с AndEngine.

NiffyShibby
источник
Просто быстрое замечание: не нужно беспокоиться о параллельном доступе к данным, если один из обращений доступен только для чтения. Пока вы выполняете рендеринг только для чтения необработанных данных, чтобы использовать их для рендеринга, а не для обновления данных во время их обработки, проблем не возникает. Один поток обновляет данные, другой поток просто читает и отображает их.
Джеймс
Ну, параллельный доступ все еще является проблемой, так как пользователь может купить участок земли, построить здание на этой земле и разместить арендатора в доме, поэтому основной поток создает данные и может изменять данные. Параллельный доступ - не столько проблема, сколько его пример совместного использования между основным потоком и дочерним потоком.
NiffyShibby
Я говорю о зависимости как о проблеме, кажется, что такие люди, как Google, думают, что скрывать зависимость - не мудрая вещь. Мои расчеты арендатора зависят от строительного участка, здания, создания спрайта на экране (у меня могут быть реляционные отношения между арендатором здания и созданием спрайта арендатора в другом месте)
NiffyShibby
Я предполагаю, что мое предложение должно быть истолковано как создание вещей, которые являются резьбовыми, вещами, которые либо самодостаточны, либо требуют доступа только для чтения к данным, управляемым другим потоком. Рендеринг был бы примером чего-то, что вы могли бы использовать как нужен только доступ на чтение к данным, чтобы он мог их отображать.
Джеймс
1
Джеймс, даже доступ только для чтения может быть плохой идеей, если другой поток находится в процессе внесения изменений в этот объект. При сложной структуре данных это может вызвать сбой, а при использовании простых типов данных это может привести к несогласованному чтению.
Kylotan

Ответы:

4

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

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

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

Вы переосмысливаете остальную часть вопроса:

Внедрение зависимостей здесь представляет собой «красную сельдь»: все встраивание зависимостей действительно означает, что вы передаете («внедряете») зависимости интерфейса в экземпляры этого интерфейса, обычно во время создания.

Это означает, что если у вас есть класс, который моделирует a House, который должен знать, Cityчто он находится, тогда Houseконструктор может выглядеть так:

public House( City containingCity ) {
  m_city = containingCity; // Store in a member variable for later access
  ...
}

Ничего особенного.

Использование синглтона не нужно (вы часто видите, как это делается в некоторых из безумно сложных, чрезмерно спроектированных «DI-фреймворках», таких как Caliburn, которые предназначены для «корпоративных» приложений с графическим интерфейсом - это не делает его хорошим решением). На самом деле, введение синглетонов часто является антитезой хорошего управления зависимостями. Они также могут вызывать серьезные проблемы с многопоточным кодом, потому что обычно они не могут быть поточно-ориентированными без блокировок - чем больше блокировок вы должны получить, тем хуже ваша проблема подходит для параллельной обработки.


источник
Я помню, как говорил, что синглтоны были плохими в моем первоначальном посте ...
NiffyShibby
Я помню, что в моем первоначальном посте синглоны были плохими, но это было удалено. Я думаю, что я понимаю, что вы говорите. Например, мой маленький человек ходит по экрану, поскольку он делает так, что вызывается поток обновления, он должен обновить его, но не может, потому что основной поток использует объект, таким образом, мой другой поток заблокирован. Где, как я должен обновлять между рендерингом.
NiffyShibby
Кто-то прислал мне полезную ссылку. gamedev.stackexchange.com/questions/95/…
NiffyShibby
5

Обычным решением проблем параллелизма является изоляция данных .

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

Здесь есть два подхода.

Первый из них - неизменность . Неизменяемые структуры / переменные - это те, которые никогда не меняют своего состояния. Сначала это может звучать бесполезно - как можно использовать «переменную», которая никогда не меняется? Тем не менее, мы можем поменять местами эти переменные! Рассмотрим этот пример: предположим, что у вас есть Tenantкласс с кучей полей, которые должны находиться в согласованном состоянии. Если вы изменяете Tenantобъект в потоке A и одновременно наблюдаете его из потока B, поток B может увидеть объект в несогласованном состоянии. Однако, если Tenantон неизменен, поток А не может его изменить. Вместо этого он создает новый Tenantобъект с полями, установленными как требуется, и заменяет его старым. Обмен - это просто изменение одной ссылки, которая, вероятно, является атомарной, и, следовательно, нет возможности наблюдать объект в несогласованном состоянии.

Второй подход - обмен сообщениями . Идея заключается в том, что когда все данные «принадлежат» какому-либо потоку, мы можем сообщить этому потоку, что делать с данными. Каждый поток в этой архитектуре имеет очередь сообщений - список Messageобъектов и насос обмена сообщениями - постоянно работающий метод, который удаляет сообщение из очереди, интерпретирует его и вызывает некоторый метод-обработчик. Например, предположим, что вы постучали по участку земли, сигнализируя о том, что его нужно купить. Поток пользовательского интерфейса не может изменить Plotобъект напрямую, потому что он принадлежит логическому потоку (и, вероятно, является неизменным). Таким образом, поток пользовательского интерфейса BuyMessageвместо этого создает объект и добавляет его в очередь логического потока. Поток логики при запуске принимает сообщение из очереди и вызываетBuyPlot(), извлекая параметры из объекта сообщения. Например BuySuccessfulMessage, он может отправить сообщение назад, инструктируя поток пользовательского интерфейса: «Теперь у вас есть больше земли!» окно на экране. Конечно, доступ к очереди сообщений должен быть синхронизирован с блокировкой, критической секцией или как бы она ни называлась в AndEngine. Но это единственная точка синхронизации между потоками, и потоки приостанавливаются на очень короткое время, поэтому это не проблема.

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

Также обратите внимание, что «только чтение» не обязательно означает неизменность ! Любая сложная структура данных, такая как хеш-таблица, может изменить свое внутреннее состояние при доступе к чтению, поэтому сначала сверьтесь с документацией.

Ничего
источник
Это звучит аккуратно, мне придется провести с ним некоторое тестирование, это звучит довольно дорого, я думал в духе DI с областью синглтона, а затем использовал блокировки для одновременного доступа. Но я никогда не думал сделать это таким образом, это могло бы сработать: D
NiffyShibby
Ну, вот как мы делаем параллелизм на многопоточном многопоточном сервере. Вероятно, немного излишним для простой игры, но я бы сам использовал этот подход.
Nevermind
4

Вероятно, 99% компьютерных программ, написанных в истории, использовали только 1 поток и работали нормально. У меня нет опыта работы с AndEngine, но очень редко можно найти системы, требующие многопоточности, только несколько, которые могли бы извлечь из этого пользу, при наличии подходящего оборудования.

Традиционно, для симуляции и графического интерфейса / рендеринга в одном потоке, вы просто делаете немного симуляции, затем выполняете рендеринг и повторяете, как правило, много раз в секунду.

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

Kylotan
источник
Andengine выполняет рендеринг для меня, но я все еще чувствую, что вычисления должны идти в другом потоке, поскольку основной поток пользовательского интерфейса будет замедляться, если не будет заблокирован, если все будет выполнено в одном потоке.
NiffyShibby
Почему ты это чувствуешь? У вас есть расчеты, которые стоят дороже, чем типичная 3D-игра? А знаете ли вы, что большинство Android-устройств имеют только одно ядро ​​и поэтому не получают никакой выгоды от дополнительной производительности?
Kylotan
Нет, но приятно отделить логику и четко определить, что делается, если вы сохраняете это в том же потоке, вам придется ссылаться на основной класс, где это происходит, или делать некоторые DI с одноэлементной областью действия. Что не так много проблем. Что касается ядра, мы видим, что выходит больше двухъядерных Android-устройств, моя идея игры может работать не совсем хорошо на одноядерном устройстве, в то время как на двухъядерном она может работать довольно хорошо.
NiffyShibby
Поэтому проектирование всего в одном потоке не кажется мне хорошей идеей, по крайней мере, с помощью потоков я могу отделить логику, и в будущем мне не придется беспокоиться о попытках улучшить производительность, как я задумал с самого начала.
NiffyShibby
Но ваша проблема по-прежнему последовательна, поэтому вы, скорее всего, заблокируете оба потока, ожидая их присоединения в любом случае, если только вы не изолировали данные (давая потоку рендеринга что-то, что нужно делать, пока тикает логический поток), и рендеринг кадра или так позади симуляции. Подход, который вы описываете, не является общепринятой передовой практикой для проектирования параллелизма.