ワーカスレッド内の実装方法

[戻る]
亀山 2010/02/19(金) 11:05:02
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();
    }
}

などとして無理やりメッセージループを回しておけば回避はできたのですが、
このようなことをしないとデッドロックするという時点で、
ワーカスレッドの実装方法が間違っているのではないかと思っています。

よろしくお願いいたします。

2010/02/19(金) 11:59:10
ワーカースレッドからInsertItem()を行っているならその解釈でいいと思う。

正しい実装方法は知りません。

メッセージポンプの利用は個人的に好きじゃない。
ところでウィンドウ閉じるボタンとか押せてしまう?
PumpMessageが何をしているのか知らないので不安。
PeekMessageってメッセージが無かったときどうなるんだっけ?
ワーカースレッドからInsertItem()しなくなった後ここが実行され
描画もマウス動かすことも一切なかったら止まってしまう?
何もメッセージが来ないってことはないか。

正直面倒だよね。
安全のために排他制御するほど性能落ちたりデッドロックするミスの可能性高まるが
ほったらかしにしてたら原因究明が困難なバグになるし。

ITO 2010/02/19(金) 13:46:16
うーん、
 ウインドウの描画程度ならthisポインター渡しでいいと思いますが、
ウインドウの移動・サイズ変更・アクティブ等ウインドウハンドルに関わる
操作の場合、ユーザーがウインドウの移動・サイズ変更等の操作を行なったときに
例外が発生して異常終了する可能性が高いです。
 今回のメッセージ「SendMessage()」もウィンドウハンドルを使うので例外が発生する
可能性が高いですね。
 画面の描画はウインドウハンドルの参照、すなわちthisポインター参照で大丈夫
みたいです。
 ウインドウメッセージでなく、イベントを使ってみたらどうですか?

maru 2010/02/19(金) 16:47:12
ワーカースレッド側は処理を終了する際にメッセージをポストするようにして、
ダイアログ側はワーカスレッドの終了をメッセージで判断するようにしましょう。

おんどり 2010/02/20(土) 07:07:49
リストコントールをワーカースレッドが直接アクセスするのが嫌ですね
CTypedPtrListかなにかで処理結果をためるキューを作成し
ワーカースレッドから排他制御をしながらキューに書きこんだのち
ワーカースレッドからPostMessageを受けて
ダイアログはキューから排他制御ながらデータを取り出し
リストコンロトールに書きこむというように私なら作成します。

PATIO 2010/02/22(月) 09:55:32
基本的にMFCのWindow周りのインスタンスは
スレッドを跨いで引き渡しては駄目ですと言うのが
Microsoftの公式見解のはずです。
なので、動いていたとしてもそれは保証された動作ではない
と言う事になると思います。

基本的に画面の描画処理はメインスレッドに任せて
ワーカースレッドは検索結果をメインスレッドに
引き渡す為の仕組みを作成して、
引き渡されたメインスレッド側でコントロールの制御を
行なうのがMicrosoftが保証している実装になると思います。

PATIO 2010/02/22(月) 10:03:13
追記

HWNDで引き渡せば、ワーカースレッド側でコントロールに対して
メッセージを送信する事も可能ですけれど、ワーカースレッドは
基本的に検索処理だけに専念するようにしてウインドウ周りの
制御はメインスレッドに任せてしまう方が個人的にはすっきりすると
思います。ワーカースレッドからHWNDをつかってコントロールを
制御する事自体はできると思いますけれど、
ワーカースレッドの中にメッセージループを作成しないと
うまく行かない実装は本末転倒な気がします。
そもそも、GUIの更新とユーザーオペレーションが
検索処理によって止まってしまうのを防ぐ為にワーカースレッドに
したのだと思いますから。

仲澤@失業者 2010/02/22(月) 10:28:09
個人的には、スレッドから直接リストコントロールの更新をさせるのは、
かなりやばい実装であると考えます。スレッドをまたぐSendMessage()は
 1.まず、送信側が待たされる
 2.HWNDを所有するスレッドに切り替える
 3.HWNDのコールバックに処理させる
 4.HWNDが処理したら、送信側スレッドに切り替える
 5.送信側の「待ち」を解除する
