---
# System prepended metadata

title: 掌握 Unity JobSystem 體驗多執行續的神奇力量 (一)－JobSystem 基本介紹

---

# 掌握 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);
    }
}
```