Как сделать передачу сообщений между потоками в многопоточном движке менее громоздкой?

18

Движок C ++, над которым я сейчас работаю, разделен на несколько больших потоков: Generation (для создания моего процедурного контента), Gameplay (для AI, скриптов, симуляции), Physics и Rendering.

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

В начале процесса я заметил пару вещей:

  1. Система обмена сообщениями громоздка. Создание нового типа сообщения означает создание подкласса базового класса Message, создание нового перечисления для его типа и написание логики для того, как потоки должны интерпретировать новый тип сообщения. Это скорость развития и подвержена ошибкам в стиле опечаток. (Sidenote - работая над этим, я оцениваю, какими замечательными могут быть динамические языки!)

    Есть лучший способ это сделать? Должен ли я использовать что-то вроде boost :: bind, чтобы сделать это автоматически? Я беспокоюсь, что если я сделаю это, я потеряю способность говорить, сортировать сообщения по типу или что-то в этом роде. Не уверен, что такой менеджмент станет необходимым.

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

    Например, есть некоторые ресурсы, которые редко пишутся, но часто читаются из нескольких потоков. Должен ли я быть открытым к идее иметь общие данные, защищенные мьютексами, к которым могут обращаться все потоки?

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

Raptormeat
источник
6
Здесь действительно нет ни одного целенаправленного вопроса, и поэтому этот пост не очень подходит для стиля вопросов и ответов этого сайта. Я бы порекомендовал вам разбить ваш пост на отдельные посты, по одному на вопрос, и перефокусировать вопросы, чтобы они задавали конкретную проблему, с которой вы сталкиваетесь, вместо смутного набора советов или советов.
2
Если вы хотите вступить в более общий разговор, я бы порекомендовал вам попробовать этот пост на форумах gamedev.net . Как сказал Джош, поскольку ваш «вопрос» не является каким-то конкретным вопросом, его было бы довольно сложно разместить в формате StackExchange.
Cypher
Спасибо за отзывы ребята! Я надеялся, что у кого-то с большим знанием может быть один ресурс / опыт / парадигма, которая могла бы решить несколько моих проблем одновременно. У меня возникает ощущение, что одна большая идея может объединить мои различные проблемы в одну вещь, которую я упускаю, и я думал, что кто-то с большим опытом, чем я, мог бы это признать ... Но, возможно, нет, и в любом случае - взятые точки !
Raptormeat
Я переименовал ваш заголовок, чтобы он был более конкретным для передачи сообщений, поскольку вопросы типа «подсказок» подразумевают, что не существует конкретной проблемы, которую нужно решить (и поэтому в эти дни я бы назвал это «не реальным вопросом»).
Тетрад
Вы уверены, что вам нужны отдельные темы для физики и игрового процесса? Эти двое кажутся очень переплетенными. Также трудно понять, как предлагать советы, не зная, как и с кем общаться.
Никол Болас

Ответы:

10

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

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

инженер
источник
1
Любые рекомендации для конкретных библиотек пулов потоков, чтобы проверить?
Имре
Ник - большое спасибо за ответ. Что касается вашего первого пункта - я думаю, что это отличная идея и, вероятно, направление, в котором я буду двигаться. В настоящее время достаточно рано, что я еще не знаю, что должно быть с двойной буферизацией. Я буду помнить это, поскольку это укрепляется в течение долгого времени. На ваш второй пункт - спасибо за предложение! Да, преимущества потоковых задач очевидны. Я прочитаю ваши ссылки и подумаю над этим. Не уверен на 100%, сработает ли это для меня / как заставить это работать на меня, но я обязательно подумаю над этим серьезно. Благодарность!
Raptormeat
1
@imre посмотрите библиотеку Boost - у них есть будущее, которое является хорошим / простым способом приблизиться к этим вещам.
Джонатан Дикинсон
5

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

