前回は、単一の ByteArray オブジェクトを複数の Worker から共有する方法を紹介しました。 (ActionScript Woker 間での ByteArray オブジェクトの共有)
その中で触れたように、共有 ByteArray 機能を使うときは、複数の Worker が同時に ByteArray オブジェクトを更新しないように、注意を払う必要があります。
そのためには、
- アドミックなメソッドを利用する
- Worker 間で、共有リソースの使用に対する占有権のコーディネートを行う
といった手段を用います。このうち前者のアトミックなメソッドは前回の記事の後半に紹介しました。
占有権のコーディネートに関しては、Flash Player 11.5 から Mutex というクラスが追加されました。この Mutex が今回のテーマです。
Mutex の基本的な利用手順は以下のような流れになります。
- リソースを共有する Worker 間で Mutex のオブジェクトを共有する
- Mutex オブジェクトのオーナー権を取得する
- 共有リソースの操作を行う
- Mutex オブジェクトのオーナー権を放棄する
- 以下、ステップ 2 から繰り返し
Mutex オブジェクトのオーナー権は、一度に 1 つの Worker しか取得できないことが保障されています。そのため、全ての Worker が上の流れに従えば、共有リソースの操作も安心です。
しかし、もし、上の流れに従わない Worker が 1 つでもあれば、せっかく他の Worker が Mutex を使った制御に参加していても意味がありません。言い換えると、Mutex はあくまで道具であって、共有リソースへの正しいアクセス制御の実現は、開発者側の責任ということです。
Mutex オブジェクトの生成と共有
Mutex は特殊なクラスで、オブジェクトを Worker 間で共有しても、コピーが作られることがありません。 setSharedProperty() メソッドもしくは MessageChannel を使って Mutex オブジェクトを共有すると、自動的に Worker 同士が同じ Mutex オブジェクトを参照する状態がつくられます。
下は、Mutex オブジェクトを共有するサンプルです。 (setSharedProperty() メソッドの解説はActionScript Worker (Flash Player マルチスレッド) の基本的な使い方をご覧ください)
import flash.concurrent.Mutex; import flash.display.Sprite; import flash.system.Worker; import flash.system.WorkerDomain; public class mutexSample extends Sprite { private var myMutex:Mutex; public function mutexSample() { if (Worker.current.isPrimordial) // 親Worker { var w:Worker; w = WorkerDomain.current.createWorker(loaderInfo.bytes); // Mutexオブジェクトを生成して子Workerと共有 myMutex = new Mutex(); w.setSharedProperty("mutex", myMutex); w.start(); // 以後適当に必要な処理を記述 } else // ここから子Workerの記述 { // 共有されたMutexオブジェクトへの参照を取得 myMutex = Worker.current.getSharedProperty("mutex") as Mutex; // 以後適当に必要な処理を記述 } }
}
見ての通り、単純に Mutex オブジェクトを渡すだけです。ここまで準備できたら、あとは必要に応じて、共有リソースにアクセスするためのオーナー権を取得、開放します。
Mutex オブジェクトのオーナー権
Mutex クラスには、インスタンスのオーナー権を操作するために、3 つのメソッドが提供されています。
- public function lock():void
- public function tryLock():Boolean
- public function unlock():void
lock() メソッドは、Mutex オブジェクトのオーナー権を取得するためのメソッドです。他の Worker がオーナー権を保持している場合は、それが開放されて自分のものになるまで lock() メソッドは待ち続けます。
一旦、lock() が返ってきたら、Mutex オブジェクトのオーナー権を取得した状態になっています。前述のように、一度に 1 つの Worker のみがオーナーになれることが保障されています。
共有リソースへのアクセスが完了したら、忘れずにオーナー権を放棄します。そうしないと他の Worker がいつまで経っても共有リソースを利用できません。その際に使用するメソッドが unlock() です。
下は、lock() と unlock() を使う際の、基本的な流れを示したサンプルです。
// Mutexのオーナー権を取得する myMutex.lock(); doSomething("共有リソースを操作"); // 終了したらオーナー権を放棄 myMutex.unlock();
unlock() を呼んで一旦オーナー権を放棄したら、他の Worker が共有リソースを使用している可能性があります。再び lock() を実行するまで、共有リソースへのアクセスは控えるようにします。
(ちなみに、複数の Worker が Mutex のオーナー権を待っている場合、最も長く待っている Worker の優先度が高くなります。ただし、実際にスケジューリングを行うのは OS のため、Worker の実行順序は完全には保障されません)
Worker が Mutex オブジェクトのオーナー件を取得したいけれど、他の Woker が既に取得していたら待ちたくない、という場合もあるかもしれません。そんなときは tryLock() というメソッドが利用できます。
tryLock() は、Mutex オブジェクトが開放されている状態のときだけオーナー権を取得します。呼び出した結果、戻り値が true であればオーナー権が取得できた、false であれば他の Worker がオーナー件を既に取得していた、という意味です。
if (myMutex.tryLock()) // 戻り値がtrueならオーナー権を取得できている { // 共有リソースを使った処理を実行 ... // unlock()を呼ぶのは忘れずに myMutex.unlock(); } else // 他のWorkerが既にオーナー権を取得していた ...
ところで、Mutex オブジェクトのオーナー権を取得した Worker は、複数回 lock() や tryLock() を呼び出して何重にも鍵をかけることができます。
Mutex オブジェクトは何回 lock() が呼び出されたかを記録していて、同じ回数 unlock() が呼び出されるまで、オーナー権を開放しません。
myMutex.lock(); myMutex.lock(); // unlock()を2回呼ぶまでオーナー権が開放されない myMutex.unlock(); myMutex.unlock();
上の例では、2 回 lock() が呼ばれているため、unlock() も 2 回呼ばれるまで、他の Worker は待たされることになります。
より複雑なシナリオの場合
さて、ここで、共有する ByteArray オブジェクトが 2 つある場合を考えてみます。それぞれ、byteArray1、byteArray2 という名前だと仮定します。これらを 2 つの Worker (Worker1 と Worker2) が共有するものとします。
シナリオ 1:
- Worker1 は byteArray1 を使うために Mutex オブジェクトのオーナー権を取得している
- Worker2 は byteArray2 を使おうとして、Worker1 がオーナー権を放棄するのを待っている
これは Worker2 にとっては時間の無駄です。byteArray2 は自由に使える状態にあるからです。
この場合は、共有するリソースごとに Mutex オブジェクトを用意すると良さそうです。例えば、mutex1 と mutex2 という Mutex オブジェクトを共有しておいて、byteArray1 を使用するときは mutex1 のオーナー件が必要、byteArray2 を使用するときは mutex2 のオーナー件が必要、というルールを決めるという方法です。
// byteArray1使用のためmutex1のオーナー件を取得 mutex1.lock(); byteArray1[0] = 1; // 終了したらオーナー権を放棄 mutex1.unlock();
これで、byteArray2 を使いたい Worker2 は、mutex2 のオーナー権だけ取得すればよく、Worker1 を待つこともなくなります。
シナリオ 2:
- Worker1 は byteArray1 を使うために mutex1 のオーナー権を取得している
- Worker2 は byteArray2 を使うために mutex2 のオーナー権を取得している
- Worker1 は byteArray2 が必要になって mutex2.lock() を実行する
- Worker2 は byteArray1 が必要になって mutex1.lock() を実行する
これは困った状態です。ステップ 3 により Worker1 は Worker2 が mutex2 のオーナー権を放棄するまで待ち続けます。一方、ステップ 4 で mutex1.lock() を実行してしまった Worker2 は、Worker1 が mutex1 のオーナー権を放棄するまで固まったままです。
このように、お互いが相手をブロックしてしまう状態 (デッドロック) に陥る可能性は、リソースを共有すると、常についてまわります。
このシナリオでは、ステップの 3 と 4 で、2 つ目の Mutex のオーナー権の確保に lock() ではなく tryLock() を使うことで、デッドロックは回避されます。しかし、それだけではシナリオが前に進みません。どちらかが譲らない限り、競合関係はそのままです。
Worker 間のコーディネート
この記事の冒頭に、「Worker 間で、共有リソースの使用に対する占有権のコーディネートを行う」 と書きましたが、リソースの占有権だけコーディネートしても、解決できないシナリオがありそうです。
その場合、例えば、上のシナリオ 2 であれば、Worker1 の処理を優先させることにして、以下のようなルールが実現できれば先に進めそうです。
- Worker2 は Worker1 が mutex1 のオーナー権を取得していることを発見したら、一旦 mutex2 のオーナー権を放棄する
- Worker1 は Worker2 が mutex2 のオーナー権を取得していることを発見したら、Worker2 が mutex2 のオーナー権を放棄する間で待つ
- Worker1 は mutex1 と mutex2 が不要になったら Worker2 に伝える
つまり、各 Worker の処理状況をコーディネートするという考え方です。
ところが、Mutex には、このような 「ある条件が起きるまで待つ、ある条件が起きたら伝える」 という機能がありません。ということで、Flash Player 11.5 には、Mutex の他に、Worker 間の同期を取るための Condition というクラスが用意されています。
コメントする