の順番になりますが、2又は3の時点でHWNDが有効であることを保障している
わけではないと、自分は解釈しています。
この解釈によると、何らかの理由でHWNDが無効になったり、HWNDを所有する
スレッドが実行不能になれば、容易に送信側はSendMessage()から復帰することが
できなくなります。

やはり、スレッドは有効なことが保障されているバッファに結果を保管する
までの役目としておき、それをUI上で表現するのは別のルーチンが行うべき
ではないでしょうか。

tetrapod 2010/02/22(月) 10:29:44
初期の提示コードはワーカースレッドの中にあるんだっけ?
俺は
・ワーカースレッドを中断させたときに
・その中断=ワーカースレッドの終了を UI スレッドが待っている
・この作り方だとデッドロックすることがある
というふうに読んだが。

ワーカースレッドの終了なり完了なりを UI スレッドが待つのは本末転倒。
なので UI 側がワーカーの状況を知りたいのであれば
・ワーカースレッドが UI スレッドにメッセージを送るように修正するとか。
・ワーカースレッドは何も考えずにただ終了するようにしておき
 UI スレッドがタイマーなどで随時状況を見に行くとか。
やりかたは何通りもあるのでお好きな方法で。

PATIO 2010/02/22(月) 10:42:37
すいません、メッセージループがワーカースレッドの方に
あると勘違いしてました。

ただ、どちらにしても実装方法には問題ありですね。
tetrapodさんが書いておられるようにメインスレッド側は
ワーカースレッドを起動したらその場で待つのではなくて
そのままその関数を終了させる。
ワーカースレッドの終了を知りたいのであれば、
ワーカースレッドにメインスレッドのウインドウハンドルを
渡しておいて終了する時にユーザー定義メッセージをPostして
終了する。
メインスレッド側はPostされたメッセージの受信で
ワーカースレッドの終了を感知するとした方が簡単かも。

あと、いずれにしてもワーカースレッド自身が直接UIを
制御するのは止めた方が私はすっきりすると思います。
ワーカースレッドはあくまでも検索処理だけに専念して
UIの制御に関しては手を出さない方が良いと思います。

亀山 2010/02/22(月) 11:58:32
あさん、ITOさん、maruさん、おんどりさん、PATIOさん、
仲澤@失業者さん、tetrapodさん、ご意見ありがとうございます。

現在、キャンセルがあったときにもタイマを使って
ワーカスレッドの終了を見張るようにソースを修正してみているところです。

ちなみに、以下の件についてお伺いしたいのですが、よろしいでしょうか。

> ワーカースレッドはあくまでも検索処理だけに専念して
> UIの制御に関しては手を出さない方が良いと思います。

たしかによく考えると、この方法だと
メインスレッドで作成されたリストビューをワーカスレッドから制御していますね。
ただ、以下の件と絡んでくるのですが、

> CTypedPtrListかなにかで処理結果をためるキューを作成し
> ワーカースレッドから排他制御をしながらキューに書きこんだのち
> ワーカースレッドからPostMessageを受けて
> ダイアログはキューから排他制御ながらデータを取り出し
> リストコンロトールに書きこむというように私なら作成します。

この場合、処理結果(リストに追加するべき情報)を表す構造体やクラスは、
ワーカスレッドがnewで作成し、メインスレッドのダイアログが使い終わったら
ダイアログ側でdeleteで削除、という流れになるのでしょうか?
SendでなくPostで処理結果があることを通知するとなると、
そのオブジェクトを誰が作って誰がいつ破棄するのかという疑問があったため、
このへんのお決まりの手法があれば教えていただけると嬉しいです。

