Создание и запуск задания
Для создания и успешного выполнения задания необходимо выполнить следующие действия:
- Создание рабочего места:
IJobinterface. - Расписание задания: Вызов метода
Scheduleв задании. - Дождаться завершения задания: возвращается немедленно, если задание уже завершено, и вы можете вызвать
Completeметод на работе, когда вы хотите получить доступ к данным.
Создать задание
Чтобы создать задание в Unity, реализуйте IJob интерфейс. Вы можете использовать свой IJob реализация для планирования одного задания, которое выполняется параллельно с любыми другими заданиями, которые выполняются.
IJob имеет один требуемый метод: Execute, который Unity ссылается, когда рабочая нить выполняет задание.
При создании задания можно также создать для него JobHandle, которые должны использоваться другими методами для ссылки на задание.
Важно: От обращения к нестатическим и изменяемым статическим данным изнутри задания ничто не защищает. Такое обращение обходит все системы безопасности и может привести к аварийному завершению вашего приложения или Unity Editor.
При запуске Unity система заданий создает копию данных запланированного задания, что предотвращает чтение или запись одних и тех же данных более чем одним потоком. После завершения задания доступны только данные, записанные в NativeContainer. Это происходит потому, что и копия NativeContainer, используемая заданием, и оригинальный объект NativeContainer указывают на одну и ту же память. Дополнительную информацию см. в документации по Безопасные типы потока.
Когда система заданий получает задание из очереди заданий, она запускает метод Execute один раз на одном потоке. Как правило, система заданий запускает задания на фоновых потоках, но она может выбрать главный поток, если он становится бездействующим. По этой причине вам следует проектировать задания так, чтобы они выполнялись под рамкой.
Запланировать задание
Чтобы запланировать задание, позвоните Schedule. Это помещает задание в очередь заданий, и система заданий начинает выполнять задание, как только все его зависимости, если таковые имеются, завершено. После планирования, вы не можете прервать работу. Вы можете только вызвать Schedule из основной нити.
Совет: Задания имеют метод Run, который можно использовать вместо Schedule для немедленного выполнения задания на главном потоке. Вы можете использовать его для отладки.
Завершить работу
Как только вы вызовете Schedule и система заданий выполнит задание, вы можете вызвать методы Complete на JobHandle для доступа к данным в задании. Лучше всего вызывать Complete как можно позже в коде. Когда вы вызываете Complete, основной поток может безопасно получить доступ к экземплярам NativeContainer, которые использовались заданием. Вызов Complete также очищает состояние в системе безопасности. Если этого не сделать, возникнет утечка памяти.
Примеры работ
Ниже приведен пример задания, которое суммирует два значения с плавающей запятой. В нем реализованы IJob, используется NativeArray для получения результатов задания и используется метод Execute с реализацией задания внутри него:
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
// Job adding two floating point values together
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
Следующий пример основан на задании MyJob для планирования задания на основном потоке:
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
public class MyScheduledJob : MonoBehaviour
{
// Create a native array of a single float to store the result. Using a
// NativeArray is the only way you can get the results of the job, whether
// you're getting one value or an array of values.
NativeArray<float> result;
// Create a JobHandle for the job
JobHandle handle;
// Set up the job
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
// Update is called once per frame
void Update()
{
// Set up the job data
result = new NativeArray<float>(1, Allocator.TempJob);
MyJob jobData = new MyJob
{
a = 10,
b = 10,
result = result
};
// Schedule the job
handle = jobData.Schedule();
}
private void LateUpdate()
{
// Sometime later in the frame, wait for the job to complete before accessing the results.
handle.Complete();
// All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArray
// float aPlusB = result[0];
// Free the memory allocated by the result array
result.Dispose();
}
}
Рекомендации по планированию и завершению
Лучше всего вызывать Schedule на задание, как только у вас есть необходимые данные, и не вызывать Complete на нем, пока вам не нужны результаты.
Вы можете запланировать менее важные задания в той части кадра, где они не конкурируют с более важными заданиями.
Например, если между концом одного кадра и началом следующего кадра имеется период, в котором не выполняются никакие задания, и приемлемым является задержка одного кадра, можно запланировать задание на конец кадра и использовать его результаты в следующем кадре. В качестве альтернативы, если приложение насыщено другими заданиями в этот переходный период, а в другом месте кадра имеется недоиспользуемый период, более эффективно запланировать задание именно там.
Вы также можете использовать Profiler чтобы увидеть, где Unity ждет завершения работ. Маркер WaitForJobGroupID на главном потоке указывает на это. Этот маркер может означать, что вы ввели зависимость данных где-то, что вы должны разрешить. JobHandle.Complete чтобы выяснить, где у вас есть зависимости данных, которые заставляют главный поток ждать.
Избегайте использования длинных заданий
В отличие от потоков, задания не приводят к выполнению. После начала задания, этот поток рабочего задания обязуется завершить задание перед запуском любого другого задания. Таким образом, наилучшей практикой является разделение длинных запущенных заданий на меньшие задания, которые зависят друг от друга, вместо подачи заданий, которые занимают много времени для завершения относительно других заданий в системе.
Система заданий обычно запускает несколько цепей зависимостей заданий, так что если вы разбиете длинные выполняемые задачи на несколько частей, есть шанс, что несколько цепей заданий будут продвигаться. Если вместо этого система заданий заполнена длительными выполняемыми заданиями, они могут полностью потреблять все рабочие потоки и блокировать независимые задания от выполнения. Это может выдвинуть время завершения важных заданий, которые главный поток явно ожидает, что приводит к застоям на главном потоке, которые в противном случае не существовали бы.
В частности, длительно выполняемые задания IJobParallelFor негативно влияют на систему заданий, поскольку эти типы заданий намеренно пытаются запускаться на как можно большем количестве рабочих потоков для размера пакета заданий. Если вы не можете разделить длинные параллельные задания, рассмотрите возможность увеличения размера пакета вашего задания при его планировании, чтобы ограничить количество рабочих, подбирающих длительно выполняемое задание.
MyParallelJob jobData = new MyParallelJob();
jobData.Data = someData;
jobData.Result = someArray;
// Use half the available worker threads, clamped to a minimum of 1 worker thread
const int numBatches = Mathf.Max(1, JobsUtility.JobWorkerCount / 2);
const int totalItems = someArray.Length;
const int batchSize = totalItems / numBatches;
// Schedule the job with one Execute per index in the results array and batchSize items per processing batch
JobHandle handle = jobData.Schedule(result.Length, totalItems, batchSize);