Unity-JobSystem-官方文檔自譯

C# Job System

以下內容爲筆者人工翻譯,部分理解可能會有偏差,歡迎指正!

NativeContainer相關內容,請參考筆者另一篇譯文:https://mp.csdn.net/console/editor/html/105369322


overview


    Unity C#Job 允許使用者編寫簡單易用的多線程代碼,並且提供了和Unity本身良好的交互。

    編寫多線程代碼可以提供很好的性能表現,比如非常重要的幀率表現,使用Job的BurstCompile可以生成更高質量的代碼,並且可以在移動設備上減少電量的持續消耗。

    一個很重要的地方是,job系統整合了Unity的內部資源(Unity本地的工作系統),業務代碼和Unity共享工作線程。這個合作避免了創建大於內核數量的線程,從而避免了cpu資源的爭奪。


什麼是Job System?

    Job System通過創建一個Job而不是線程來管理多線程代碼,Job System在多個內核之間管理工作線程,通常一個cpu內核有一個初始線程,來避免線程上下文的切換。

    Job System將多個Job放入一個工作隊列去執行,工作線程從這裏獲取並執行他們,這個系統同樣需要管理他們的依賴,並確保jobs按照合適的順序執行。


what is a job?

    Job是一個很小的工作單元,接收參數並操作數據,類似一個函數方法的執行行爲,job可能是獨立的,也可能依賴於其他job的執行結果。


什麼是job之間的依賴?

    在複雜的系統裏,比如遊戲開發的需求,通常job並不會是獨立的,一個job通常是爲另一個job準備數據,Job系統支持了這個特性,如果任務a依賴於任務b,系統會保證他們有正確的執行順序。


C# Job System裏的安全系統

    當你編寫多線程代碼的時候,通常會有Condition Race的風險,當一個操作的輸出依賴於其控制範圍之外的另一個進程的執行時間時,就會出現Condition Race。
    Condition Race通常不算是bug,但是是一種不確定的行爲,當一個Condition Race引發一個bug,由於時間的不確定性,將會很難發現bug的真正原因,因爲只能依賴Condition Race才能重現bug。在代碼中debug可能導致問題莫名消失,因爲斷點調試和log會改變線程的時間節點,Condition Race大大增加了多線程開發的難度。


安全系統

    爲了讓多線程開發變得簡單,C# job system檢測所有潛在的Condition Race,並且規避可能導致的bug。
    比如:當在你的主線程代碼中向job發送了一個數據的引用,此時無法查證當主線程在讀取數據的同時,是否有job在嘗試寫入,此時便產生Condition Race。

    C# JobSystem通過發送每個job一份操作數據的拷貝,而不是主線程數據的引用,來解決Condition Race問題。這份拷貝隔離了數據,因此得以解決Condition Race問題。

    Job System需要拷貝數據,意味着job只能訪問blittable的數據類型,這些數據類型在本地和託管代碼中不需要轉換。

    job system可以使用memcpy來拷貝blittable的數據,並且在unity的本地堆和託管堆之間轉換,他在調度job的時候,使用memcpy來將數據放入本地內存,並賦予數據的管理權限。


創建Jobs

    在Unity中創建一個Job的話,需要實現IJob接口,IJob可以讓你的Job和其他正在運行的Job同時執行,tips:job是指Unity中任何實現IJob接口的結構。

    通過以下步驟創建Job:
        1.    創建一個實現IJob的結構
        2.    添加需要的成員變量(blittable type或NativeContainer類型)
        3.    在你的結構中創建一個Excute方法,來實現Job需要的功能

        執行Job的時候,每個內核執行一次Excute方法,注意:設計Job的時候,謹記他們是在操作一份數據的copy,除非使用NativeContainer。因此在主線程中訪問數據的唯一辦法是寫入nativeContainer。



調度Job

   通過以下步驟在主線程中調度一個Job:
        1.    實例化Job
        2.    添加Job數據
        3.    調用Schedule方法
        調用Schedule方法會將該Job放入Job隊列,在適當的時間執行,一旦被調度好之後,就沒辦法被打斷。
        
        注意:只能在主線程中執行Schedule方法

  JobHandle和依賴:
        Job的Schedule方法會返回一個JobHandle,這個HobHandle在代碼中可以作爲其他Job的依賴,如果一個Job依賴另外一個    Job的執行結果,可以將第一個job的handle作爲第二個job的schedule方法的參數。

  合併依賴:
        如果一個job有多個依賴,可以用JobHandle.CombineDependencies方法去合併,該方法可以讓你把多個依賴作爲       Schedule方法的參數。

