# 掌握 Unity JobSystem 體驗多執行續的神奇力量 (一)-JobSystem 基本介紹 # 內容規劃 JobSystem 是 Unity 提供的一個多執行緒方案,讓原本在主執行緒上運行的代碼能夠輕鬆、安全和高效地運行在 worker thread 上,並且盡可能平行運行。這有助於減少主執行緒完成遊戲邏輯運算所需要的總時間,提供顯著的效能提升。 本系列文章將會由三篇組成: * 掌握 Unity JobSystem 體驗多執行續的神奇力量 (一)-JobSystem 基本介紹 * 掌握 Unity JobSystem 體驗多執行續的神奇力量 (二)-以 JobSystem 實作鳥群模擬算法 * 掌握 Unity JobSystem 體驗多執行續的神奇力量 (三)-深入探討 JobSystem 本文章為系列的第一篇。 大綱 * [多執行緒並行運算](#多執行緒並行運算) * [常用的 Job 類型](#常用的-Job-類型) * [專有名詞簡介](#專有名詞簡介) # 多執行緒並行運算 在不考慮多執行緒的情況下,如果一個遊戲的 `Update()` 原先花費 24ms,我們的目標是將它降低到 16ms 好讓遊戲保有 60FPS 的表現,那麼我們只有兩個選擇:讓 CPU 運行得更快(通常不是好選擇),或者讓 CPU 做更少的工作。 ```csharp void Update() { // <lots of simulation logic...> } ``` ![](https://blog-api.unity.com/sites/default/files/2023-02/TftT_Job-System_Image-1.png?imwidth=1260&) *在主執行緒上執行 24ms 的 `Update()` 方法* 為了減少 8ms 的計算工作,通常可以通過改進演算法、將工作分散到多個幀、刪除冗於的計算等方式來嘗試達到目標,再不然就是透過削減遊戲內容和遊戲規格來降低計算的複雜性(這當然不理想)。 現在大多數的 CPU 都是多核心的,這表示如果我們能夠神奇而安全的將 `Update()` 方法中的工作分配給四個 CPU 核心,則原本需要 24ms 的 `Update()` 方法最後就只需要 6ms 即可完成工作! ```csharp void Update() { // Some magic has split our logic into 4 equal parts // that can run in parallel. Wowee! PartialUpdateA(); PartialUpdateB(); PartialUpdateC(); PartialUpdateD(); } ``` ![](https://blog-api.unity.com/sites/default/files/2023-02/TftT_Job-System_Image-2.png?imwidth=1260&) *`Update()` 被分成四個部分更新,每個更新都在自己的執行緒上運行* 別高興得太早,這都只是幻想,在沒有任何幫助的情況下,沒有什麼魔法可以將 `Update()` 方法分成幾部分並在單獨的核心上運行他們,儘管我們有 128 核心的 CPU,上面的 24ms `Update()` 方法仍然是需要 24ms 的執行時間。 如果沒有魔法可以做到這件事,那不是非常浪費多核心 CPU 的潛力嘛!沒關係,我們現在就要來說說 JobSystem 這個 Unity 提供的多執行緒方案如何達到我們的需求:smile:。 # 常用的 Job 類型 Job 指的是執行一項特定任務的工作單元,它可以接收參數並對資料進行操作,行為上其實就類似於調用一個方法。Job 可以獨立運行,也可以依賴於其他 Job 完成後才運行,在 Unity 中,Job 指的是任何實現 `IJob` 或類似介面的 struct,Job 的類型基本如下: * **IJob**: 最基礎的 job,在 worker thread 上運行一個單獨的任務。 一個 job 的長相大致如下: * 實作 `IJob` 介面的 struct。 * struct 內包含 blittable 類型與 NativeContainer 的成員變數。 * `IJob` 需要實作 `Excute()` 方法。 ```csharp struct MyJob : IJob { public int a; public int b; public NativeArray<int> result; public void Execute() { result[0] = a + b; } } ``` * **IJobFor**: 一個會在內部用 for 迴圈呼叫每個 `Excute(int index)` 的 job,在一個執行緒上獨立運行。 ```csharp struct MyJob : IJobFor { public int a; public int b; public NativeArray<int> result; public void Execute(int index) { result[index] = (a + b) * index; } } ``` * **IJobParallelFor**: 如同 `IJobFor` 一樣會在內部用 for 迴圈呼叫每個 `Excute(int index)`,不同的是 `IJobParallelFor` 能夠並行地運行。每個並行運行的 worker thread 都有一個獨占的 index 來安全地訪問他們之間的共享數據。 ```csharp struct MyJob : IJobParallelFor { public int a; public int b; public NativeArray<int> result; public void Execute(int index) { result[index] = (a + b) * index; } } ``` # 專有名詞簡介 在真正開始使用 JobSystem 撰寫程式碼之前,先稍微介紹一些專有名詞,讓大家先有個概念,後面的章節中會再挑一些內容作重點介紹。 * **Job**: 就像前面說的是一個執行一項特定任務的工作單元,在 worker thread 上運行。 * **Safety System** 為了避免發生 race condition 的錯誤狀況發生,job 內只能使用 blittable types 資料。 Job 內的資料預設都是可以讀寫的,我們可以對每個資料加上 `[ReadOnly]`/`[WriteOnly]` 的標籤。 * **NativeContainer** A thread-safe C# wrapper for native memory,它是一個能讓不同的執行緒之間共享的記憶體空間。我們能夠把主執行緒的資料透過 NativeContainer 提供給 job 進行讀寫。 * **NativeContainer Allocators** NativeContainer 的記憶體空間配置與釋放有三種 Allocator 類型,在建立 NativeContainer 時,必須要將它指定為其中一種: * Allocator.Temp:最快的配置類型,存在時間小於等於一幀。 * Allocator.TempJob:速度適中的配置類型,存在時間為四幀,需要在存續時間內主動呼叫 `Dispose()` 來釋放,否則會跳出警告。 * Allocator.Persistent:最慢的配置類型,可以永久存在。 * **Job Schedule** 在我們建立出一個 job 之後,需要呼叫 `Schedule()` 方法,系統即會將 job 分派給某個 worker thread 執行。 ```csharp MyJob _myJob = new MyJob {...}; _myJob.Schedule(); ``` * **Job Handle** `Schedule()` 方法會回傳一個 `JobHandle`,可以用它來判斷對應的 job 是否執行完畢,也可以用作另一個 job 呼叫 `Schedule()` 時的參數。 ```csharp MyJob _myJobA = new MyJob {...}; JobHandle _jobAHandle = _myJobA.Schedule(); ``` * **Job Dependencies** 從 `Schedule()` 方法取得 `JobHandle` 後,`JobHandle` 可以作為參數讓下一個 job 的 `Schedule()` 方法使用,表示下個 job 會等收到的 Handle 所對應的 job 執行完才開始執行。 ```csharp MyJob _myJobB = new MyJob {...}; JobHandle _jobBHandle = _myJobB.Schedule(_jobAHandle); //表示 _myJobA 執行完才開始執行 _myJobB ``` * **JobHandle Complete** 在主執行緒中呼叫 JobHandle 的 `Complete()` 方法,會讓主執行緒等到對應的 job 以及它相依的 job 都執行完畢後,才會繼續往下執行。如此便能讓我們在呼叫 `Complete()` 方法之後安全的使用 job 拿去讀寫的資料。 ```csharp _jobBHandle.Complete(); // 下面可以安全的使用 _myJobA 與 _myJobB 拿去讀寫的資料 ``` * **BurstCompile** 我們可以在 job 的 struct 加上 `[BurstCompile]` 標籤,能夠簡單無腦的優化 job 的執行效能。 ```csharp [BurstCompile] struct MyJob : IJob {...} ``` 完整的 job 範例如下: ```csharp public class MyJobExample : MonoBehaviour { //主執行緒與 job 間共享的資料 NativeArray<int> result; JobHandle handle; //一個簡單執行兩個 int 相加的 job struct MyJob : IJob { public int a; public int b; public NativeArray<int> result; public void Execute() { result[0] = a + b; } } void Start() { //建立一個持續存在的 NativeArray,長度為 1,執行緒之間可以共享 result = new NativeArray<int>(1, Allocator.Persistent); } void Update() { //建立一個 job,將所需要的資料與 NativeArray 塞進去 MyJob jobData = new MyJob { a = 10, b = 10, result = result }; //呼叫 job 的 Schedule() 取得 JobHandle handle = jobData.Schedule(); } private void LateUpdate() { //呼叫 JobHandle 的 Complete() 確保 job 執行完畢 handle.Complete(); //可以安全拿到 job 執行完的結果 int res = result[0]; Debug.Log(res); } } ```