Unity學習—JobSystem

譯自官方手冊,簡述 Unity 另外一個多線程實現方式,JobSystem,爲 Unity ECS 系統實現的根本html

原文git

JobSystem

JobSystem 管理一組多核中的工做線程(Work Thread),爲避免上下文切換一般一個邏輯核配一個工做線程github

JobSystem 持有一個 Job 隊列,工做線程從該隊列中獲取 Job 執行c#

Job 是執行特定任務的小工做單元,Job 能夠互相依賴緩存

線程安全

JobSystem 執行時複製而非引用數據,避免了數據競爭,但 JobSystem 只能使用memcpy複製 blittable types 數據。Blittable types 是 .Net 框架中的數據類型,該類型數據在託管代碼與原生代碼間傳遞無需轉換安全

NativeContainer

複製數據來保證線程安全的弊端就是任務的結果也是獨立的,所以使用NativeContainer將結果儲存在公共內存中多線程

NativeContainer以相對安全的託管類型的方式指向一個非託管的內存地址,使Job 能夠直接訪問主線程數據而非複製框架

Unity 自帶 NativeContainer類型爲 NativeArray,ECS 包又擴展了NativeListNativeHashMapNativeMultiHashMapNativeQueuesvg

默認狀況下,Job 同時擁有NativeContainer的讀寫權限,但 C# Job System 不容許多個 Job 同時擁有對一個NativeContainer的寫權限,所以對不須要寫權限的NativeContainer加上[ReadOnly]特性,以減小性能影響post

[ReadOnly]
public NativeArray<int> input;
複製代碼

JobSystem 支持多個 Job 同時讀取同一數據

NativeContainer Allocator

根據 Job 執行時長決定使用哪一種 Allocator

  • Allocator.Temp

    最快的分配方法,適用於一幀或幾幀的生命時長,不能將該類型分配的數據傳給 Job,在方法 Return 前執行Dispose

  • Allocator.TempJob

    分配速度比 Temp 慢比 Persistent 快,4幀的生命時長且線程安全。若四幀內沒有調用Dispose,控制檯會打印原生代碼生成的警告。大部分小任務都使用該類型分配NativeContainer

  • Allocator.Persistent

    是對malloc的包裝,可以維持儘量地生命時長,性能不足的狀況下不該使用

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
複製代碼

建立 Job

  1. 聲明實現 IJob 接口的結構體
  2. 添加blittable typesNativeContainer類型的成員變量
  3. 實現Execute方法

當 Job 執行時,Execute在一個核上執行一次

public struct MyJob : IJob
{
    public float a;
    public float b;
    public NativeArray<float> result;

    public void Execute()
    {
        result[0] = a + b;
    }
}
複製代碼

調度 Job

  1. 建立 Job
  2. 填充 Job 數據
  3. 調用Schedule方法

只能在主線程調用Schedule方法,將 Job 放入隊列等待執行,一旦 Job 被調度進隊列舊沒法中斷

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

// 填充數據
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;

// 調度 Job
JobHandle handle = jobData.Schedule();

// 等待完成
handle.Complete();

// 全部 NativeArray 指向同一內存,可外部訪問
float aPlusB = result[0];

// 釋放 result array
result.Dispose();
複製代碼

JobHandle 和 dependencies

JobHandle 是在調用Schedule返回的句柄,可以使用該句柄做爲參數傳入另外一個 Job 的Schedule做爲依賴,使後者等待前者執行完成再執行

JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);
複製代碼

對於多個依賴的 Job 可以使用JobHandle.CombineDependencies組合這些 JobHandle

NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);
// 填充 handles
JobHandle jh = JobHandle.CombineDependencies(handles);
複製代碼

調用 JobHandle 的Complete方法可以使主線程等待任務執行完成以安全訪問該 Job 使用的 NativeContainer,該方法會從內存中刷新 Job 並開始執行而後將該 Job 中的NativeContainer持有權返回主線程

若不需訪問數據,但須要當即刷新執行 Job 緩存,則能夠使用JobHandle.ScheduleBatchedJobs,但會影響性能

public struct MyJob : IJob
{
    public float a;
    public float b;
    public NativeArray<float> result;

    public void Execute()
    {
        result[0] = a + b;
    }
}

// Job adding one to a value
public struct AddOneJob : IJob
{
    public NativeArray<float> result;
    
    public void Execute()
    {
        result[0] = result[0] + 1;
    }
}

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

// Setup the data for job #1
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;

// Schedule job #1
JobHandle firstHandle = jobData.Schedule();

// Setup the data for job #2
AddOneJob incJobData = new AddOneJob();
incJobData.result = result;

