#想定している読者ターゲット
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が発生してしまう
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を少し使いやすくするためのユーティリティジェネリッククラスを定義する
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で使ってみよう
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クラス
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クラス使用例
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と親和性が高い…かもしれない
間違ってたら
コメントお願いします
コードのライセンス
パブドメ