仲澤@失業者 2010/02/22(月) 13:11:13
>この場合、処理結果(リストに追加するべき情報)を表す構造体やクラスは、
>ワーカスレッドがnewで作成し、メインスレッドのダイアログが使い終わったら
>ダイアログ側でdeleteで削除、という流れになるのでしょうか?

違うと思います。スレッド/DLG双方がなくても「処理結果」は存在しなくては
なりません。これにより、スレッド/DLGのどちらか、又は双方、しかも順不同
に失われても大丈夫です。

PATIO 2010/02/22(月) 16:12:34
やり方次第と言う部分も有りますけれど。

キューその物はプロセスが開始する時点で作成すると思います。
排他処理も必要になると思います。
ポインタリストで作成するならワーカースレッド側で
作成して、メインスレッド側で取り出して削除と言うのも
有りでしょう。

検索スレッドと表示スレッドで同期を取るようにするのであれば、
受け渡し領域を一つにしておいてワーカースレッド側で置いたら
メインスレッド側で取り出すまで待つと言う手もあると思います。
この場合、メインスレッドがさっさと取り出してくれないと
検索側が止まってしまいますけれど。
この辺は仕組みの組み方次第ではないかなと思います。
理想からするとメインスレッドは置かれた物を取り出して
どんどん処理する。取り出したらキューから外される。
ワーカースレッドはキューにどんどん追加する。
ワーカースレッドは追加する時にキューが空だったら
追加した後、メインスレッドにデータを置いたメッセージを
送信する。
最後に検索が終わったらメインスレッドに通知する。
とか。

かなり殴り書きなので不整合があるかもしれませんけれど。

PATIO 2010/02/22(月) 16:14:18
補足

> ポインタリストで作成するならワーカースレッド側で
> 作成して、メインスレッド側で取り出して削除と言うのも
> 有りでしょう。

これはポインタリストに入れるレコード情報の事を
言っています。リストその物は作成されている必要が
ありますが、中に入れる物はその都度作成すると思うので。

亀山 2010/02/23(火) 13:43:01
仲澤@失業者さん、PATIOさん、ご意見ありがとうございます。
数値情報以外のものをSendではなくPostでやりとりしようとすると
とたんに情報の管理が難しくなりますね。

今回の件そのものについては解決マークを付けさせていただきますが、
最後にもう一つよろしいでしょうか。

ワーカスレッドが終了したことをダイアログ側が知りたい場合、
CWinThreadのm_bAutoDeleteをFALSEにした状態でワーカスレッドを開始し、
そのCWinThreadポインタを使って、GetExitCodeThread()で
STILL_ACTIVEかどうかを調べることになるのでしょうか?

現在のところは、ワーカスレッドを終了する直前でフラグを立てておいて
ダイアログ側のOnTimer()の中でそのフラグをチェックすれば、
CWinThread自体は自動削除のままにできてよいかなと思って
そのような仕組みにしてみました(AfxBeginThread()の引数は2つのまま)。

何度も動作テストしてみたところ、デッドロックは解決していそうなのですが、
ワーカスレッド自身がフラグを立てた直後はまだ終了していない状態ですから、
スレッド切り替えのタイミングによっては、終了していない状態で
ダイアログ側が終了したと判断してしまうのかなと疑問に思っています。

これは、ダイアログがOnTimer()でチェックする方法だけでなく、
もう一つmaruさんやtetrapodさんやPATIOさんに提示していただいた
PostMessage()でダイアログに終了を通知する方法でも
同じ問題になるのかなと思っています(Postした直後はまだ生きていますよね?)。

終了するまさに一歩手前だから、万一の絶妙なタイミングでダイアログ側が先に
次の処理に移ってしまっても大した問題にはならないのかなとは思いますが、
どうせならこの機会に学んでおきたいと思い、追加で質問させていただきます。
よろしくお願いします。

亀山 2010/02/23(火) 13:43:48
[[解決]]
解決マークを付けさせていただきますと言っておいてチェックしていませんでした(恥)。
よろしくお願いします。