Идея состоит в том, что будет 2 копии каждой игровой сущности (я знаю, что это расточительно). Одна копия будет настоящей копией, а другая - прошлой. Настоящий экземпляр только для записи , а последняя - только для чтения . Когда вы переходите к обновлению, вы назначаете диапазоны вашего списка сущностей столько потоков, сколько считаете нужным. Каждый поток имеет доступ на запись к текущим копиям в назначенном диапазоне, и каждый поток имеет доступ на чтение ко всем прошлым копиям сущностей и, таким образом, может обновлять назначенные текущие копии, используя данные из прошлых копий без блокировки. Между каждым кадром текущая копия становится прошлой, однако вы хотите справиться с обменом ролями.

Джон Макдональд
источник
4

У нас была такая же проблема, только с C #. После долгих раздумий о легкости (или отсутствии таковой) создания новых сообщений лучшее, что мы могли сделать, - это создать для них генератор кода. Это немного уродливо, но удобно: учитывая только описание содержимого сообщения, он генерирует класс сообщения, перечисления, код обработки заполнителей и т. Д. - весь этот код почти одинаков каждый раз и действительно подвержен опечаткам.

Я не совсем доволен этим, но это лучше, чем писать весь этот код вручную.

Что касается общих данных, лучший ответ, конечно, «это зависит». Но, как правило, если некоторые данные читаются часто и нужны многим потокам, обмен ими того стоит. Для обеспечения безопасности потоков лучше всего сделать его неизменным , но если об этом не может быть и речи, мьютекс может подойти. В C # естьReaderWriterLockSlim класс, специально разработанный для таких случаев; Я уверен, что есть эквивалент C ++.

Еще одна идея для связи потоков, которая, вероятно, решит вашу первую проблему, состоит в том, чтобы передавать обработчики вместо сообщений. Я не уверен, как это сделать в C ++, но в C # вы можете отправить delegateобъект в другой поток (например, добавить его в какую-то очередь сообщений) и фактически вызвать этот делегат из принимающего потока. Это позволяет создавать «специальные» сообщения на месте. Я только играл с этой идеей, никогда не пробовал ее в производстве, так что на самом деле она может оказаться плохой.

Ничего
источник
Спасибо за всю замечательную информацию! Последний бит о обработчиках похож на то, что я упоминал об использовании привязки или функторов для передачи функций. Мне нравится идея - я мог бы попробовать ее и посмотреть, является ли она отстойной или потрясающей: D Могу начать с создания класса CallDelegateMessage и погружения моего пальца в воду.
Raptormeat
1

Я нахожусь только на этапе разработки какого-то многопоточного игрового кода, поэтому я могу только поделиться своими мыслями, а не каким-либо реальным опытом. С учетом сказанного, я думаю по следующим направлениям:

  • Большая часть игровых данных должна быть доступна только для чтения .
  • Запись данных возможна с помощью своего рода сообщений.
  • Чтобы избежать обновления данных, пока другой поток их читает, игровой цикл имеет две отдельные фазы: чтение и обновление.
  • На этапе чтения:
  • Все общие данные доступны только для чтения для всех потоков.
  • Потоки могут вычислять вещи (используя локальное хранилище потоков) и генерировать запросы на обновление , которые в основном представляют собой объекты команды / сообщения, помещенные в очередь, для последующего применения.
  • На этапе обновления:
  • Все общие данные доступны только для записи. Данные следует предполагать в неизвестном / нестабильном состоянии.
  • Здесь обрабатываются объекты запроса на обновление.

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

В целом, я думаю, что этот подход хорошо подходит для системы пула потоков. Проблемные части:

  • Синхронизация потоков обновления (убедитесь, что несколько потоков не пытаются обновить один и тот же набор данных).
  • Убедитесь, что на этапе чтения ни один поток не может случайно записать общие данные. Я боюсь, что будет слишком много места для ошибок программирования, и я не уверен, сколько из них может быть легко поймано инструментами отладки.
  • Написание кода таким образом, чтобы вы не могли рассчитывать на то, что ваши промежуточные результаты будут доступны для чтения сразу. То есть, вы не можете писать, x += 2; if (x > 5) ...если x является общим. Вам нужно либо сделать локальную копию x, либо создать запрос на обновление, и выполнить условие только при следующем запуске. Последнее означало бы множество дополнительных шаблонных кодов, сохраняющих локальное состояние потока.
Имре
источник