譯自官方手冊,簡述 Unity 另外一個多線程實現方式,JobSystem,爲 Unity ECS 系統實現的根本html
原文git
JobSystem 管理一組多核中的工做線程(Work Thread),爲避免上下文切換一般一個邏輯核配一個工做線程github
JobSystem 持有一個 Job 隊列,工做線程從該隊列中獲取 Job 執行c#
Job 是執行特定任務的小工做單元,Job 能夠互相依賴緩存
JobSystem 執行時複製而非引用數據,避免了數據競爭,但 JobSystem 只能使用memcpy
複製 blittable types 數據。Blittable types
是 .Net 框架中的數據類型,該類型數據在託管代碼與原生代碼間傳遞無需轉換安全
複製數據來保證線程安全的弊端就是任務的結果也是獨立的,所以使用NativeContainer
將結果儲存在公共內存中多線程
NativeContainer
以相對安全的託管類型的方式指向一個非託管的內存地址,使Job 能夠直接訪問主線程數據而非複製框架
Unity 自帶 NativeContainer
類型爲 NativeArray
,ECS 包又擴展了NativeList
、NativeHashMap
、NativeMultiHashMap
和NativeQueue
svg
默認狀況下,Job 同時擁有NativeContainer
的讀寫權限,但 C# Job System 不容許多個 Job 同時擁有對一個NativeContainer
的寫權限,所以對不須要寫權限的NativeContainer
加上[ReadOnly]
特性,以減小性能影響post
[ReadOnly]
public NativeArray<int> input;
複製代碼
JobSystem 支持多個 Job 同時讀取同一數據
根據 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);
複製代碼
blittable types
或 NativeContainer
類型的成員變量Execute
方法當 Job 執行時,Execute
在一個核上執行一次
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
複製代碼
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 是在調用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();
複製代碼
對於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();
複製代碼
專門用於操做 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 編譯提高效率