在主線程中等待Jobs
        在主線程使用Jobhandle等待Job執行完成,需要執行JobHandle的Complete方法,因此主線程可以安全的訪問Job的NativeContainer數據。

        注意:Jobs不會在你Schedule的時候立刻執行,如果在主線程中等待Jobs執行完畢,並且需要訪問NativeContainer的數據,可以調用JobHandle.Complete方法,這個方法會清空內存中緩存的所有Job並開始執行進程,調用JobHandle.Complete會返回給主線程該Job的NativeContainer所有權,你需要在主線程中執行handler的Complete方法來安全的訪問數據。還有一種使用依賴關係的方式來獲取Jobs的NativeContainer的所有權,比如,你可以直接調用JobA的Complete方法,或者調用依賴JobA的JobB的Complete方法,兩種結果都能達到一樣的結果。

        此外,如果不需要讀寫數據,但是需要直接清空合併的Job依賴,可以調用JobHandle.ScheduleBatchedJobs靜態方法,但是注意,這個方法的調用會對性能有負面的影響。


C#Jobs系統注意事項和排錯


    使用Unity的Job System,需要注意以下幾點:

不要從Job中訪問靜態數據
        從Job中訪問靜態數據,會繞過所有的安全系統(Safety System),如果你訪問了錯誤的數據,很可能導致Unity莫名崩潰,比如,在domain reloads的時候訪問Monobehaviour會導致崩潰。
        注意:由於這個風險,未來的Unity版本會通過靜態分析的方式避免Jobs全局變量的訪問,如果你在一個Job中訪問靜態數據,需要意識到你的代碼可能會在未來的Unity版本中掛掉。

清空調度好的合併任務
        如果你想讓你的任務開始執行,可以使用JobHandle.ScheduleBatchedJobs方法,注意:該方法的調用可能會導致性能問題,不去清空合併的任務會導致任務調度延遲,直到主線程獲得執行結果,任何其他情況,使用JobHandle.Complete來開始處理流程。
        注意:Unity ECS系統已經隱式處理好了,因此沒必要調用JobHandle.ScheduleBatchedJobs

不要嘗試直接修改NativeContainer中的內容
        由於沒有辦法返回引用,因此不可能直接修改NativeContainer的內容,比如 nativeArray[0]++,或者var temp = nativeArray[0]; temp++,不會真正修改NativeArray的內容。

        相反,你必須根據索引拷貝數據到一個臨時的內存,修改這份拷貝,並重新保存回去,如下:
            MyStruct temp = myNativeArray[i];
            temp.memberVariable = 0;
            myNativeArray[i] = temp;

調用 JobHandle.Complete 來重新獲得操作權限
        主線程需要等待Job依賴執行完畢才能重新獲得線程使用權限,不能僅僅使用JobHandle.IsCompleted來判定,必須調用Jobhandle.Complete來重新獲取NativeContainer的操作權限,Complete方法的調用同樣會清除Safe System的狀態,不這麼做會導致內存泄漏。每一幀調度一個依賴於上一幀的job同樣會發生這種情況。

在主線程中使用 Schedule和Complete
        你只能在主線程中調用Schedule和Complete方法,如果一個Job依賴另一個Job,使用JobHandle來管理依賴,而不是在Job中調度Job

在正確的時機使用Schedule和Complete
        在你獲取到一個Job的數據之後,儘可能快的調用他的Schedule方法,並且在你只在你需要結果的時候再調用Complete方法,在不需要等待且和其他Job不存在衝突的時候儘早調度你的Job,是一個很好的習慣。例如:如果在一幀的結束和下一幀的開始之間有一段時間沒有Job在運行,並且可以接受有一幀延遲,那麼可以將Job在一幀的結束時調度,並在下一幀中使用它的結果。或者,如果你的遊戲將這段轉換時間用在了其他Job上,但是在當前幀的其他地方還有一段未被充分利用的時間,那麼在那裏調度你的工作將會更有效率。

把NativeArray類型設置爲只讀
        需要注意的是,Jobs默認擁有NativeContainer的讀寫權限,在合適的地方使用[ReadOnly]標籤來提升性能表現

檢查數據依賴
        在Unity的Profiler窗口,WaitForJobGroup標記表示主線程在等待一個工作線程的Job執行結束,這個標記表明你生成了一個你需要解決的數據依賴,通過JobHandle.Complete方法來跟蹤哪裏導致了你的主線程被強制等待依賴的數據。

Debuging Jobs
        Job有一個Run方法,可以在主線程中替代Schedule方法來立即執行job,可以用這個來調試job

不要在Job中分配託管內存
        在Job中分配託管內存會非常非常慢,而且Job系統不能使用BurstCompile來提升代碼性能,Burst是一個基於LLVM的編譯後臺,讓很多事情變得簡單,通過Jobs系統生成高度優化的機器編碼在各個平臺上獲得良好的性能表現。



        


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章