見出し画像

Unity DOTS 入門 (4) - C# Job System

DOTSの構成要素の1つである「C# Job System」を解説します。

・Unity 2019.3.14.f1
・Jobs 0.2.10

1. C# Job System

C# Job System」は、並列処理を行うための機能です。実行の順番やタイミングを気にしせず、ジョブを実行するだけで、CPUコアをフル活用することができます。

特徴は、次のとおりです。

・コードを簡潔の書ける
・GCフリー
・安全
・高速

メインスレッド」で全ての処理を実行するには重い時、処理を複数に細かい処理に分割した「Job」を作成し、「Jobキュー」に追加(スケジュール)します。「ワーカースレッド」は、「Jobキュー」から「Job」を取り出して実行します。

画像1

この時、「C# Job System」は依存関係を管理しているため、Jobが適切な順序で実行されます。たとえば、JobBがJobAに依存している場合、JobAが完了するまでJobBが実行されないことが保証されます。

2 NativeContainer

NativeContainer」は、C#が提供しているマネージドなContainer(List、Dictionaryなど)とは異なり、アンマネージドなContainerになります。GCで管理されないので、自分でDispose()でメモリを開放する必要があります。

特徴は、次のとおりです。

・メモリ割当タイプ(Allocator)を自身で決める
・使用後はDispose()でメモリを開放する必要がある
・構造体のみ(クラスはNG)
・要素数を増やせない

「NativeContainer」の種類は、次のとおりです。

・NativeArray<Value> : 配列
・NativeSlice<Value> : NativeArrayから一部切り取る
・NativeList<Value> : List
・NativeHashMap<Key, Value> : Dictionary
・NativeMultiHashMap<Key, Value> : キー毎に複数の値を持つDictionary
・NativeQueue<Value> : 先入れ先出し(FIFO)の待ち行列

「C# Job System」では「NativeContainer」で、Jobとメインスレッド間のデータ共有が可能です。Jobとメインスレッドのデータの受け渡しに使います。

3. Jobs パッケージのインストール

Jobs パッケージのインストール手順は、次のとおりです。

(1) メニュー「Window → Package Manager」でPackage Managerを開く。
(2) ウィンドウ上部の「Advances → show preview packages」をチェック。
(3) 「Jobs」パッケージ(0.2.10)を検索してインストール。

4. 単独のジョブの実行

空のゲームオブジェクト「CreateJob」を作成し、スクリプトのStart()で、単独のJobを実行します。

(1) Hierarchyウィンドウの「+ → Create Empty」で空のゲームオブジェクトを作成し、名前に「CreateJob」を指定。
(2) 「CreateJob」にスクリプト「CreateJob」を追加し、以下のように編集。

using UnityEngine;
using Unity.Jobs;
using Unity.Collections;

public class CreateJob : MonoBehaviour
{
    // Jobの定義
    private struct MyJob : IJob
    {
        public float a;
        public NativeArray<float> result;

        public void Execute()
        {
            result[0] = a;
        }
    }
    
    void Start()
    {
        // NativeContainerの生成
        NativeArray<float> resultArray = new NativeArray<float>(1, Allocator.TempJob);

        // Jobの生成
        MyJob myJob = new MyJob
        {
            a = 5f,
            result = resultArray
        };

        // Jobの実行方法の指定
        JobHandle handle = myJob.Schedule();

        // ここで他のJobを実行

        // Jobの完了を待つ
        handle.Complete();

        // 結果出力
        Debug.Log("resultArray[0] = " + resultArray[0]);

        // NativeContainerの破棄
        resultArray.Dispose();
    }
}

◎ NativeContainerの生成
NativeContainerのひとつ、「NativeArray<float>」を生成します。第1引数が「要素数」、第2引数が「メモリ割当タイプ」になります。

「メモリ割り当てタイプ」は、次のとおりです。

Allocator.Temp : メモリの割当と開放が1フレーム以下で使用。
Allocator.TempJob : メモリの割当と開放が4フレーム以内で使用。
Allocator.Persistent : アプリの存続期間中、永続的な割当を行う。

今回は、「Allocator.Temp」を指定しています。

◎ Jobの定義
Jobを定義するには、はじめに、「IJob」を継承した構造体(struct)を準備します。次に、Jobの処理で使う変数をフィールドに準備します。最後に、Execute()でJobの処理を実装します。

Jobの処理では、.Net や Unity のAPIは基本的に使えません。主に「Unity.Mathematics」を使った算術演算を行います。

今回は、数値の代入のみ行っています。

◎ Jobの生成
「Job」は、「IJob」を継承した構造体(struct)として生成します。

「Job」のフィールドには、次の2つの型を利用できます。

・プリミティブ : int, float, bool等。
・NativeContainer : NativeArray、NativeSlice、NativeList等。

Jobとメインスレッド間でデータ共有できるのは、「NativeContainer」のみで、「プリミティブ型」は入力には使えますが、出力には使えません。

◎ Jobの実行方法の指定
Jobの実行方法の指定には、以下の3つが利用できます。

・Run() : ラムダ式をメインスレッドで実行。この場合、Jobの完了を待つ。
・Schedule() : ラムダ式を単一のJobとしてスケジュール。
・ScheduleParallel() : ラムダ式をチャンク単位で分割した複数のJobとしてスケジュール。

戻り値は、「JobHandle」です。スケジュールされたJobを操作するためのハンドルになります。

◎ JobHandleの操作
「JobHandle」のComplete()を呼ぶことで、Jobの完了を待ちます。

(3) 実行。

5. 複数のJobの実行

「myJob」を処理が完了した後に、「anotherJob」を実行するように、スケジューリングします。

(1) スクリプト「CreateJob」にJob「AnotherrJob」を追加します。

    // 別のジョブ
    private struct AnotherJob : IJob
    {
        public NativeArray<float> result;

        public void Execute()
        {
            result[0] = result[0] + 1;
        }
    }

(2) スクリプト「CreateJob」のStart()を以下のように変更します。

    void Start()
    {
        // NativeContainerの生成
        NativeArray<float> resultArray = new NativeArray<float>(1, Allocator.TempJob);

        // Jobの生成
        MyJob myJob = new MyJob
        {
            a = 5f,
            result = resultArray
        };

        // 別のJobの生成
        AnotherJob anotherJob = new AnotherJob
        {
            result = resultArray
        };

        // Jobのスケジューリング
        JobHandle handle = myJob.Schedule();
        JobHandle anotherHandle = anotherJob.Schedule(handle);

        // 別のJobの完了を待つ
        anotherHandle.Complete();

        // 結果出力
        Debug.Log("resultArray[0] = " + resultArray[0]);

        // NativeContainerの破棄
        resultArray.Dispose();
    }

◎ Jobのスケジューリング
「JobHandle」を別のジョブのSchedule()の引数として渡すことで、Jobが完了してから別のJobが実行されるようになります。

(3) 実行。
5の代入が完了してから1加算されていることが確認できます。

【おまけ】 Unity.Mathematics

今回は使っていませんが、Jobのラムダ式内の算術演算には、「Unity.Mathematics」を使います。

float1, float2, float3, float4half1, half2, half3, half4int1, int2, int3, int4math.absmath.minmath.maxmath.powmath.lerpmath.clampmath.saturatemath.selectmath.rcpmath.signmath.rsqrtmath.anymath.allmath.sincos


この記事が気に入ったらサポートをしてみませんか?