ジョブからジョブのスケジュールができない理由
Unity には、設計を牽引する重要な原則がいくつかあります。
- 大前提は決定論: 決定論がネットワークゲームやリプレイツール、デバッグツールを実現します。
- 安全性: 競合状態が即座に報告されることで、より手軽かつシンプルにジョブ化されたコードを作成できるようになります。
これら 2 つの原則を適用することで、いくつかの選択肢や制限事項が生じます。
ジョブをメインスレッドでしか完了できない理由
JobHandle.Complete を呼び出すと、ジョブスケジューラーのデッドロックを解決できなくなります (ここ数年、Unity C++ コードベースでこの問題に何度も取り組んできましたが、いずれの試みも失敗し、コードを元に戻すしかありませんでした)。このデッドロックが生じるのは稀ですが、どのようなケースでも解決することは明らかに不可能であり、ジョブのタイミングに大きく依存します。
ジョブのスケジュールをメインスレッドでしか設定できない理由
シンプルにジョブのスケジュールを別のジョブから設定した一方で、そのジョブから JobHandle.Complete を呼び出さなかった場合は、決定論を保証できなくなります。メインスレッドは JobHandle.Complete() を呼び出す必要がありますが、何がその JobHandle をメインスレッドに渡すのでしょうか。他のジョブのスケジュールを設定するジョブがすでに実行されていることを、どうやって確認すればよいでしょうか。
つまり直観的には、ジョブのスケジュールを他のジョブから設定し、ジョブ内でジョブを待つ形を選びたくなるでしょう。しかし経験上、その形はどのようなケースでも問題が発生することがわかっています。そのため C# のジョブシステムは、この形をサポートしていません。
正確なサイズが事前にわかっていない場合のワークロードの処理方法
まず保守的にジョブのスケジュールを設定します。その後、ジョブの実行時に実際の処理対象の要素数が、スケジュール時に保守的に判断した要素数よりも大幅に少ないことがわかった場合には、早期終了して何もしない形でもまったく問題ありません。
実際、この方法で決定論的な実行が可能です。早期終了によって処理のバッチ全体がスキップされる場合があるとしても、パフォーマンス上の問題にはなりません。 また、内部でジョブスケジューラーのデッドロックが生じる恐れもありません。
この目的では、IJobParallelFor ではなく IJobParallelForBatch を使用する方法が非常に有効です。バッチ全体で早期終了することができるためです。
public interface IJobParallelForBatch
{
void Execute(int startIndex, int count);
}