// Schedule job #2
JobHandle secondHandle = incJobData.Schedule(firstHandle);

// Wait for job #2 to complete
secondHandle.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();
複製代碼

IJobParallelFor

對於IJob,同一時間一個 Job 只能執行一個任務,若想同一時間執行多個相同的任務,則能夠使用IJobParallelFor

一種使用場景爲 ParallelFor Job 在多核上同時對同一 NativeArray 進行操做,每核僅負責部分工做,ParallelFor Job 的Execute方法會傳入index,用於訪問數據源

struct IncrementByDeltaTimeJob: IJobParallelFor
{
    public NativeArray<float> values;
    public float deltaTime;

    public void Execute (int index)
    {
        float temp = values[index];
        temp += deltaTime;
        values[index] = temp;
    }
}
複製代碼

在調度 ParallelFor Job 時需規定調度任務總長度和每批次長度,C# Job System 會根據批次長度將任務總長分批,再放入 Unity Job 隊列,每批同步執行,每批次任務內僅執行一個 Job

當一個 Native Job 先完成時,它會「竊取」其餘 Job 的一半批任務,既優化了性能,又保證了內存訪問局部性

批次數越低,線程間的任務分配越均勻,但也會帶來額外開銷,所以須要逐一測試出最佳性能的批次數

public struct MyParallelJob : IJobParallelFor
{
    [ReadOnly]
    public NativeArray<float> a;
    [ReadOnly]
    public NativeArray<float> b;
    public NativeArray<float> result;

    public void Execute(int i)
    {
        result[i] = a[i] + b[i];
    }
}

NativeArray<float> a = new NativeArray<float>(2, Allocator.TempJob);

NativeArray<float> b = new NativeArray<float>(2, Allocator.TempJob);

NativeArray<float> result = new NativeArray<float>(2, Allocator.TempJob);

a[0] = 1.1;
b[0] = 2.2;
a[1] = 3.3;
b[1] = 4.4;

MyParallelJob jobData = new MyParallelJob();
jobData.a = a;  
jobData.b = b;
jobData.result = result;

// Schedule the job with one Execute per index in the results array and only 1 item per processing batch
JobHandle handle = jobData.Schedule(result.Length, 1);

// Wait for the job to complete
handle.Complete();

// Free the memory allocated by the arrays
a.Dispose();
b.Dispose();
result.Dispose();
複製代碼

ParallelForTransform

專門用於操做 Transform 的 Parallel Job

注意事項

  • 不要使用 Job 訪問靜態數據

    從 Job 訪問靜態數據會繞開全部安全系統,可能會致使 Unity 崩潰

  • 使用 JobHandle.ScheduleBatchedJobs 方法當即執行已調度的 Job

    Job 在被調度後會被緩存不會當即執行,該方法可當即清空緩存隊列中的 Job 並執行,但會影響性能,或調用 JobHandle.Complete 執行,ECS 系統已經隱式清空了緩存,以你無需主動調用

  • 不要更新 NativeContainer 內容

    因爲ref returns的缺陷,沒法直接修改 NativeContainer 中的內容,需按以下方式

    MyStruct temp = myNativeArray[i];
    temp.memberVariable = 0;
    myNativeArray[i] = temp;
    複製代碼
  • 調用 JobHandle.Complete 重獲全部權

    在主線程或新的 Job 使用前一 Job 佔用的NativeContainer數據前,必須調用JobHandle.Complete從新獲取其全部權,該方法會清空安全機制的狀態,不然會致使內存泄漏( 不能僅查看JobHandle.IsCompleted狀態)

  • 只能在主線程調用 Schedule 和 Complete 方法

    這兩種方法只能在主線程調用,若一個 Job 依賴於另外一個 Job,則在主線程使用 JobHandle

  • Schedule 和 Complete 的正確時機

    準備數據完成時便可調用 Schedule,僅當須要結果時才調用 Complete,如在一幀的結尾與下一幀開始的空檔中調度一個 Job

  • 使用 [ReadOnly] 標記 NativeContainer

    Job 同時用於對 NativeContainer 的讀寫權限,使用 [ReadOnly] 標記只讀 Job 中的 NativeContainer 可提高效率

  • 檢查數據依賴

    在 Profiler 窗口中,主線程上的 WaitForJobGroup 標記代表 Unity 在等待一個工做線程的任務完成,該標記可能意味着須要解決的數據依賴,可經過查找JobHandle.Complete找到這些依賴

  • Debugging jobs

    能夠調用Run方法取代Schedule在主線程執行 Job

  • 不要在 Job 中分配託管內存

    在 Job 中分配託管內存會很是慢,且沒法使用 Burst 編譯提高效率

相關文章
相關標籤/搜索