Flash Player 11.4 のベータ版公開 (関連記事) に伴い、ベータ用の ASDoc が更新されました。マルチスレッド関連のクラスの仕様も、具体的に記述されています。
(ご参考までに、ActionScript Worker の概要は以前の記事でも紹介しています)
とはいえ、いきなり API を眺めるよりは、具体的な使い方から始めた方が楽だと思いますので、今回は、ベータ版と一緒に提供されている Worker のサンプルから、HelloWold の内容を解説します。
読み進める前に、以下のファイルをダウンロードして、中に含まれる HelloWorld.zip を展開して下さい。
- サンプルファイル: flashplayer11-4_p1_ex_concurrency.zip
なお、今回のベータで利用できる ActionScript Worker は、デスクトップ環境での利用を前提としたもので、デバイス環境 (AIR) からは利用できません。
また、デバッグを含め、Worker の機能をフルにサポートする開発環境は、次バージョンの Flash Builder であるということが予告されています。次バージョンの Flash Builder は 8 月にベータ版が公開される予定だそうです。
始めに
ActionScript Worker では、最初に生成される Worker を primordial worker と呼びます。従来のシングルスレッドの環境は、primordial worker だけの状態だったと理解してよいようです。
それ以外の Worker は、primordial worker から、または更にその派生として、動的に生成します。生成された Worker は SWF ファイルを読み込み、それを実行します。
生成された Woker の内部では、使用できる API が制限されたり、一部動作の異なる API があります。つまり、従来の SWF がそのまま実行できるのは primordial worker のみです。
Worker 間では、属性を共有する、あるいは、通信用のチャネルを使用する、等の情報共有のための手段があります。
HelloWorld サンプルの基本構造
以下の説明は、説明を簡略化するため、元のサンプルコードから多少変更してあります。その点ご了承の上お読み下さい。
さて、まず、HelloWorld クラスは以下のような構造をしています。if 文を使って、自身が primordial かどうかを判断して、ロジックを振り分けています。
この記述方法を採ると、1 つのソースコードだけで、親と子の worker それぞれのロジックを記述できるので便利です。特に、開発環境が、子 Worker 用の SWF ファイル管理機能を提供しない現状では、効率的な方法だと思われます。
ただし、この様な記述をすると、Worker ごとに専用の SWF ファイルを用意するのと比べて、各 Worker に余分なリソースを読み込むことになります。なので、使う場合は選んだ方が良いかもしれません.
// コンストラクタ public function HelloWorld() { if (Worker.current.isPrimordial) { // ここに"primordial worker用"のロジックを記述 } else { // ここに"子worker用"のロジックを記述 } }
Worker.current は、スクリプトを実行している Worker のインスタンスを指します。Worker.isPrimordial が true なら、その Worker は最初に生成されたメイン Worker であることを意味します。
子 Worker の生成
Worker の生成には、WorkerDomain.current.createWorker() メソッドを使います。引数には SWF の格納された ByteArray を指定します。
今回のサンプルでは、子の Worker も、親と同じ SWF ファイルを実行します。そのため、loaderInfo からバイトデータを取得して、Worker を生成します。
private var worker:Worker = null; public function HelloWorld() { // 実行中のSWFファイルのバイトデータを取得する var myByteArray:ByteArray = new ByteArray(); myByteArray = this.loaderInfo.bytes; if (Worker.current.isPrimordial) { myWorker = WorkerDomain.current.createWorker(myByteArray); myWorker.start(); } else {} }
生成した Worker は start() メソッドを呼ぶまで実行されません。
Worker のインスタンスは、一旦実行されると、自身で System.exit() を呼ぶか、他の Worker から terminate() を呼ばれるか、例外が発生すると終了します。
さて、ここまでで 2 つの Worker を実行できるようになりました。次は、Worker 間での通信の実現です。
MessageChannel の作成
Worker 間の通信の主要な手段は MessageChannel です。Worker の createMessageChannel() メソッドでインスタンスを生成します。引数は通信先の Worker です。
MessageChannel は一方向の通信チャネルのため、双方向のコミュニケーションを行うには 2 つのインスタンスが必要になります。下のような感じです。
private var fromMain:MessageChannel; private var fromChild:MessageChannel; public function HelloWorld() { if (Worker.current.isPrimordial) { // 子Workerを生成後にメッセージチャネルを生成する ... // primordial workerから子workerへの送信チャネル fromMain = Worker.current.createMessageChannel(myWorker); // 子workerからprimordial workerへの送信チャネル fromChild = myWorker.createMessageChannel(Worker.current); ... } else {} }
メッセージを受信すると、MessageChannel のインスタンスに Event.CHANNEL_MESSAGE イベントが発生します。これを処理できるよう、受信用のチャネルにはイベントハンドラを追加します。
if (Worker.current.isPrimordial) { ... fromChild = myWorker.createMessageChannel(Worker.current); fromChild.addEventListener(Event.CHANNEL_MESSAGE, onChildMessage); ... } private function onChildMessage(evt:Event):void { if (evt.target.messageAvailable == true) { // 子Workerからのメッセージを受信する var message:String = evt.target.receive(); } }
イベントの target は MessageChannel のインスタンスです。その messageAvailable 属性の値が ture なら、receive() を使ってデータを取得できます。データの型は * です。
子 Worker 側でも同様の設定をします。当然、イベントハンドラを追加するコードは、子 Worker 内で実行されるように記述しなければなりません。
if (Worker.current.isPrimordial) { // 以下はprimordial worker内で実行される fromMain = Worker.current.createMessageChannel(myWorker); } else { // 以下は子Worker内で実行される fromMain.addEventListener(Event.CHANNEL_MESSAGE, onMainMessage); } private function onMainMessage(evt:Event):void { if (evt.target.messageAvailable == true) { // primordial workerからのメッセージを受信する var message:String = evt.target.receive(); } }
しかし、上のコードには問題があります。
それは MessageChannel の生成コードが primordial worker 内で実行されているという点です。そのため、primodial worker 内では、fromMain 変数に生成されたインスタンスへの参照が設定されています。これは当然ですね。
しかし、子 Worker では、fromMain に参照先を設定するコードが実行 (そもそも記述も) されていません。参照先が分からなければ、イベントハンドラも追加できません。
また、他の Worker の属性値を直接覗くことはできません。そのため、動的に生成されたリーソスを共有するには一手間が必要になります。
Worker 間でのオブジェクトの共有
Worker には、特定の Woker に対して、共有する属性を設定することができる setSharedProperty() メソッドがあります。最初の引数にキーとなる文字列、次の引数に任意のオブジェクトへの参照を渡します。
下の例では、primordial worker から、子 Worker に対して、2 つの MessageChannel への参照を共有しています。
if (Worker.current.isPrimordial) { // 以下はprimordial worker内で実行される myWorker.setSharedProperty("fromMain", fromMain); myWorker.setSharedProperty("fromChild", fromChild); } else { // 以下は子Worker内で実行される fromMain = Worker.current.getSharedProperty("fromMain"); fromChild = Worker.current.getSharedProperty("fromChild"); }
受け取る側の Worker は getSharedProperty() メソッドを使います。引数に適当なキーを指定すると、対応するオブジェクトへの参照が取得できます。
ところで、上の例では、子 Worker が自身への参照を取得するために、Worker.current を使用してる点にも注意して下さい。myWorker も、子 Worker 側では利用できません。
ここまでで、通信用のチャネルとイベントハンドラの設定が完了しました。あとは、実際にメッセージの送信を行うだけですが...
Worker のステータス
子 Worker に対して start() を呼んでも、直後に動作し始める保証はありません。そして、実際に動作が開始されるまで、primordial worker は子 Worker と連携処理を始めることができません。
このような要件に対応すべく、Worker の状態を通知できるよう、新しく Event.WORKER_STATE イベントが追加されました。
イベントハンドラの利用方法は、通常と同じです。ただし、ハンドラの実行される Worker が、ハンドラを追加した Worker (下の例では primordial worker) であることには注意が必要です。
if (Worker.current.isPrimordial) { myWorker = WorkerDomain.current.createWorker(myByteArray); myWorker.addEventListener(Event.WORKER_STATE, onWorkerEvent); ... myWorker.start(); } private function onWorkerEvent(evt:Event):void { if (evt.target.state == WorkerState.RUNNING) { // 子Workerが実行中、メッセージ送信も可能 fromMain.send("こちらはメインスレッドです"); } }
イベントが発行されるのは Worker の状態が変わった時です。通知される状態は以下の 3 種類です。
- NEW:新しく Woker が生成された
- RUNNNING:Worker の実行が開始された
- TERMINATED:terminate() により実行が中断された
これらの値は、イベントの target から参照される woker インスタンスの state 属性から取得します。
ステータスが RUNNING であれば、メッセージを送信できます。MessageChannel を使った送信には send() メソッドを使います。
あとは、それぞれの MessageChannel のイベントハンドラに適当な処理を記述します。
private function onMainMessage(evt:Event):void { if (evt.target.messageAvailable == true) { // primordial workerからのメッセージを受信する var message:String = evt.target.receive(); trace(message); // primordial workerにメッセージを送信 fromChild.send("こちらはサブスレッドです"); } } private function onChildMessage(evt:Event):void { if (evt.target.messageAvailable == true) { // 子Workerからのメッセージを受信する var message:String = evt.target.receive(); trace(message); } }
以上で、primordial worker から子 Worker に 「こちらはメインスレッドです」 を送り、その返事として子 Worker から primordial worker に 「こちらはサブスレッドです」 を送る処理が完成しました。
ちょっと長かったですね。
なんか前回のMAXで公開された機能が無い気がするのですが…
例えば、SWF を読み込む(FPの別インスタンスを作成する)以外にも
スレッドを作成する手段(API)があったと思うのですが?
ひょっとして、2011-11-06事件によってプロジェクトが縮小され
スレッド機能も簡素化されたのでしょうか?
それとも、今はセキュリティ的な課題があって
「FPの別インスタンスを作成する」だけになってしまったのでしょうか?
ActionScriptWorkerをFlex(Spark framework)で試したみたのですが、
正常ビルド、起動はしますが、マルチスレッドが機能していないのですが
Flexには未対応なのでしょうか?
Shigeru Nakagaki さん、こんにちは。
公開されていない製品戦略の話は私には分からないのです。すみません。
MarbayClip さん、こんにちは。
原理的には、primordial worker では Flex フレームワークは使えるように思います。
現状の Flex SDK のままで動作するなら、そのうちアドビから何らかの情報が公開されると思います。
もしかすると 8/2 日の FxUG あたりで関連する話が聞けるかも?
SDK の修正が必要な場合は、Apache からの発表を待つことになるのかもしれませんね。