Skip to content

Latest commit

 

History

History
148 lines (88 loc) · 11.7 KB

TaskScheduler-ru.md

File metadata and controls

148 lines (88 loc) · 11.7 KB

Система асинхронных задач, планировщик и менеджер потоков.

Асинхронные задачи (AsyncTask)

Базовый интерфейс для всех асинхронных задач.
Хранит статус выполнения задачи, зависимости, тип очереди и тд.

Виртуальный метод Run() гарантированно выполнится только после того как все входные зависимости завершены - их методы Run или OnCancel были вызваны. Перед вызовом Run() кэш ЦП загружается (acquire), а после вызова - выгружается (release), дополнительные синхронизации не требуются.

Внутри реализации метода Run() можно вызвать:

  • метод OnFailure() - ошибка при выполнении задачи, меняет статус и вызывает метод OnCancel() после завершения Run().
  • метод Continue() - возвращает задачу в очередь, чтобы повторно вызвать метод Run(). Может принимать список зависимых задач.

Исходник: AsyncTask.h

Корутины (CoroTask)

Корутины из C++20 сделанные поверх AsyncTask.
Интерфейс сделан через неблокирующие co_await:

bool        co_await Coro_IsCanceled
EStatus     co_await Coro_Status
ETaskQueue  co_await Coro_TaskQueue

Исходник: AsyncTask.h

Корутины (Coroutine<>)

Корутины из C++20 сделанные поверх AsyncTask, могут хранить значение внутри и возвращать его через co_await после выполнения задачи.

Исходник: Coroutine.h, Тесты

Промис (Promise<>)

Повторяет функционал корутин на C++17, который поддерживается многими компиляторами.

MakePromiseFromValue() - если аргумент dependsOn пустой, то задача не добавляется в очередь.
MakePromiseFrom() и MakePromiseFromArray() - объединяют результаты промисов в один.

Исходник: Promise.h, Тесты

Типы очередей (ETaskQueue / EThread)

  • Main - единственный поток, который обрабатывает вызовы ОС (окно, ввод и тд).
  • PerFrame - поток с высоким приоритетом, предназначен для коротких задач в пределах кадра, это физика, логика, графика и тд. Разрешен доступ к Vulkan и Metal API.
  • Renderer - аналогично PerFrame, но используется чтобы ограничить количество потоков с доступом к командному пулу (Vulkan). Должны использоваться только RenderTask и RenderTaskCoro. Память для записи команд в буфер выделяется под каждый поток (VkCommandPool).
  • Background - рабочий поток с низким приоритетом. Рекомендуется использовать для тяжелых задач:
    • Графическое АПИ: компиляция пайплайнов, выделение памяти и тд.
    • Доступ к файлам.
    • Работа с сетью.
  • FileIO - не является очередью, нужен для передачи управления в ОС, чтобы обработать завершенные асинхронные команды чтения/записи в файл.

Исходник: EThread.h

Зависимости между задачами

Поддерживаются сильные и слабые зависимости.

Если слабая зависимость отменяется или завершается с ошибкой, то зависящая от него задача все равно запустится. Явно указывается через WeakDep{task}.

Если сильная зависимость отменяется или завершается с ошибкой, то зависящая от него задача также отменяется. Неявно используется сильная зависимость, явно указывается через StrongDep{task}.

Планировщик задач для ЦП (TaskScheduler)

Метод Run() используется для добавления задач в очередь, в случае ошибки задача помечается как отмененная. Если не получилось создать задачу (исключение в конструкторе и тд), то возвращается задача-заглушка с отмененным состоянием.

Метод ProcessTask() вызывается потоком, чтобы найти задачу в указанных очередях и выполнить ее - вызывается AsyncTask::Run(), либо если задача отменена, то вызывается AsyncTask::OnCancel() и продолжается поиск задачи.

Исходник: TaskScheduler.h

Кастомизация зависимостей для асинхронных задач (ITaskDependencyManager)

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

Менеджер зависимостей хранит указатель на задачу и номер бита, к которому привязана зависимость.

Исходник: TaskScheduler.h

Управление потоками (ThreadManager)

Встроенная в движок реализация потоков для обработки задач, пользователи могут сделать свою реализацию IThread и добавить ее в планировщик (TaskScheduler::AddThread).

Стандартная реализация потоков принимает список типов очередей, поток будет выполнять задачи только из этого списка и в том порядке, в котором они расположены в списке. Например поток с EThreadArray{ ETaskQueue::PerFrame, ETaskQueue::Renderer } будет выполнять PerFrame задачи с большим приоритетом.

ThreadManager также распределяет потоки по ядрам ЦП. Если ЦП содержит энергоэффективные ядра, то потоки с Background, FileIO будут привязаны к этим ядрам, а потоки с Main, PerFrame, Renderer будут привязаны к производительным ядрам ЦП. Параметр bindThreadToPhysicalCore определяет будет ли привязка к физическим ядрам или к логическим (2 потока на ядро), потоки с Background, FileIO всегда привязываются к логическим ядрам, так как могут дольше простаивать.

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

Исходник: ThreadManager.h

Примитивы синхронизации

Блокирующие примитивы синхронизации не должны использоваться, за исключением коротких блокировок.

AsyncMutex

Используется по аналогии с Mutex, но эксклюзивная блокировка достигается за счет зависимостей между задачами.
Недостатки:

  • Разблокируется только после выполнения всего метода IAsyncTask::Run().
  • Планировщик потоков решает когда запустить следующую задачу, поэтому между ними может быть большой интервал бездействия.

Исходник: AsyncMutex.h

Производительность

Производительность планировщика зависит от скорости добавления и поиска задачи в очереди. Очередь работает без блокировок (lock-free), используется несколько внутренних очередей и распределение поиска по ним в зависимости от потока (EThreadSeed).

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

Пример худшего случая 1
Всего задач: 350k
Максимальное количество задач в очереди: 25k
Время выполнения задачи: 7мкс
Потеря времени в планировщике: 30%

Пример худшего случая 2
Всего задач: 350k
Максимальное количество задач в очереди: 32k
Время выполнения задачи: 22мкс
Потеря времени в планировщике: 12%

Пример хорошего случая
Всего задач: 350k
Максимальное количество задач в очереди: 32k
Время выполнения задачи: 110мкс
Потеря времени в планировщике: 2.7%

Тесты