LoginSignup
5

More than 3 years have passed since last update.

posted at

updated at

Unity C# Job Systemに参照型を持ち込む

想定している読者ターゲット

C# Job Systemをある程度理解しており、参照型をJob内に持ち込めないことに悩んでいる人

概要

UnityのC# Job Systemには値型しか持ち込めない
しかし、参照型をどうしても持ち込みたい場面はある

そこでこの記事ではGCHandleを利用してJobに参照型を持ち込む方法を示す

豆知識

(他にもstatic関数を経由して渡す方法などがある
((https://www.slideshare.net/UnityTechnologiesJapan/cedec2018cpu-entity-component-systemecs の101ページ参照のこと))
が、static経由は個人的に使い勝手に難がある(同一Jobを違うパラメータで並列で動かす場合などで)と思ったのでGCHandleを使う)

注意点

以下の実装を使い参照型を持ち込むとBurstが使えなくなり
C# Job Systemで用意されている安全システムを失う
並列に処理することで起きる問題は自身で回避する必要がある

実装

GCHandleのためのPoolクラス

まず、GCHandleを使い回すためのPoolクラスを定義する
これを用意しないと、GCHandle.AllocでGC.Allocが発生してしまう

GCHandlePool.cs
    public static class GCHandlePool
    {
        // 使い回すためのスタックコンテナ
        protected static readonly Stack<GCHandle> stack = new Stack<GCHandle>();

        // GCHandleを生成する(プールされたオブジェクトがあるならそれを返す)
        public static GCHandle Create<T>(T value)
        {
            if (stack.Count == 0)
            {
                return GCHandle.Alloc(value);
            }
            else
            {
                var ret = stack.Pop();
                ret.Target = value; // Targetにセットする
                return ret;
            }
        }

        // GCHandleを開放する
        public static void Release(GCHandle value)
        {
            if (value.IsAllocated)
            {
                value.Target = null; // Targetを開放する
                stack.Push(value); // スタックコンテナに積んで次回に使い回す
            }
        }
    }

上記コードはマルチスレッドが考慮されていないが
C# Job Systemもメインスレッド以外でScheduleを呼ぶとエラーとなるので
これもメインスレッド以外からは呼ばれることはないと思い、lockやConcurrentStackなどを使っていない

GCHandleを使いやすくするためのユーティリティクラス

次に、GCHandleを少し使いやすくするためのユーティリティジェネリッククラスを定義する

GCHandle.cs

    public struct GCHandle<T> : System.IDisposable
    {
        GCHandle handle;

        // T型にキャストして返す
        public T Target
        {
            get
            {
                return (T)handle.Target;
            }

            set
            {
                handle.Target = value;
            }
        }

        // Pool経由で作成する
        public void Create(T value)
        {
            handle = GCHandlePool.Create(value);
        }

        // プール経由で開放する
        public void Dispose()
        {
            GCHandlePool.Release(handle);
            handle = default;
        }
    }

使用例

さて、実際にJob Systemで使ってみよう

Test.cs

    public static class Test
    {
        struct Job : IJob
        {
            // public string str2; // こちらを定義すると実行時にエラーとなる

            public GCHandle<string> str; // stringを指すGCHandleなら大丈夫

            void IJob.Execute()
            {
                Debug.Log(str.Target); // Job内部から参照型のstringにアクセスできてる!
             }
        }

        public static void Do()
        {
            Job job = new Job();
            job.str.Create("Test String"); // Job内で使う文字列を渡す

            var jobHandle = job.Schedule(); // Jobを実行する

            jobHandle.Complete(); // 今回は即座にJobの終了を待つ

            job.str.Dispose(); // Jobが終わったらちゃんと開放しないとまずい
        }
    }

これで無事Jobに参照型を持ち込むことに成功した

おわり

おまけ

これを利用してC# Parallelと似た簡単に使える並列関数群の一部をJob Systemで組む例を示す

Parallelクラス

Parallel.cas
    public static class Parallel
    {
        // 渡したActionを実行するためのJob
        struct Invoker : System.IDisposable
        {
            struct Job : IJob
            {
                public GCHandle<System.Action> func; // System.Actionを指すGCHandle

                void IJob.Execute()
                {
                    func.Target.Invoke(); // Job内部から参照型のSystem.Actionにアクセスできてる!
                }
            }

            Job job;

            public JobHandle Process(System.Action a)
            {
                job.func.Create(a); // job.funcにSystem.Actionを代入する
                return job.Schedule();
            }

            public void Dispose()
            {
                job.func.Dispose(); // ちゃんと開放する
            }
        }

        // aとbの関数を並列で実行する
        public static void Invoke(System.Action a, System.Action b)
        {
            using (var job1 = new Invoker())
            using (var job2 = new Invoker())
            {
                JobHandle.CombineDependencies(job1.Process(a), job2.Process(b)).Complete();
            }
        }

        // aとbとcの関数を並列で実行する
        public static void Invoke(System.Action a, System.Action b, System.Action c)
        {
            using (var job1 = new Invoker())
            using (var job2 = new Invoker())
            using (var job3 = new Invoker())
            {
                JobHandle.CombineDependencies(job1.Process(a), job2.Process(b), job3.Process(c)).Complete();
            }
        }

        // Parallel.Forと同じようなことをするためのジョブ
        struct JobFor : System.IDisposable
        {
            struct Job : IJobParallelFor
            {
                public GCHandle<System.Action<int>> func;
                public int start;

                void IJobParallelFor.Execute(int i)
                {
                    func.Target.Invoke(start + i);
                }
            }

            Job job;

            public JobHandle Process(System.Action<int> a, int start, int count, int batchCount)
            {
                job.func.Create(a);
                job.start = start;

                return job.Schedule(count, batchCount);
            }

            public void Dispose()
            {
                job.func.Dispose();
            }
        }

        // 初期値startからcount回funcを並列に呼び出す
        public static void For(int start, int count, int batchCount, System.Action<int> func)
        {
            using (var job1 = new JobFor())
            {
                job1.Process(func, start, count, batchCount).Complete();
            }
        }

        // Parallel.ForEachと同じようなことをするためのジョブ
        struct JobForEachArray<T> : System.IDisposable
        {
            struct Job : IJobParallelFor
            {
                public GCHandle<System.Action<T>> func;
                public int start;
                public GCHandle<T[]> list;

                void IJobParallelFor.Execute(int i)
                {
                    func.Target.Invoke(list.Target[start + i]);
                }
            }

            Job job;

            public JobHandle Process(T[] list, int start, int count, int batchCount, System.Action<T> func)
            {
                job.func.Create(func);
                job.list.Create(list);
                job.start = start;

                return job.Schedule(count, batchCount);
            }

            public void Dispose()
            {
                job.func.Dispose();
                job.list.Dispose();
            }
        }

        // listの要素ごとに並列にfuncを実行する
        public static void ForEach<T>(T[] list, int start, int count, int batchCount, System.Action<T> func)
        {
            using (var job1 = new JobForEachArray<T>())
            {
                job1.Process(list, start, count, batchCount, func).Complete();
            }
        }

    }

Parallelクラス使用例

Test.cs
    public static class Test
    {
        public static void Do()
        {
            // 渡した関数を並列で実行する
            Parallel.Invoke(()=>Debug.Log("A"), () => Debug.Log("B"), () => Debug.Log("C"));

            var list = new string[] { "pA", "pB", "pC" };

            // listの要素ごとに並列に実行する
            Parallel.ForEach(list, 0, list.Length, 1, x =>
            {
                Debug.Log(x);
            });
        }
    }

C# Parallelを使わずにこちらを使うメリット

・C# Parallelは使用するとGC.Allocが発生するがこちらは発生しない

・C#Job System上で動くのでProfilerに表示される、job Systemと競合しないなどUnityと親和性が高い…かもしれない

間違ってたら

コメントお願いします

コードのライセンス

パブドメ

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
5