Поддержка многопоточности в Entity Component System
Иногда нам надо обработать очень много данных, причем логика достаточно сложна и занимает много времени процессора. Соверменные процессоры имеют несколько ядер, между которыми мы можем распределять вычисления для параллельного исполнения. Как получить подобное поведение в моем ECS фреймворке без значительных трудозатрат и переработке кода?
Самое простое решение - запустить несколько новых потоков, послать в них нужные данные, обработать и получить ответ. Вроде все просто, но существует ряд проблем.
Проблемы
Синхронизация операций чтения / записи operation данных
Как решение, мы можем разделить данные на изолированные блоки и слать каждый блок в отдельный “worker”-поток - в этом случае мы можем обращаться к этим данным без дополнительной синхронизации. По этой же причине мы не можем создавать новые сущности и события добавления / удаления / изменения данных компонентов - синхронизация записи серьезно ударит по производительности.
Получение результатов работы потоков обратно в основной поток
Как решение, мы можем блокировать основной поток и ждать завершения работы все потоков. Это работает достаточно неплохо в текущей версии ECS фреймворка - многопоточная система может быть стандартной IEcsRunSystem
-системой и работать как часть EcsSystems
группы. Как дополнительная опция - синхронизация с основным потоком может быть выполнена позже через специальные методы синхронизации.
[Обновлено 28.11.2018] Поддержка многопоточности вынесена в отдельный опциональный репозиторий. Основной поток теперь является таким же “worker”-ом и требует принудительной синхронизации в пределах текущей системы для обеспечения целостности ECS-данных.
Количество данных не может быть обработано за одинаковое время с фиксированным размером блока данных для потока
Есть 2 случая:
- “Worker” может обработать только фиксированное количество сущностей. Если “worker”-ов не хватает на обработку всех данных - менеджер задач блокирует основной поток и ждет, когда освободится очередной “worker” и нагружает его новыми данными. Это повторяется пока все данные не будут обработаны.
- Все сущности делятся поровну между “worker”-ами. Это может быть полезным для уменьшения времени блокировки основного потока, но уменьшает контроль над времене исполнения каждого “worker”-а.
В текущей версии реализован первый случай, но второй выглядит более привлекательным.
[Обновлено 28.11.2018] В текущей версии реализована смешанная схема. Каждый “worker” имеет минимальный размер блока данных, который он готов обработать. Как только количество данных перестает вмещаться в эти лимиты по количеству текущих “worker”-ов - данные начинают равномерно распределяться между всеми “worker”-ами.
Пример
Любая многпоточная ECS-система должна наследоваться от класса EcsMultiThreadSystem
:
1 | class TestMultiThreadSystem : EcsMultiThreadSystem { |
Тесты производительности
Условия
3 системы с одинаковой логикой обработки каждой сущности:
- Стандартная
IEcsRunSystem
-система EcsMultiThreadSystem
-система с 4 “worker”-амиEcsMultiThreadSystem
-система с 8 “worker”-ами
Результаты
- Стандартная
IEcsRunSystem
-система: 532ms EcsMultiThreadSystem
-система с 4 “worker”-ами: 118msEcsMultiThreadSystem
-система с 8 “worker”-ами: 34ms
Выглядит неплохо, но может стать лучше при условии распределения данных между “worker”-ами вместо работы исключительно с фиксированным блоком данных. Это поведение может быть изменено в будущем, не забывайте следить за обновлениями!