Движок C ++, над которым я сейчас работаю, разделен на несколько больших потоков: Generation (для создания моего процедурного контента), Gameplay (для AI, скриптов, симуляции), Physics и Rendering.
Потоки взаимодействуют друг с другом через небольшие объекты сообщений, которые передаются из потока в поток. Перед переходом поток обрабатывает все свои входящие сообщения - обновления для преобразования, добавления и удаления объектов и т. Д. Иногда один поток (Генерация) создает что-то (Искусство) и передает его другому потоку (Визуализация) для постоянного владения.
В начале процесса я заметил пару вещей:
Система обмена сообщениями громоздка. Создание нового типа сообщения означает создание подкласса базового класса Message, создание нового перечисления для его типа и написание логики для того, как потоки должны интерпретировать новый тип сообщения. Это скорость развития и подвержена ошибкам в стиле опечаток. (Sidenote - работая над этим, я оцениваю, какими замечательными могут быть динамические языки!)
Есть лучший способ это сделать? Должен ли я использовать что-то вроде boost :: bind, чтобы сделать это автоматически? Я беспокоюсь, что если я сделаю это, я потеряю способность говорить, сортировать сообщения по типу или что-то в этом роде. Не уверен, что такой менеджмент станет необходимым.
Первый момент важен, потому что эти темы так много общаются. Создание и передача сообщений - большая часть того, как все происходит. Я хотел бы упростить эту систему, но также быть открытым для других парадигм, которые могут быть столь же полезными. Существуют ли разные многопоточные конструкции, о которых мне следует подумать, чтобы облегчить эту задачу?
Например, есть некоторые ресурсы, которые редко пишутся, но часто читаются из нескольких потоков. Должен ли я быть открытым к идее иметь общие данные, защищенные мьютексами, к которым могут обращаться все потоки?
Это мой первый раз, когда я проектирую что-то с учетом многопоточности с нуля. На этом раннем этапе я на самом деле думаю, что все идет хорошо (учитывая), но я беспокоюсь о масштабировании и моей собственной эффективности при внедрении новых вещей.
источник
Ответы:
К вашей более широкой проблеме, попробуйте найти способы максимально сократить межпотоковое взаимодействие. Лучше вообще избегать проблем с синхронизацией, если можете. Это может быть достигнуто двойной буферизацией ваших данных, вводя задержку одного обновления, но значительно облегчая задачу работы с общими данными.
Кроме того, рассматривали ли вы не многопоточность по подсистеме, а вместо этого использование порождения потоков или пулов потоков для разветвления задачи? (см. это в отношении вашей конкретной проблемы, для пула потоков.) Этот краткий документ кратко излагает назначение и использование шаблона пула. Смотрите эти информативные ответытакже. Как уже отмечалось, пулы потоков улучшают масштабируемость в качестве бонуса. И это «пишите один раз, используйте где угодно», а не заставляйте потоки на подсистеме играть хорошо при каждом написании новой игры или движка. Существует также множество надежных сторонних решений для пула потоков. Было бы проще начать с порождения потоков и позже перейти к пулам потоков, если нужно уменьшить накладные расходы на порождающие и уничтожающие потоки.
источник
Вы спрашивали о разных многопоточных конструкциях. Мой друг рассказал мне об этом методе, который мне показался довольно крутым.
Идея состоит в том, что будет 2 копии каждой игровой сущности (я знаю, что это расточительно). Одна копия будет настоящей копией, а другая - прошлой. Настоящий экземпляр только для записи , а последняя - только для чтения . Когда вы переходите к обновлению, вы назначаете диапазоны вашего списка сущностей столько потоков, сколько считаете нужным. Каждый поток имеет доступ на запись к текущим копиям в назначенном диапазоне, и каждый поток имеет доступ на чтение ко всем прошлым копиям сущностей и, таким образом, может обновлять назначенные текущие копии, используя данные из прошлых копий без блокировки. Между каждым кадром текущая копия становится прошлой, однако вы хотите справиться с обменом ролями.
источник
У нас была такая же проблема, только с C #. После долгих раздумий о легкости (или отсутствии таковой) создания новых сообщений лучшее, что мы могли сделать, - это создать для них генератор кода. Это немного уродливо, но удобно: учитывая только описание содержимого сообщения, он генерирует класс сообщения, перечисления, код обработки заполнителей и т. Д. - весь этот код почти одинаков каждый раз и действительно подвержен опечаткам.
Я не совсем доволен этим, но это лучше, чем писать весь этот код вручную.
Что касается общих данных, лучший ответ, конечно, «это зависит». Но, как правило, если некоторые данные читаются часто и нужны многим потокам, обмен ими того стоит. Для обеспечения безопасности потоков лучше всего сделать его неизменным , но если об этом не может быть и речи, мьютекс может подойти. В C # есть
ReaderWriterLockSlim
класс, специально разработанный для таких случаев; Я уверен, что есть эквивалент C ++.Еще одна идея для связи потоков, которая, вероятно, решит вашу первую проблему, состоит в том, чтобы передавать обработчики вместо сообщений. Я не уверен, как это сделать в C ++, но в C # вы можете отправить
delegate
объект в другой поток (например, добавить его в какую-то очередь сообщений) и фактически вызвать этот делегат из принимающего потока. Это позволяет создавать «специальные» сообщения на месте. Я только играл с этой идеей, никогда не пробовал ее в производстве, так что на самом деле она может оказаться плохой.источник
Я нахожусь только на этапе разработки какого-то многопоточного игрового кода, поэтому я могу только поделиться своими мыслями, а не каким-либо реальным опытом. С учетом сказанного, я думаю по следующим направлениям:
Я думаю (хотя я не уверен) теоретически это должно означать, что на этапах чтения и обновления любое количество потоков может работать одновременно с минимальной синхронизацией. На этапе чтения никто не записывает общие данные, поэтому проблем с параллелизмом возникнуть не должно. Фаза обновления, ну, это сложнее. Параллельные обновления на том же фрагменте данных были бы проблемой, поэтому здесь необходима некоторая синхронизация. Тем не менее, я все еще могу запустить произвольное количество потоков обновлений, если они работают с разными наборами данных.
В целом, я думаю, что этот подход хорошо подходит для системы пула потоков. Проблемные части:
x += 2; if (x > 5) ...
если x является общим. Вам нужно либо сделать локальную копию x, либо создать запрос на обновление, и выполнить условие только при следующем запуске. Последнее означало бы множество дополнительных шаблонных кодов, сохраняющих локальное состояние потока.источник