仲澤@失業者 2010/02/23(火) 15:30:35
スレッドの終了確認にはイベントを使用するのが通常ですが、
簡略に済ませたい場合、かつ、ローカルに済ませたい場合、
自分はDLG、スレッドが両方ともなくなっても存在している
オブジェクト内にvolatileのフラグを用意してこれで判定しています。

>現在のところは、ワーカスレッドを終了する直前でフラグを立てておいて

の「フラグ」はvolatileであるべきです。
でないと、リリースビルド時にキャッシュされた「フラグ」が
参照されてしまい、うまく働がない場合がありえます。

>ワーカスレッド自身がフラグを立てた直後はまだ終了していない状態ですから、
>スレッド切り替えのタイミングによっては、終了していない状態で
>ダイアログ側が終了したと判断してしまうのかなと疑問に思っています。

開始処理と同様、スレッドの終了処理が開始されてしまうと終了処理を完了
するまでは、原理的に他のスレッドに切り替えられないはずです。
スレッド関数のリターン直前の「フラグ」設定は、事実上スレッドの
最後の処理と同意です。

tetrapod 2010/02/23(火) 16:34:51
俺はいつも
・bAutoDelete=FALSE とする (そのためには CREATE_SUSPENDED を使う)
・基本的にスレッド終了チェックは不要なように作る
 すなわち、スレッドが終了するまで絶対に Dlg や WND が消滅しないように作る
・チェックが必要であるなら WaitForSingleObject(スレッド, 0) でチェック
としているよん。

PATIO 2010/02/26(金) 13:18:11
私の場合ですが、基本的にウインドウメッセージで通知するのは
タイミングのみで具体的な情報はグローバル領域に作った
共有データクラスのインスタンスで引き渡します。

共有データクラスは、内部にやり取りするデータを保持していて
取り出しと設定はクラスメソッドでやります。
メンバー変数は勿論private変数です。
クリティカルセクションを使って排他処理行い。
データを扱う側は意識しなくて良いようにします。

あと、Postによって通知されるのはあくまでも終了するよという
通知です。勿論、それを受けたメインスレッドはワーカースレッドの
終了をスレッドハンドルで監視します。
ここが終了の待ち合わせになります。
ワーカースレッドが完全に終了している状態を確認する場合、
そのようにします。
この辺のやり取りは、CWinThreadのm_bAutoDeleteをFALSEして
あくまでもスレッドのオブジェクトの破棄はメインスレッドで
やると言うスタンスになります。

私自身としてはワーカースレッドの状態(存在とか動作中等)は、
メインスレッドが完全に把握している状態にしようとします。
これは、ワーカースレッド動作中にアプリを終了させるような場合に
ワーカースレッドの終了を待ち合わせて必要な終了処理を行う為にも
必要な事だと考えています。

亀山 2010/02/27(土) 17:16:23
[[解決]]
仲澤@失業者さん、tetrapodさん、PATIOさん、
引き続き解説いただきありがとうございます。

> 開始処理と同様、スレッドの終了処理が開始されてしまうと終了処理を完了
> するまでは、原理的に他のスレッドに切り替えられないはずです。
> スレッド関数のリターン直前の「フラグ」設定は、事実上スレッドの
> 最後の処理と同意です。

これはつまり、フラグを立ててから直後に死ぬまでの間に
UIスレッド側に切り替わってその立ったフラグを見てしまうことは、
原理的には無いということになるのですね。

となると、フラグをタイマで監視する方法も、
UIスレッド側にPostMessage()で終了を伝える方法も、
原理上は2つの引数のまま(m_bAutoDelete=TRUEのまま)
ワーカスレッドを開始しても問題無いのかなと感じますが、
tetrapodさんもPATIOさんもm_bAutoDeleteはFALSEにされているのですね。

終了したのを知りたいだけでなく、高度な監視をしようとすると、
やはりスレッドのハンドルは持っておいたほうがよいということですよね。

じっくり勉強させていただきます。
みなさんありがとうごうざいました。

古いスレッドには追加発言できません。