VC2008 MFCです。
ダイアログ内で、Windowsのファイル検索のように、
キャンセルボタンを押せる状態で検索処理を行うようにするため、
検索ルーチン自体をワーカスレッドとして分離し、そのスレッド内で、
見つかったものをリストコントロールに追加していくルーチンにしてみました。
そのために、AfxBeginThread()のLPVOIDの引数には、
ダイアログ自身のポインタ(this)を入れてあります。
キャンセルボタンが押されたときには、フラグを立てておき、
直後にワーカスレッドが終わるのをWaitForSingleObject(hThread, INFINITE)で
待つようにしたのですが、待ち状態になってから
スレッドのほうでリストコントロールに最後の追加が行われると
デッドロックになってしまいます。
InsertItem()はLVN_INSERTITEMをSendMessage()しているだけなので、
待ち状態に入っていてメッセージが処理されないためだろう
と思っているのですが、まずこの解釈は正しいでしょうか。
もし上の解釈が正しいとすると、この手の途中キャンセル付きの検索機能は
どのようにワーカスレッドを実装すればよいものなのでしょうか。
とりあえず、WaitForSingleObject(hThread, INFINITE)の部分を
while (WaitForSingleObject(hThread, 0) == WAIT_TIMEOUT) {
MSG msg;
if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
AfxGetThread()->PumpMessage();
}
}
などとして無理やりメッセージループを回しておけば回避はできたのですが、
このようなことをしないとデッドロックするという時点で、
ワーカスレッドの実装方法が間違っているのではないかと思っています。
よろしくお願いいたします。
ワーカースレッドからInsertItem()を行っているならその解釈でいいと思う。
正しい実装方法は知りません。
メッセージポンプの利用は個人的に好きじゃない。
ところでウィンドウ閉じるボタンとか押せてしまう?
PumpMessageが何をしているのか知らないので不安。
PeekMessageってメッセージが無かったときどうなるんだっけ?
ワーカースレッドからInsertItem()しなくなった後ここが実行され
描画もマウス動かすことも一切なかったら止まってしまう?
何もメッセージが来ないってことはないか。
正直面倒だよね。
安全のために排他制御するほど性能落ちたりデッドロックするミスの可能性高まるが
ほったらかしにしてたら原因究明が困難なバグになるし。
うーん、
ウインドウの描画程度ならthisポインター渡しでいいと思いますが、
ウインドウの移動・サイズ変更・アクティブ等ウインドウハンドルに関わる
操作の場合、ユーザーがウインドウの移動・サイズ変更等の操作を行なったときに
例外が発生して異常終了する可能性が高いです。
今回のメッセージ「SendMessage()」もウィンドウハンドルを使うので例外が発生する
可能性が高いですね。
画面の描画はウインドウハンドルの参照、すなわちthisポインター参照で大丈夫
みたいです。
ウインドウメッセージでなく、イベントを使ってみたらどうですか?
ワーカースレッド側は処理を終了する際にメッセージをポストするようにして、
ダイアログ側はワーカスレッドの終了をメッセージで判断するようにしましょう。
リストコントールをワーカースレッドが直接アクセスするのが嫌ですね
CTypedPtrListかなにかで処理結果をためるキューを作成し
ワーカースレッドから排他制御をしながらキューに書きこんだのち
ワーカースレッドからPostMessageを受けて
ダイアログはキューから排他制御ながらデータを取り出し
リストコンロトールに書きこむというように私なら作成します。
基本的にMFCのWindow周りのインスタンスは
スレッドを跨いで引き渡しては駄目ですと言うのが
Microsoftの公式見解のはずです。
なので、動いていたとしてもそれは保証された動作ではない
と言う事になると思います。
基本的に画面の描画処理はメインスレッドに任せて
ワーカースレッドは検索結果をメインスレッドに
引き渡す為の仕組みを作成して、
引き渡されたメインスレッド側でコントロールの制御を
行なうのがMicrosoftが保証している実装になると思います。
追記
HWNDで引き渡せば、ワーカースレッド側でコントロールに対して
メッセージを送信する事も可能ですけれど、ワーカースレッドは
基本的に検索処理だけに専念するようにしてウインドウ周りの
制御はメインスレッドに任せてしまう方が個人的にはすっきりすると
思います。ワーカースレッドからHWNDをつかってコントロールを
制御する事自体はできると思いますけれど、
ワーカースレッドの中にメッセージループを作成しないと
うまく行かない実装は本末転倒な気がします。
そもそも、GUIの更新とユーザーオペレーションが
検索処理によって止まってしまうのを防ぐ為にワーカースレッドに
したのだと思いますから。
個人的には、スレッドから直接リストコントロールの更新をさせるのは、
かなりやばい実装であると考えます。スレッドをまたぐSendMessage()は
1.まず、送信側が待たされる
2.HWNDを所有するスレッドに切り替える
3.HWNDのコールバックに処理させる
4.HWNDが処理したら、送信側スレッドに切り替える
5.送信側の「待ち」を解除する
の順番になりますが、2又は3の時点でHWNDが有効であることを保障している
わけではないと、自分は解釈しています。
この解釈によると、何らかの理由でHWNDが無効になったり、HWNDを所有する
スレッドが実行不能になれば、容易に送信側はSendMessage()から復帰することが
できなくなります。
やはり、スレッドは有効なことが保障されているバッファに結果を保管する
までの役目としておき、それをUI上で表現するのは別のルーチンが行うべき
ではないでしょうか。
初期の提示コードはワーカースレッドの中にあるんだっけ?
俺は
・ワーカースレッドを中断させたときに
・その中断=ワーカースレッドの終了を UI スレッドが待っている
・この作り方だとデッドロックすることがある
というふうに読んだが。
ワーカースレッドの終了なり完了なりを UI スレッドが待つのは本末転倒。
なので UI 側がワーカーの状況を知りたいのであれば
・ワーカースレッドが UI スレッドにメッセージを送るように修正するとか。
・ワーカースレッドは何も考えずにただ終了するようにしておき
UI スレッドがタイマーなどで随時状況を見に行くとか。
やりかたは何通りもあるのでお好きな方法で。
すいません、メッセージループがワーカースレッドの方に
あると勘違いしてました。
ただ、どちらにしても実装方法には問題ありですね。
tetrapodさんが書いておられるようにメインスレッド側は
ワーカースレッドを起動したらその場で待つのではなくて
そのままその関数を終了させる。
ワーカースレッドの終了を知りたいのであれば、
ワーカースレッドにメインスレッドのウインドウハンドルを
渡しておいて終了する時にユーザー定義メッセージをPostして
終了する。
メインスレッド側はPostされたメッセージの受信で
ワーカースレッドの終了を感知するとした方が簡単かも。
あと、いずれにしてもワーカースレッド自身が直接UIを
制御するのは止めた方が私はすっきりすると思います。
ワーカースレッドはあくまでも検索処理だけに専念して
UIの制御に関しては手を出さない方が良いと思います。
あさん、ITOさん、maruさん、おんどりさん、PATIOさん、
仲澤@失業者さん、tetrapodさん、ご意見ありがとうございます。
現在、キャンセルがあったときにもタイマを使って
ワーカスレッドの終了を見張るようにソースを修正してみているところです。
ちなみに、以下の件についてお伺いしたいのですが、よろしいでしょうか。
> ワーカースレッドはあくまでも検索処理だけに専念して
> UIの制御に関しては手を出さない方が良いと思います。
たしかによく考えると、この方法だと
メインスレッドで作成されたリストビューをワーカスレッドから制御していますね。
ただ、以下の件と絡んでくるのですが、
> CTypedPtrListかなにかで処理結果をためるキューを作成し
> ワーカースレッドから排他制御をしながらキューに書きこんだのち
> ワーカースレッドからPostMessageを受けて
> ダイアログはキューから排他制御ながらデータを取り出し
> リストコンロトールに書きこむというように私なら作成します。
この場合、処理結果(リストに追加するべき情報)を表す構造体やクラスは、
ワーカスレッドがnewで作成し、メインスレッドのダイアログが使い終わったら
ダイアログ側でdeleteで削除、という流れになるのでしょうか?
SendでなくPostで処理結果があることを通知するとなると、
そのオブジェクトを誰が作って誰がいつ破棄するのかという疑問があったため、
このへんのお決まりの手法があれば教えていただけると嬉しいです。
>この場合、処理結果(リストに追加するべき情報)を表す構造体やクラスは、
>ワーカスレッドがnewで作成し、メインスレッドのダイアログが使い終わったら
>ダイアログ側でdeleteで削除、という流れになるのでしょうか?
違うと思います。スレッド/DLG双方がなくても「処理結果」は存在しなくては
なりません。これにより、スレッド/DLGのどちらか、又は双方、しかも順不同
に失われても大丈夫です。
やり方次第と言う部分も有りますけれど。
キューその物はプロセスが開始する時点で作成すると思います。
排他処理も必要になると思います。
ポインタリストで作成するならワーカースレッド側で
作成して、メインスレッド側で取り出して削除と言うのも
有りでしょう。
検索スレッドと表示スレッドで同期を取るようにするのであれば、
受け渡し領域を一つにしておいてワーカースレッド側で置いたら
メインスレッド側で取り出すまで待つと言う手もあると思います。
この場合、メインスレッドがさっさと取り出してくれないと
検索側が止まってしまいますけれど。
この辺は仕組みの組み方次第ではないかなと思います。
理想からするとメインスレッドは置かれた物を取り出して
どんどん処理する。取り出したらキューから外される。
ワーカースレッドはキューにどんどん追加する。
ワーカースレッドは追加する時にキューが空だったら
追加した後、メインスレッドにデータを置いたメッセージを
送信する。
最後に検索が終わったらメインスレッドに通知する。
とか。
かなり殴り書きなので不整合があるかもしれませんけれど。
補足
> ポインタリストで作成するならワーカースレッド側で
> 作成して、メインスレッド側で取り出して削除と言うのも
> 有りでしょう。
これはポインタリストに入れるレコード情報の事を
言っています。リストその物は作成されている必要が
ありますが、中に入れる物はその都度作成すると思うので。
仲澤@失業者さん、PATIOさん、ご意見ありがとうございます。
数値情報以外のものをSendではなくPostでやりとりしようとすると
とたんに情報の管理が難しくなりますね。
今回の件そのものについては解決マークを付けさせていただきますが、
最後にもう一つよろしいでしょうか。
ワーカスレッドが終了したことをダイアログ側が知りたい場合、
CWinThreadのm_bAutoDeleteをFALSEにした状態でワーカスレッドを開始し、
そのCWinThreadポインタを使って、GetExitCodeThread()で
STILL_ACTIVEかどうかを調べることになるのでしょうか?
現在のところは、ワーカスレッドを終了する直前でフラグを立てておいて
ダイアログ側のOnTimer()の中でそのフラグをチェックすれば、
CWinThread自体は自動削除のままにできてよいかなと思って
そのような仕組みにしてみました(AfxBeginThread()の引数は2つのまま)。
何度も動作テストしてみたところ、デッドロックは解決していそうなのですが、
ワーカスレッド自身がフラグを立てた直後はまだ終了していない状態ですから、
スレッド切り替えのタイミングによっては、終了していない状態で
ダイアログ側が終了したと判断してしまうのかなと疑問に思っています。
これは、ダイアログがOnTimer()でチェックする方法だけでなく、
もう一つmaruさんやtetrapodさんやPATIOさんに提示していただいた
PostMessage()でダイアログに終了を通知する方法でも
同じ問題になるのかなと思っています(Postした直後はまだ生きていますよね?)。
終了するまさに一歩手前だから、万一の絶妙なタイミングでダイアログ側が先に
次の処理に移ってしまっても大した問題にはならないのかなとは思いますが、
どうせならこの機会に学んでおきたいと思い、追加で質問させていただきます。
よろしくお願いします。