アセットストアにリリースしている Fast Shadow Receiver というアセットでは、自分でスレッドプールを作成してマルチスレッドで動かしていた(Unityでマルチスレッドプログラム参照)のですが、Unityでもジョブシステムが導入されたので、遅れ馳せながらジョブを使うように変更しました。Unityがもつスレッドプールと自前のスレッドプールが両方存在するとスレッドの奪い合いが起きて効率が悪くなっちゃうので。
ただ、Unity のジョブシステムにはひとつ困ったことがあって、参照型を渡すことができないのです。たぶん、Unity のジョブキューはネイティブコードで実装されていて、マネージドのポインタをジョブキューに渡してしまうと、ジョブが実行される前にガーベッジコレクションなどでポインタの位置が変わってしまう可能性があって、とてつもなく危険だからでしょう。
ジョブシステムの典型的な使い方としては、NativeArray
なんとかならないものかと探してみたら、GCHandleという便利なものがありました。この構造体はマネージドのポインタをネイティブコードに安全に渡すためのものなので、まさに望み通りの代物です。
使い方も簡単で、こんな風に使えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System.Runtime.InteropServices; using Unity.Jobs; public struct Job : IJob { private GCHandle m_jobHandle; public Job(System.Action jobFunction) { m_jobHandle = GCHandle.Alloc(jobFunction, GCHandleType.Normal); } public void Execute() { System.Action jobFunction = (System.Action)m_jobHandle.Target; m_jobHandle.Free(); jobFunction(); } } |
GCHandle.Alloc
で GCHandle を作成して、m_jobHandle.Target
で参照型を取り出します。m_jobHandle.Free()
を忘れるとメモリリークするのでご注意ください。
ジョブを呼び出すときは、
1 |
JobHandle jobHandle = new Job(jobFunction).Schedule(); |
みたいにします。これで jobFunction
がジョブスレッドで実行されるんですが、ジョブと同期を取るために、JobHandle を保持しておく必要があります。
でも、単発でジョブを実行したいときなんかに、いちいち JobHandle を保持しておくのも面倒なので、次のようなユーティリティークラスを作ることにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
public static class JobUtils { private struct Job : IJob { private GCHandle m_jobHandle; public Job(System.Action jobFunction) { m_jobHandle = GCHandle.Alloc(jobFunction, GCHandleType.Normal); } public void Execute() { System.Action jobFunction = (System.Action)m_jobHandle.Target; m_jobHandle.Free(); jobFunction(); } } private static Dictionary<object, Dictionary<int, JobHandle>> s_jobHandles = new Dictionary<object, Dictionary<int, JobHandle>>(); public static bool StartJob(object issuer, int jobId, System.Action jobFunction) { Dictionary<int, JobHandle> jobHandles; if (s_jobHandles.TryGetValue(issuer, out jobHandles)) { if (jobHandles.ContainsKey(jobId)) { Debug.Log("The job has already started. JobId: " + jobId + ", Issuer: " + issuer.ToString()); return false; } } else { jobHandles = new Dictionary<int, JobHandle>(); s_jobHandles.Add(issuer, jobHandles); } jobHandles.Add(jobId, new Job(jobFunction).Schedule()); return true; } public static void WaitForJobComplete(object issuer, int jobId) { Dictionary<int, JobHandle> jobHandles; if (s_jobHandles.TryGetValue(issuer, out jobHandles)) { JobHandle jobHandle; if (jobHandles.TryGetValue(jobId, out jobHandle)) { jobHandle.Complete(); jobHandles.Remove(jobId); if (jobHandles.Count == 0) { s_jobHandles.Remove(issuer); } } } } public static bool IsJobCompleted(object issuer, int jobId) { Dictionary<int, JobHandle> jobHandles; if (s_jobHandles.TryGetValue(issuer, out jobHandles)) { JobHandle jobHandle; if (jobHandles.TryGetValue(jobId, out jobHandle)) { if (jobHandle.IsCompleted) { jobHandles.Remove(jobId); if (jobHandles.Count == 0) { s_jobHandles.Remove(issuer); } return true; } return false; } } return true; } } |
JobHandle をユーティリティークラスの Dictionary に保存しておいて、いちいち使う側が JobHandle を保持しなくてすむようになっています。GCHandle の使い方ということで紹介しましたが、System.Actionデリゲートに使いたい参照型のデータをバインドしておけば、これだけで好き放題できるので、なかなか便利です。注意点としては、デリゲートを毎回作成してしまうと GCAlloc が発生するので、System.Action 型の変数に保持してから使った方がいいのと、ジョブスレッドで参照型が使えるとは言っても、その参照型がスレッドセーフになっているかは別問題なので、十分注意が必要ということです。