関連記事
http://unitygems.com/threads/
November 2, 2012
デッドロックにハマらない
結局、変数が同時修正される問題の解決策は、一つの変数に対して一度に一つのスレッドしかアクセス出来ないようにすることだ。当然、複数のスレッドが同時に作業出来なくなることをも意味し、矛盾が生じるため、この制御を行なう時間は制限する必要がある。
C# には、特定のオブジェクトに対して特定の時間帯に、一つのスレッドしか存在し得ないようにする lock キーワードが用意されている。なお、値型やプリミティブは lock 出来ないため、あえてオブジェクトにと厳密に申し上げたので注意いただきたい。
int a = 50; object guard = new object(); void ThreadOneCode() { //コードを追加出来る lock(guard) { a = a + 1; } //コードを追加出来る } void ThreadTwoCode() { //ここにもコードを追加出来る lock(guard) { a = a - 1000; } //ここにもコードを追加出来る }lock(guard) {...} の中の全ては、guard を使用出来るスレッドは同時に一つのみアクセスできることを保証する。これはどのようなオブジェクトにも必要であればこれを使用出来る。
もし互いにネストしているものを lock 使用すると、たくさんの問題に出くわす可能性がある。例えば内部ロックを外すために、必要な何か外部ロックと同じものを必要とするために外せない、といった状況に陥る場合がある(すでにロックしているため外せない)。これはデッドロックと呼ばれ、外すことが不可能であり、結果的に全てを凍結させる。真剣にスレッドプログラミングに取り組むとどこかで遭遇してしまう問題だ。
もしスレッドの扱いは初心者ならば、出来るかぎりメモリや変数を共有しないように努めるべきだ。変数をスレッドに渡して、戻り値として変数を受け取るのは良いが、同じメモリを使用しないようにすべきだ。
スレッドを同期する方法はたくさんあるが、言語に作り込まれているのは Lock のみで、その他はシステム上のオブジェクトの構造により保守される。この方法で対応するのであれば、効率的なコードを書く手法はいくつかある。例えば値を読み込みは複数許容するけど、書き込みするのは一つのスレッドのみ書き込みできる所有者に限定する、などだ。
Unityでの実践的なスレッド活用
Unityでは自身のクラスインスタンスおよびUnityの値型(Vector3, Quaternion) のみ処理できる。前述のとおり、これによる制限は一部発生する。それでもメッシュの頂点編集を二つ目ののスレッドでおこない、Vector3の配列をメインスレッドに戻すなどして実際のオブジェクトを更新させることが出来る。
A* を使用した経路検索コードを記述してマップ上のある地点間の経路を書くことも出来る。
何をするにせよ、何らかの効果を発揮するためには、新しいスレッドからメインゲームのスレッドへと情報を渡す必要が発生する。
どうやってスレッド間で機能する優れたコードを記述すれば良いだろうか。現行の if や チェックを全て踏まえて、処理の準備が出来てるか示すために、クラスが大量のバッファやフラグで一杯々々にならないようにする必要がある。こういうゴチャゴチャが通常バグにつながり、スレッドを、本当の意味で効率的に使用するための障壁になるものだ。
クロージャおよび無名関数を使用することで、こよ作業をより簡単に出来るフレームワークを構築出来る。このチュートリアルの中でコード全体を記述することはないが、このリンク先にはあり、どのように使えば良いかはこれからお見せしたい。
スレッドの活用例
このためのクラスの名前は Loom と呼ばれる。Loomにより別のスレッド上でコードを容易に実行することが出来て、そのもう一つのスレッドも必要な時にメインのゲームスレッドでコードを実行することご出来る。
気にすべき関数はたった二つだ:
- RunAsync(Action) により、ステートメントのセットを別スレッドで実行する
- QueueOnMainThread(Action, [任意で指定] float time) により、ステートメントのセットをメインスレッドで実行する (任意で delay を指定)。
Loom をアクセスするには Loom.Current を使用する。これはゲームのメインスレッドと作用する透明なゲームオブジェクトを作成することを行なう。
Loom を使用して、全ての頂点を乗算処理してメッシュを更新する例を示す:
//二つ目のスレッドをメッシュ上でスケール void ScaleMesh(Mesh mesh, float scale) { //メッシュの頂点を取得 var vertices = mesh.vertices; //アクションを新しいスレッドで実行 Loom.RunAsync(()=>{ //頂点を一通りループ for(var i = 0; i < vertices.Length; i++) { //頂点をスケール vertices[i] = vertices[i] * scale; } //メインスレッドでコード実行して //メッシュを更新 Loom.QueueOnMainThread(()=>{ //頂点をセット mesh.vertices = vertices; //境界線を再計算 mesh.RecalculateBounds(); }); }); }これはクロージャの使用方法を学ぶのに好いサンプルでもある。
二つ目のスレッドで実行されるアクション (引数なし、戻り値なしのメソッド) はラムダ関数 (または無名関数) を用いて作成される。クロージャの素晴らしいところはクラスの全ての変数および自身の関数のローカル変数の両方にアクセス出来ることだ。
ラムダ関数の定義には ()=>{ ... } を使用して、この関数の中身は全て、別のスレッドで実行される。
頂点が更新されたとき、メインスレッドでメッシュを更新する必要があるためQueueOnMainThreadを使用し、これは次にUpdateサイクルが処理されたときに実行される(コールされるタイミングによって当該フレームまたは次のフレーム)。QueueOnMainThread もまたActionを引数とするため、全く同じテクニックを使用して更新された頂点を元のメッシュに渡す。この辺りが最高に便利なんだ!!
ソースコード
Loom の ソースコードはここでダウンロード出来る。同じことをUnityScript で実現するには次のように記述する:
//二つ目のスレッドでメッシュをスケール function ScaleMesh(mesh : Mesh, scale : float) { //メッシュの頂点を取得 var vertices = mesh.vertices; //アクションを新規のスレッドで実行 Loom.RunAsync(function() { //頂点を一通りループ for(var i = 0; i < vertices.Length; i++) { //頂点をスケール vertices[i] = vertices[i] * scale; } //メインスレッドでコードを実行 //してメッシュを更新 Loom.QueueOnMainThread(function() { //頂点をセット mesh.vertices = vertices; //境界線を再計算 mesh.RecalculateBounds(); }); }); }----
並行処理を行いたい、というケースでサンプルまでついているので有難いところだ。色々試すと良いだろう。
うーん、いつもながら中身が濃いぜ!
0 件のコメント:
コメントを投稿