LoginSignup
8
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-13

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

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と親和性が高い…かもしれない

間違ってたら

コメントお願いします

コードのライセンス

パブドメ

8
5
2

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
  3. You can use dark theme
What you can do with signing up
8
5