В моей игре есть участки земли со зданиями (дома, ресурсные центры). В таких зданиях, как дома, есть арендаторы, комнаты, дополнения и т. Д., И есть несколько значений, которые необходимо смоделировать на основе всех этих переменных.
Теперь я хотел бы использовать AndEngine для внешнего интерфейса и создать еще один поток для расчетов симуляции (возможно, позже включу ИИ в этот поток). Это так, что один целый поток не выполняет всю работу и вызывает проблемы, такие как блокировка. Это вводит проблему параллелизма и зависимости .
Проблема с валютой - это мой основной поток пользовательского интерфейса, и поток вычислений должен был бы получить доступ ко всем объектам моделирования. Поэтому я должен сделать их поточно-ориентированными, но я не знаю, как хранить и структурировать объекты симуляции, чтобы сделать это возможным.
Проблема зависимости заключается в том, что для вычисления значений мои вычисления зависят от значений других объектов.
Как лучше всего связать мой объект-арендатор в здании с моими расчетами? Жестко закодировать это в класс арендатора? Что такое хороший способ сделать «хранилище» алгоритмов, чтобы их можно было легко настроить?
Простым ленивым способом было бы объединить все в класс, который содержит все объекты, такие как участки земли (которые в свою очередь владеют зданиями и так далее). Этот класс также будет содержать состояние игры, такое как технология, доступная пользователю, пулы объектов для таких вещей, как спрайты. Но это ленивый и опасный способ, верно?
Редактировать: я смотрел на Dependency Injection, но насколько хорошо он справляется с классом, который содержит другие объекты? т. е. мой участок земли, со зданием, в котором есть арендатор и множество других ценностей. DI выглядит как боль в заднице с AndEngine.
источник
Ответы:
Ваша проблема по своей сути последовательная - вы должны выполнить обновление симуляции, прежде чем сможете ее отрендерить. Выгрузка симуляции в другой поток просто означает, что основной поток пользовательского интерфейса ничего не делает, пока поток симуляции тикает (что означает, что он заблокирован).
Общепринятая «лучшая практика» для параллелизма - не помещать рендеринг в один поток, а симуляцию в другой, как вы предлагаете. Я настоятельно рекомендую против такого подхода, на самом деле. Эти две операции естественным образом связаны между собой, и хотя они могут быть грубо форсированы, они не оптимальны и не масштабируются .
Лучший подход - сделать части обновления или рендеринга параллельными, но оставить обновления и рендеринг всегда последовательными. Так, например, если у вас есть естественная граница в вашей симуляции (например, если дома никогда не влияют друг на друга в вашей симуляции), вы можете засунуть все дома в ведра из N домов и раскрутить кучу потоков, каждый из которых обрабатывает один и пусть эти потоки присоединятся до завершения шага обновления. Это гораздо лучше масштабируется и намного лучше подходит для параллельного дизайна.
Вы переосмысливаете остальную часть вопроса:
Внедрение зависимостей здесь представляет собой «красную сельдь»: все встраивание зависимостей действительно означает, что вы передаете («внедряете») зависимости интерфейса в экземпляры этого интерфейса, обычно во время создания.
Это означает, что если у вас есть класс, который моделирует a
House
, который должен знать,City
что он находится, тогдаHouse
конструктор может выглядеть так:Ничего особенного.
Использование синглтона не нужно (вы часто видите, как это делается в некоторых из безумно сложных, чрезмерно спроектированных «DI-фреймворках», таких как Caliburn, которые предназначены для «корпоративных» приложений с графическим интерфейсом - это не делает его хорошим решением). На самом деле, введение синглетонов часто является антитезой хорошего управления зависимостями. Они также могут вызывать серьезные проблемы с многопоточным кодом, потому что обычно они не могут быть поточно-ориентированными без блокировок - чем больше блокировок вы должны получить, тем хуже ваша проблема подходит для параллельной обработки.
источник
Обычным решением проблем параллелизма является изоляция данных .
Изоляция означает, что каждый поток имеет свои собственные данные и не касается данных других потоков. Таким образом, нет проблем с параллелизмом ... но тогда у нас есть проблема общения. Как эти потоки могут работать вместе, если у них нет общих данных?
Здесь есть два подхода.
Первый из них - неизменность . Неизменяемые структуры / переменные - это те, которые никогда не меняют своего состояния. Сначала это может звучать бесполезно - как можно использовать «переменную», которая никогда не меняется? Тем не менее, мы можем поменять местами эти переменные! Рассмотрим этот пример: предположим, что у вас есть
Tenant
класс с кучей полей, которые должны находиться в согласованном состоянии. Если вы изменяетеTenant
объект в потоке A и одновременно наблюдаете его из потока B, поток B может увидеть объект в несогласованном состоянии. Однако, еслиTenant
он неизменен, поток А не может его изменить. Вместо этого он создает новыйTenant
объект с полями, установленными как требуется, и заменяет его старым. Обмен - это просто изменение одной ссылки, которая, вероятно, является атомарной, и, следовательно, нет возможности наблюдать объект в несогласованном состоянии.Второй подход - обмен сообщениями . Идея заключается в том, что когда все данные «принадлежат» какому-либо потоку, мы можем сообщить этому потоку, что делать с данными. Каждый поток в этой архитектуре имеет очередь сообщений - список
Message
объектов и насос обмена сообщениями - постоянно работающий метод, который удаляет сообщение из очереди, интерпретирует его и вызывает некоторый метод-обработчик. Например, предположим, что вы постучали по участку земли, сигнализируя о том, что его нужно купить. Поток пользовательского интерфейса не может изменитьPlot
объект напрямую, потому что он принадлежит логическому потоку (и, вероятно, является неизменным). Таким образом, поток пользовательского интерфейсаBuyMessage
вместо этого создает объект и добавляет его в очередь логического потока. Поток логики при запуске принимает сообщение из очереди и вызываетBuyPlot()
, извлекая параметры из объекта сообщения. НапримерBuySuccessfulMessage
, он может отправить сообщение назад, инструктируя поток пользовательского интерфейса: «Теперь у вас есть больше земли!» окно на экране. Конечно, доступ к очереди сообщений должен быть синхронизирован с блокировкой, критической секцией или как бы она ни называлась в AndEngine. Но это единственная точка синхронизации между потоками, и потоки приостанавливаются на очень короткое время, поэтому это не проблема.Эти два подхода лучше всего использовать в сочетании. Ваши потоки должны взаимодействовать с сообщениями и иметь некоторые неизменяемые данные, «открытые» для других потоков - например, неизменный список графиков для пользовательского интерфейса для их рисования.
Также обратите внимание, что «только чтение» не обязательно означает неизменность ! Любая сложная структура данных, такая как хеш-таблица, может изменить свое внутреннее состояние при доступе к чтению, поэтому сначала сверьтесь с документацией.
источник
Вероятно, 99% компьютерных программ, написанных в истории, использовали только 1 поток и работали нормально. У меня нет опыта работы с AndEngine, но очень редко можно найти системы, требующие многопоточности, только несколько, которые могли бы извлечь из этого пользу, при наличии подходящего оборудования.
Традиционно, для симуляции и графического интерфейса / рендеринга в одном потоке, вы просто делаете немного симуляции, затем выполняете рендеринг и повторяете, как правило, много раз в секунду.
Когда у кого-то мало опыта использования нескольких процессов или он не совсем понимает, что означает «безопасность» потока (это неопределенный термин, который может означать множество разных вещей), слишком легко вносить множество ошибок в систему. Поэтому лично я бы порекомендовал использовать однопоточный подход, чередование симуляции и рендеринга, а также сохранение любых потоков для операций, которые, как вы точно знаете, будут занимать много времени и абсолютно требуют потоков, а не модели, основанной на событиях.
источник