フューチャーホームコントローラーを支える技術・スレッド間メソッドパッシング
音声認識でいろいろな家電を操作できるようにするフューチャーホームコントローラーを支える技術を解説します。
フューチャーホームコントローラーのソースコードはそのうちGPLとかで開示したいのですが、今はまだ開示していないので、
フューチャーホームコントローラーのサブプロダクトとして、ソースを公開している、FHCコマンダーのソースコードをベースに説明します。
FHCコマンダーのソースコードはここにあります。
http://rti-giken.jp/fhc/help/ref/fhc_commander.html
http://rti-giken.jp/fhc/help/ref/src.fhc_commander.zip
(フューチャーホームコントローラーと毎回書くのは疲れるので、略してFHCとか書くことがあります。)
さて、今回は、フューチャーホームコントローラーで利用しているスレッド管理システムと、スレッド間メソッドパッシングとかと勝手に読んでいる方法について解説したいと思います。
たくさんのスレッド
フューチャーホームコントローラーには、たくさんのスレッドが存在します。
そいつらを良い感じに協調動作させる仕組みが必要です。
スレッド競合のバグは非常に取りづらいので、適切に管理していかなければいけません。
そこで今回使ったのは、C#のInvokeみたいな、スレッド間で、メソッドを実行しあう仕組みです。
ただ、どのスレッドにもメソッドを差し込めるようにすると、設計がめんどくさい割にはメリットがないので、
差し込めるのは、メインスレッドだけです。
各スレッドを含む各スレッドは、メインスレッドに対して、任意の関数(メソッド)を差し込むことができます。
MainWindow::m()->AsyncInvoke( [=](){ MainWindow::m()->ScriptManager.SpeakEnd(task.callback,task.text); } ); MainWindow::m()->SyncInvoke([=](){ auto xr = MainWindow::m()->Recognition.AddCommandRegexp( this->CreateCallback( key1,key2 ) , it->second); if(!xr) { ERRORLOG("音声認識項目" + it->first + ":" + it->second + "を登録できません" ); } });
AsyncInvokeは、メインスレッドで以下のメソッドを、非同期で実行します。なので、=でキャプチャしてます。
SyncInvokeは、メインスレッドで以下のメソッドを、同期で実行します。なので、&でキャプチャしてます。
なんか、windowsのメッセージぽいですね。
そうです。
windowsの SendMessage / PostMessage とやっていることは変わりません。
そして、windows版の実装は、まさに SendMessage / PostMessageで実装しています。
メッセージと何が違うの?
メッセージってメッセージを送って、相手がそのメッセージを見て実行するものをキメます。
ですが、この仕組は、送る側がこれを実行しろよ!!とメソッドを送りつけることができます。
結局、相手側にこれをやってくりーと処理をお願いするときは、処理の内容までわかっていないと頼めないことがあります。
そして、処理の内容は毎回細かく変わります。
それだったら、メッセージだけではなくて、やってほしい処理の内容まで送りつけられたほうが合理的です。
それに、処理をお願いする所と、実際実行する処理が遠くに離れ離れになってしまうと、可読性も落ちます。
すぐ近くに実行する処理があるというのが特徴です。
調停役のメインスレッド
FHCはたくさんのスレッドを実行しています。
たくさんの人がぎゃーぎゃー言い始めると収集がつかなくなるので、
マネージャというか調停役が必要になります。
メインスレッドはまさに調停役を行なっています。
書く処理の中で、自分が管理していないスレッドで、スレッドセーフなメソッド以外を呼び出したいときは、メインスレッドを経由して呼び出します。
メインスレッドを経由して呼び出すのは、このメソッドパッシングで簡単に作ることができます。
もちろん、超重い処理を行なってしまうと、メインスレッドが死んでしまうので、適度なものにしなくてはいけません。
//スレッドセーフなメソッドならばOK スレッドA ↓ ↓ これはOK ↓ スレッドB 「スレッドセーフ」なメソッド //自分が管理していないスレッドでスレッドセーフなメソッド以外は直接呼んではいけない スレッドA ↓ ↓ 禁止!! これは許さない ↓ スレッドB 「スレッドセーフでない」メソッド //自分が管理していないスレッドでスレッドセーフなメソッド以外は、メインスレッドを経由して呼び出す スレッドA ↓ ↓ メインスレッド ↓ ↓ スレッドB 「スレッドセーフでない」メソッド
FHCはサーバ型アプリのため、メインスレッドが多少フリーズしても、キューがつまるだけで、全体にはそれほど影響を与えません。
もし、ゲームとかのリアルタイム性が高いアプリの場合は、調停役スレッドを一つ作って、メインスレッドはUIに徹する必要があるかもしれません。
今回は、ゲームではないので、そこはあんまり気にしません。
スレッド実行アサートマクロ
//メインスレッドでしか動きません #define ASSERT_IS_MAIN_THREAD_RUNNING() (assert(XLDebugUtil::GetMainThreadID() == boost::this_thread::get_id())) //メインスレッド以外で動きます #define ASSERT___IS_WORKER_THREAD_RUNNING() (assert(XLDebugUtil::GetMainThreadID() != boost::this_thread::get_id())) //このスレッドでのみ動きます #define ASSERT___IS_WORKER_THREAD_RUNNING_OF(X) (assert(((X).get_id()) != boost::this_thread::get_id())) //どのスレッドでも動きます #define ASSERT___IS_THREADFREE()
FHCでは、ASSERTを多用しています。
契約にする設計が好きな人が作ったので当然といえば当然ですが、たくさん出てきます。
その中で、FHC独自のASSERTが、実行しているスレッドによるASSERTです。
つまり、
//このメソッドは mainスレッド実行するように!! //このスレッドは、メインスレッド以外で実行するようにとか //このスレッドは、どのスレッドでも実行出来ますよー
とかコメントを書く暇があったら、以下のように書くと、間違って実行しようとしたら、ASSERTで止めてくれます。
//このメソッドは mainスレッド実行するように!! ASSERT_IS_MAIN_THREAD_RUNNING(); //このスレッドは、メインスレッド以外で実行するようにとか ASSERT___IS_WORKER_THREAD_RUNNING(); //このスレッドは、どのスレッドでも実行出来ますよー ASSERT___IS_THREADFREE();
スレッド競合が起きる時って、開発時というよりもメンテ時だと思ってます。
開発時とかは、スレッドがどー動いているか、頭のなかにイメージとかがあったりしますが、
時間が経つと、それも忘れてしまって、つい、このスレッドでは呼べないメソッドを呼んでしまって、大惨事になるという感じです。
このマクロを嵌めておけば、間違って読んだらASSERTで落ちるだけですから、とても便利です。
続く・・・