Unity C# Job System介紹(二) 安全性系統和NativeContainer

C# Job System中的安全性系統

https://docs.unity3d.com/Manual/JobSystemSafetySystem.html​docs.unity3d.com

資源競爭

當我們編寫多線程代碼時,經常會有產生資源競爭的風險。資源競爭會在一項操作的輸出依賴於另一項它掌控之外的操作時發生。

資源競爭並不總是視爲一個bug,但它卻是不確定行爲發生的原因之一。當資源競爭確實引發了一個bug時,因爲是偶然發生的,因此很難找到問題發生的確切原因,你只能在偶然情況下才能重現這種問題。調試時問題可能就消失了,因爲斷點和日誌可能改變單個進程的執行時機。因此資源競爭成爲了編寫多線程代碼時最大的挑戰。

安全性系統

爲了讓用戶更容易地編寫多線程代碼,Unity中的C# Job System會檢測所有潛在的資源競爭,從而避免用戶遇到由此產生的bug

舉例來說:如果C# Job System需要在主線程中發送一個數據的引用給一個Job,Job在寫入對應數據的時候無法判斷主線程是否也在同時操作該數據。這種情況下就會導致資源競爭。

C# Job System通過給每一個需要操作數據的Job一份數據的拷貝而不是主線程中數據的引用來避免這個問題。拷貝和原本的數據獨立,從而排除了資源競爭。

C# Job System拷貝數據的方式表明了一個Job只能訪問可以位塊傳輸的數據類型(blitable data types)。這種數據類型在託管代碼和原生代碼之間進行傳遞的時候不需要類型轉換。

C# Job System可以使用memcpy來拷貝可位塊傳輸數據,並在Unity的託管部分和原生部分之間傳遞它們。它在安排job時使用memcpy將數據放入原生內存,並給予託管部分在job執行時訪問這份拷貝數據的接口。查閱更多的信息,查看Scheduling jobs

NativeContainer

https://docs.unity3d.com/Manual/JobSystemNativeContainer.html​docs.unity3d.com

安全性系統中拷貝數據的缺點是單個job的計算結果是與外部隔離的。爲了突破這個限制,我們需要把結果放在一種共享內存——NativeContainer中。

什麼是NativeContainer?

NativeContainer是一種託管的數據類型,爲原生內存提供一種相對安全的C#封裝。它包括一個指向非託管分配內存的指針。當和Unity C# Job System一起使用時,一個NativeContainer使得一個Job可以訪問和主線程共享的數據,而不是在一份拷貝數據上工作。

有什麼可用的NativeContainer類型?

Unity使用一個叫做NativeArray的NativeContainer。你還可以通過一個NativeSlice來操作一個NativeArray,從而獲得從某個特定位置開始確定長度的NativeArray子集。

注意:Entity Component System(ECS)包擴展了Unity.Collections命名空間,包括了其他類型的NativeArray:

  • NativeList - 一個可變長的NativeArray
  • NativeHashMap - 鍵值對
  • NativeMultiHashMap - 每個Key可以對應多個值
  • NativeQueue - 一個先進先出(FIFO)隊列

NativeContainer和其安全性系統

安全性系統是所有NativeContainer類型的組成部分。它會追蹤所有關於任何NaiveContainer的讀寫。

注意:所有關於NativeContainer類型的安全性檢查(包括下標邊界檢查,內存釋放檢查和資源競爭檢查),只在Unity Editor和Play Mode中生效。(譯者:即只在編輯器環境中進行檢查)

安全性系統是由DisposeSentinelAtomicSafetyHandle組成的。DisposeSentinel檢測內存泄漏同時在你沒有正確釋放內存的時候給你一個錯誤信息。但內存泄漏的錯誤只有在泄露發生很久之後纔會觸發。

使用AtomicSafetyHandle在代碼中進行NativeContainer所有權的轉移。舉例來說,如果兩個已經安排的jobs向同一個NativeArray寫入數據,安全性系統會拋出一個異常,帶有明確的錯誤信息關於爲什麼以及如何解決這個問題。安全性系統會在你安排一個違規的job後拋出一個異常。

在這種情況下,你可以在安排job的時候添加一個依賴。第一個job可以寫入到NativeContainer,一旦它執行完畢,下一個job可以安全地讀取和寫入同一個NativeContainer。讀取和寫入的限制同樣影響在訪問主線程中的數據時生效。安全性系統允許多個jobs並行的讀取同一份數據。

通常來說,當一個job有NativeContainer的訪問權限時,它同時擁有讀取和寫入的權限。這種配置會使性能變差。一個C# Job System不允許你在有job正在對一個NativeContainer進行讀寫的時候,安排另一個job對該NaiveContainer擁有寫入權限。

如果某個job不需要向某個NativeContainer寫入,可以將該NativeContainer加上[ReadOnly]屬性,像這樣

[ReadOnly]
public NativeArray<int> input;

在上面的例子中,你可以在其他jobs擁有該NativeContainer只讀權限的時候同時執行該job。

注意:這邊沒有針對從一個job中訪問靜態數據的保護。訪問靜態數據可以繞過所有的安全性系統並可能導致Unity奔潰。關於更多的信息,可以查看C# Job System建議和錯誤定位

NativeContainer分配器(Allocator)

當創建一個NativeContainer時,你必須指定你需要的內存分配類型。分配的類型由jobs運行的時間來決定。這種情況下你可以在每一種情況下使分配器達到可能的最好性能。

這裏對於NativeContainer的內存分配有三個分配器類型。當你初始化你的NativeContainer時你需要指定一個合適的分配器。

  • Allocator.Temp是最快的分配類型。它適用於分配一個生命週期只有一幀或更短時間的操作。你不應當把一個分配器爲Temp類型分配的NativeContainer傳遞給jobs使用。你同時需要在函數返回之前調用Dispose方法(例如MonoBehaviour.Update,或者其他從原生到託管代碼的調用)
  • Allocator.TempJob是相比於Temp是一個較慢的分配類型但它比Persistent要快。這是一個生命週期爲四幀的內存分配而且它是線程安全的。如果你在四幀之內沒有調用Dispose,控制檯會打印一個由原生代碼生成的警告信息。絕大部分小jobs使用這種類型的NativeContainer分配器。
  • Allocator.Persistent是最慢的分配類型但,它可以持續存在到你需要的時間,如果必要的話可以貫穿應用程序的整個生命週期。它是直接調用malloc的一個封裝。長時間的jos可以使用這種分配類型。當性能比較緊張的時候你不應當使用Persistent

使用示例:

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

注意:上例中的1表明了NativeArray的長度。在這個例子中,它只有一個數組元素(因爲它只在result中存儲了一塊數據)。

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