C# Job System
概述
設計目的:簡單安全地使用多線程,隨便就能寫出高性能代碼
收益:FPS更高,電池消耗更低(Burst編譯器)
并行性:C# Job System和Unity Native Job System共享工作線程worker threads,也就是它們不會創建超過CPU cores數量的線程,也就不會導致CPU資源搶占問題。
什么是多線程
單線程:一次執行一條指令,產生一個結果
多線程:利用CPU的多核,多條指令同時執行,其他線程執行完成后會將結果同步給主線程。
多線程好的實踐:幾個運行時間很長的任務。
游戲代碼的特點:大量小而短的任務。
解決方案:線程池。
context switching:線程上下文切換,性能敏感的,要盡量避免。
? ? 當激活的線程數超過CPU cores時,就會導致CPU資源爭奪,從而觸發頻繁的context switching。
? ? 過程:先saving執行了一部分的當前線程,然后執行另外的線程,切回來的時候再reconstructing之前的線程再繼續執行。
什么是Job System
簡化多線程:job system通過創建jobs來實現多線程,而不是直接創建thread。
job概念:完成特定任務的一個小的工作單元。job接收參數并操作數據,類似于函數調用。job之間可以有依賴關系,也就是一個job可以等另一個job完成之后再執行。
job system管理一組worker threads,并且保證一個logical CPU core一個worker thread,避免context switching。
job system將jobs放在一個job queue里面,worker threads從job queue里面獲取job然后執行。
job依賴性:job system管理job依賴關系,并保證執行時序的正確性。
C# Job System的Safety System
Race conditions:競爭條件,一個輸出結果依賴于不受控制的事件出現的順序或時機。
在寫多線程代碼時,race conditions是一個很大的挑戰。race conditions不是bug,但它會導致不確定性行為。并且一旦出現,就很難定位,也很難調試,因為它依賴時機,打斷點和加log本身都會改變各個獨立線程執行的時機。
Safety system:為了寫出更安全的多線程代碼,C# Job System會檢查所有的潛在的race conditions并保護代碼不受可能會產生的bug的影響(這句話有點模糊......)。
解決辦法:數據拷貝,每個job操作來自主線程數據的副本,而不是操作原數據。這樣數據獨立,就不會產生race conditions了。
blittable data types:job只能訪問blittable的數據,這些數據在托管代碼和native代碼之間拷貝的時候,不需要做額外的類型轉換。
拷貝方式:memcpy
NativeContainer
NativeContainer實際上是native memory的一個wrapper,包含一個指向非托管內存的指針。
不需要拷貝:使用NativeContainer可以讓一個job和main thread共享數據,而不用拷貝。(copy雖然能保證Safety System,但每個job的計算結果也是分開的)。
可使用的C#類型定義:
| 數據結構 | 說明 | 來源 |
| NativeArray | 數組 | Unity |
| NativeSlice | 可以訪問一個NativeArray的某一部分 | Unity |
| NativeList | 一個可變長的NativeArray | ECS |
| NativeHashMap | key value pairs | ECS |
| NativeMultiHashMap | 一個key對應多個values | ECS |
| NativeQueue | FIFO的queue | ECS |
Safety System安全策略:?? ?
Safety System內置于所有的NativeContainer,會自動跟蹤NativeContainer的讀寫狀態。
? ? 注意:所有的safety checkes都只在Editor和PlayMode模式下生效:bounds checks、deallocation checks、race condition checks。
? ? 還有一部分安全策略:
?? ?? ??DisposeSentinel:自動檢測memory leak并報錯。依賴宏定義ENABLE_UNITY_COLLECTIONS_CHECKS。
?? ?? ??AtomicSafetyHandle:用來轉移NativeContainer的控制權。比如當2個jobs同時寫一個NativeContainer,Safety System就會拋出一個error,并描述如何解決。異常會在產生沖突的job調度時拋出。依賴宏定義ENABLE_UNITY_COLLECTIONS_CHECKS。
?? ?? ? 這種情況下,可以使用job依賴,讓其中一個job依賴另外一個job的完成。
規則:Safety System允許多個job同時read同一塊數據。
規則:Safety System不允許一個job正在writing數據時,調度激活另一個“擁有write權限”的job(不是不讓同時write)。
規則:手動指定job對數據的只讀:(默認是可讀寫,會影響性能)
[ReadOnly]public NativeArray<int> input;注意:job對static data的訪問沒有Safety System安全保護,所以使用不當可能造成crash。
?
NativeContainer Allocator分配器:
(1)Allocator.Temp
?? ?最快,維持1 frame,job不能用,需要手動Dispose(),比如可以再native層的callback調用時使用。
(2)Allocator.TempJbo
? ? 稍微慢一點,最多維持4 frames,thread-safe,如果4 frames內沒有Dispose(),會有warning。大多數small jobs都會使用這個類型的分配器.
(3)Allocator.Persistent
? ? 最慢,但是可持久存在,就是malloc的wrapper。Longer jobs使用這個類型,但在性能敏感的地方不應該使用。
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);創建Job
三要素:
(1)創建一個struct實現接口IJob;
(2)添加數據成員:要么是blittable類型, 要么是NativeContainer;
(3)添加Execute()方法實現。
執行job時,job.Execute()方法會在一個cpu core上執行一次。
注意:job操作數據是基于拷貝的,除非是NativeContainer類型。那么,一個job訪問main thread數據的唯一方式就是使用NativeContainer。
public struct TestJob : IJob {public float a;public float b;public NativeArray<float> result;public void Execute(){result[0] = a + b;} }調度Job
三要素:
(1)實例化job;
(2)設置數據;
(3)調用job.Schedule()方法。
調用Schedule方法會將job放到job queue里面等待執行。一旦開始schedule,就沒法中斷job了。(疑問:這個once scheduled,是job.Schedule方法,還是從job queue里面拿出來開始執行?)
private void TestScheduleJob() {// Create a native array of a single float to store the result. This example waits for the job to complete for illustration purposesNativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);// Set up the job dataMyJob jobData = new MyJob();jobData.a = 10;jobData.b = 10;jobData.result = result;// Schedule the jobJobHandle handle = jobData.Schedule();// Wait for the job to completehandle.Complete();// All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArrayfloat aPlusB = result[0];// Free the memory allocated by the result arrayresult.Dispose(); }JobHandle和Job依賴
設置job依賴關系:
JobHandle firstJobHandle = firstJob.Schedule(); secondJob.Schedule(firstJobHandle);secondJob依賴firstJob的結果。
組合依賴項:
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob); // Populate `handles` with `JobHandles` from multiple scheduled jobs... JobHandle jh = JobHandle.CombineDependencies(handles);在main thread中等待jobs執行完成:
? ??flush job:使用JobHandle.Complete()來等待job執行完成。
?? ?job只有Schedule之后才會執行,如果你想在main thread中訪問job的正在使用的數據,你可以調用JohHandle.Comlete()。該方法flush job,并開始執行,然后將NativeContainer的數據權限返回給main thread。
? ? 如果你不需要訪問數據,也可以調用統一static flush函數:JobHandle.ScheduleBatchedJobs(),當然該方法會影響到性能。
public struct MyJob : IJob {public float a;public float b;public NativeArray<float> result;public void Execute(){result[0] = a + b;} } public struct AddOneJob : IJob {public NativeArray<float> result;public void Execute(){result[0] = result[0] + 1;} }private void TestScheduleJob() {NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);MyJob jobData = new MyJob();jobData.a = 10;jobData.b = 10;jobData.result = result;JobHandle firstHandle = jobData.Schedule();AddOneJob incJobData = new AddOneJob();incJobData.result = result;JobHandle secondHandle = incJobData.Schedule(firstHandle);secondHandle.Complete();float aPlusB = result[0];result.Dispose(); }ParallelFor jobs 并行job
IJob只能一次一個job執行一個任務,但游戲開發中經常需要重復執行某個動作很多次,這時候就可以用到并行任務IJobParallelFor。
?? ?ParallelFor jobs使用NativeArray作為數據源,并且運行在多個core上,還是一個job一個core,只是每個job只負責處理完整數據的一個子集。
? ? Execute(idx)方法對于數據源NativeArray中的每個item都調用一次。
?調度:
需要手動指定執行次數,表示需要分多少次獨立Execute來執行,一般直接取NativeArray的數組長度作為執行次數,一次處理一個數據。? ? ? ?
當一個native job提前完成它的batches,它會從其他的native job偷取一部分batches,然后繼續執行。
顆粒度問題:分得太細會有work不斷重建的開銷,分得太粗又會有單核負載問題。
嘗試法:所以最佳實踐是從1開始逐步增加,直到性能不再提高。
public struct MyParallelJob : IJobParallelFor {public NativeArray<float> a;public NativeArray<float> b;public NativeArray<float> result;public void Execute(int index){result[index] = a[index] + b[index];} }private void TestScheduleParallelJob() {NativeArray<float> a = new NativeArray<float>(10, Allocator.TempJob);NativeArray<float> b = new NativeArray<float>(10, Allocator.TempJob);NativeArray<float> result = new NativeArray<float>(10, Allocator.TempJob);for(int i = 0; i < 10; ++i){a[i] = i * 0.3f;b[i] = i * 0.5f;}MyParallelJob jobData = new MyParallelJob();jobData.a = a;jobData.b = b;jobData.result = result;JobHandle handle = jobData.Schedule(10, 1);handle.Complete();for(int i = 0; i < 10; ++i){Debug.LogError(result[i]);}a.Dispose();b.Dispose();result.Dispose(); }ParallelForTransform jobs
public struct MyTransformParallelJob : IJobParallelForTransform {public void Execute(int index, TransformAccess transform){} }注意事項:
(1)不能在job中訪問static數據
? ? 在job中訪問static數據是沒有Safety System保證的,可能會導致crash。unity后續版本會增加static analysis來阻止這種用法。
?
(2)Flush scheduled batchs
? ? JobHandle.ScheduleBatchedJobs:當你想要你的job開始執行是,可以調用這個函數flush調度的batch。
? ? 不flush batch會導致調度延遲到主線程等待batch執行結果時才觸發執行。
? ? JobHandle.Complete:直接開始執行。
? ? 在ECS中,batch flush是隱式執行的,不需要手動調用JobHandle.ScheduleBatchJobs。
?? ?
(3)不要試圖更新NativeContainer的內容
? ? 因為缺乏ref returns機制,所以不要這樣用:
nativeArray[0]++;// 等同于:var tmp = nativeArray[0];tmp++;// 不生效!// 正確的寫法是:var tmp = nativeArray[0];tmp++;nativeArray[0] = tmp;MyStruct temp = myNativeArray[i]; temp.memberVariable = 0;myNativeArray[i] = temp;(4)調用JobHandle.Complete來讓main thread重獲控制權
? ? 主線程在訪問數據之前,需要依賴的job調用complete。不能只是check JobHandle.IsCompleted,而是需要手動調用JobHandle.Complete()。
? ? 此調用還會清理Safety System的狀態,不調用的話會有內存泄漏。
?
(5)在主線程中使用Schedule和Complete
? ? 這兩個函數只能在主線程中調用。不能因為一個job依賴另一個job,就在前一個job中手動schedule另一個job。
?
(6)在正確的時間使用Schedule和Complete
? ? Schedule:在數據填充完畢,立馬調用
? ? Complete:只在你需要result的時候調用
?? ?
(7)NativeContainer添加read-only標記
? ? 默認是可讀寫的,如果確定只讀就標記為read-only,可以提升性能。
?
(8)檢查數據依賴
? ? 如果在profiler里看到main thread有“WaitForJobGroup”,就表示在等待worker thread處理完成。也就是說你的代碼里面在什么地方引入了一個data dependency,這時候可以通過檢查JobHandle.Complete來看一下是什么依賴關系導致了main thread需要等待的情況。
?
(9)調試jobs
? ? Jobs有一個Run函數,你可以用它來替換原本調用Schedule的地方,從而在main thread上立即執行這個job??梢允褂眠@個方法來調試。
?
(10)不要在job里面分配托管內存managed memory
? ? 在job里面分配托管內存是非常慢的,而且會導致Burst compiler沒法使用。
? ? Burst是基于LLVM的后端編譯技術,它可以利用平臺特定能力將c# jobs代碼編譯成高度優化過的機器碼。
?
Unity GDC 2018: C# Job System
https://www.youtube.com/playlist?list=PLX2vGYjWbI0RuXtGMYKqChoZC2b-H4tck
?
Unity at GDC - Job System & Entity Component System
https://www.youtube.com/watch?v=kwnb9Clh2Is&t=1s
?
Job System介紹
http://www.pianshen.com/article/634466006/
總結
以上是生活随笔為你收集整理的C# Job System的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 光大携程菁英白金信用卡好申请吗?想说爱你
- 下一篇: Unity游戏开发——C#特性Attri