問題は、Windowsの機能であるイベントをWaitForSingleObjectで待つときにメインスレッドが停止することにある。WaitForSingleObjectはあるイベントがSetされるまでスレッドを止めて待つAPI関数なのだが、イベントをSetするのに必要な関数を同じスレッドに置いていたため、永久にイベントがSetされることが無かった。
以前の回路でこの問題は起こらなかったのは、イベントをSetする動作の基点が装置からのハードウェア割り込みだったからだ。この場合、装置からの割り込みが起こると、割り込みチェック専用のスレッドにあるオブジェクトがそれを検出し、イベントをSetする。
これに対して新しい回路はUSB通信を使っているために装置からの割り込みを起こすことができない。その代わりに、ホストPCが定期的に装置に問い合わせ(ポーリング)をする必要がある。問題はポーリングを起こす方法である。これまでは横着をしてSetTimer(API関数)でタイマを作り、タイマのカウントがゼロになるごとにポーリング関数を読んでいた。ポーリングの結果、装置からのメッセージが検出されると、イベントがSetされてWaitForSingleObjectが解除される。
ところが、SetTimerで作るタイマーはWaitForSingleObjectと同じメインスレッド上で動作するため、メインスレッドがWaitForSingleObjectで停止するとタイマも一緒に停止してしまう。こうなると装置への問い合わせは行われず、装置がメッセージを出しても検出されないのでWaitForSingleObjectは解除されない。
概念的に書くと、
A:通信オブジェクト(メインスレッド)
B:装置からのメッセージ検出オブジェクト(スレッド)
C:装置上のCPU
として、あるべき動作は
- (タイマがBに定期的にCのフラグをチェックさせる)
- AがCPUに命令を出す
- AはWaitForSingleObjectで休眠する
- Cは命令を処理し、処理が終わったことを表すフラグを立てる
- Bがフラグの変化を検出する
- BがCの状態を読み取る
- BはイベントをSetし、WaitForSingleObjectを解除する(Aの休眠が終わる)
- AはBの状態を読み取り、処理を継続する。
ところが実際は、
- (タイマがBに定期的にCのフラグをチェックさせる)
- AがCPUに命令を出す
- AはWaitForSingleObjectで休眠する
- タイマもWaitForSingleObjectで休眠する(Cのフラグをチェックできない)
- Cは命令を処理し、処理が終わったことを表すフラグを立てる
- (Cのフラグをチェックできない)
- やがてWaitForSingleObjectがタイムアウトし、Aとタイマが休眠から覚める。
となっていた。
対処方法は大きく3通り。
- マルチメディアタイマtimeSetTimerを使う(別スレッドで動く)
- ポーリングをトリガする別のスレッドを新たに作る
- 全く別の仕組みを考える
(3)は労力が大きいので最後の手段として保留する。
まず(1)を試してみると、確かに動作するのだがマルチメディアタイマのオーバーヘッドが大きいためソフトウェア全体の処理が重くなってしまう。実用に支障が出そうなレベルの負荷の増加だたのでこのままでは採用できない。
次に(2)を試してみる。こちらも動作するのだが単にスレッド中の無限ループでポーリングを行うと負荷が大き過ぎる。そこでポーリングの間隔を空けるためにAPIのSleep関数を使った。これで負荷も減り、目的の機能も動作するようになった。
修正後の動作は次のようになる。
D:ここで導入した、ポーリングを駆動するオブジェクト(スレッド)
- (DはBは定期的にCのフラグをチェックしている)
- AがCPUに命令を出す
- AはWaitForSingleObjectで休眠する
- (DはBは定期的にCのフラグをチェックしている)
- Cは命令を処理し、処理が終わったことを表すフラグを立てる
- (定期チェックによって)BがCをフラグの変化を検出する
- BがCの状態を読み取る
- BはイベントをSetし、WaitForSingleObjectを解除する(Aの休眠が終わる)
- AはBの状態を読み取り、処理を継続する。
実を言うとSleep関数の動作を勘違いしていたたために、この方法に落ち着くまでに余計な時間を
食ってしまった。SetTimerと同じくめ院スレッドを休眠させる関数だと思い込んでいたのだが、実際にはSleepは個別のスレッドを休眠させる関数であった。