ダイアログの切替時にワーカススレッドを正しく終了させる方法


マサ  2010-06-07 10:53:32  No: 71700  IP: [192.*.*.*]

タブコントロールで2つのタブが存在する、ダイアログベースのアプリを作成しています。

各タブで使用しているダイアログ上で、切替表示時(OnShowWindow())に、ログファイルを読み込み、各行をCTreeCtrlでツリー表示させることをしています。
ツリーコントロールのアイテム数が不明ということで、別スレッドで処理を行いアイテム追加を行っています。

ログの読込み、TreeCtrlにアイテム追加中でも、タブコントロールを切替できるよう作成する必要があります。
始めは、※1の方法で、ダイアログの非表示時にスレッドを終了させる方法を考えていましたが、どうも、WaitForSingleObject()が呼ばれたときにforループ中でフリーズしてしまう現象が起こりました。
いろいろ試行錯誤して、※2のようにPostMessage()で1アイテムあたりのデータを送信する方法に変更したところ、正常にログ読込み時でも終了することができました。

正常に動作しているように見えますが原因がわからず、少し気持ち悪い状態です。
長文で申し訳ありませんが、原因がわかる方がいましたらご教授お願いいたしますm(_ _)m

--追伸--
メインのスレッドでWaitForSingleObjectを使っているのは少し気持ち悪いですが、ログ読込みのスレッドが完全に終了してから
ダイアログの切替を行いたいために、以下のコードにしました。ログの読込みにはさほど時間がかかりませんが、TreeCtrlにアイテム挿入時に時間がかかるため、
実質、ログ読込みのスレッドの停止時にはすぐに終了します。


//■メンバ変数
//TreeCtrl
CTreeCtrl  m_cLogTreeCtrl;
//停止フラグ
bool m_bStopFlag;


void CLogDlg::OnShowWindow(BOOL bShow, UINT nStatus)
{
    //表示
    if(bShow) {
        m_bStopFlag = false;
        m_pReadLogThr = AfxBeginThread(CLogDlg::LogTreadProcStub, this);
        m_pReadLogThr->m_bAutoDelete = FALSE;
        m_pReadLogThr->ResumeThread();
    }
    //非表示
    else {
        m_bStopFlag = true;

        //スレッドインスタンスの破棄
        if(m_pReadLogThr) {
            WaitForSingleObject( m_pReadLogThr->m_hThread, INFINITE );            
            delete m_pReadLogThr;
            m_pReadLogThr = NULL;
        }
    }
}



//ダメな方法  ※1
bool CLogDlg::LogThreadProc() {
    
    //ログ読込み作業 省略・・・
    vector<CString> aLog;
    
    for(UINT ii = 0; ii < aLog.size(); ii++) {

        if(m_bStopFlag) break;

        m_cLogTreeCtrl.SetItem(....);    //ログを追加
    }
    
}

///////////////////////////////////////////////////////////////////////////////
//正常に行えた方法  ※2
bool CLogDlg::LogThreadProc() {
    
    //ログ読込み作業 省略・・・
    vector<CString> aLog;
    
    for(UINT ii = 0; ii < aLog.size(); ii++) {

        if(m_bStopFlag) break;

        PostMessage(...);    //1アイテムあたりのデータを送信する
    }
}


LRESULT CLogDlg::OnReceiveMsg(UINT wParam,LONG lParam) 
{
    m_cLogTreeCtrl.SetItem(....);    //ログを追加
}

編集 削除
マサ  2010-06-07 10:55:25  No: 71701  IP: [192.*.*.*]

開発環境を記述を忘れました。
Visual Studio2008 MFC環境になります。

編集 削除
tetrapod  2010-06-07 11:59:36  No: 71702  IP: [192.*.*.*]

何が聞きたいのか微妙にわからないが・・・

CWnd にはスレッドローカル性という制約があるので、
CWnd オブジェクトを生成したスレッドと、
CWnd オブジェクトを操作するスレッドが違う、と、動作は保証されない。
http://support.microsoft.com/kb/147578/ja
機械翻訳はさっぱりわからんな・・・原文見たほうがいい。

m_cLogTreeCtrl は UI スレッド上で生成されるので
AfxBeginThread で生成したワーカースレッド上でこれを直接使ってはダメ

編集 削除
NOR  2010-06-07 12:10:12  No: 71703  IP: [192.*.*.*]

m_cLogTreeCtrl.SetItem()は、TVM_SETITEMをSendMessage()で送っているだけです。

そして、そのツリーコントロールを持つメインスレッドは、
WaitForSingleObject(INFINITE)で待っています。

これがデッドロックの原因です。

編集 削除
maru  2010-06-07 15:49:19  No: 71704  IP: [192.*.*.*]

> ツリーコントロールのアイテム数が不明ということで、別スレッドで処理を行いアイテム追加を行っています。
この考えが間違っていると思う。
ユーザインターフェースの処理が時間がかかるのでワーカスレッドで処理
するってのはおかしくない?
ワーカスレッドは処理に時間がかかってその間ユーザインターフェースが
動かなくなるのを回避するものだよね。ユーザインターフェースの処理に
時間がかかるのを回避することはできません(と私は思っている)。
原因はtetrapodさんが書いているとおり。

表示を制御することによって更新中に不要な再表示を抑制することが可能
です。CWnd::SetRedraw()を使ってみてください。
既にご存知でしたらごめんなさい。

編集 削除
マサ  2010-06-07 20:09:28  No: 71705  IP: [192.*.*.*]

返信ありがとうございます。

>>tetrapodさん、NOR さん
スレッドを挟んで、CWnd オブジェクトを互いに操作するのは、動作保障外ということですね。
ワーカススレッドからTreeCtrlの操作は、SendMessage(PostMessage)かでメインスレッドにメッセージを渡して、そこから操作する方法が正しいということでよろしいでしょうか?


>>maruさん
このあたりも、自分の方がよくわかっていないかも知れませんが、
たとえばCTreeCtrlに1万個のアイテム追加するようなケースで
(通常は起こりえないですが・・)メインスレッドで、追加をしてしまった場合、処理が終わるまで、GUI操作ができなくなります。
なので別途スレッドでログの読込みと、SendMessge()でうまくメインスレッドにメッセージを飛ばすことで、うまくメッセージがディスパッチされ、画面がフリーズすること無く処理が完了すると思っていました。

編集 削除
maru  2010-06-07 21:41:17  No: 71706  IP: [192.*.*.*]

確かに別スレッドからツリーに追加する項目をPostすることでコントロール
への追加中でも中断ができますね。
このような目的でメッセージをPostしているのは初めて見ました。
# 処理の経過を表示するためにメッセージを投げるのはよくやります。

ふつう描画はすぐに終わるのでわざわざ項目を追加する目的でスレッドを
作成しません。確かに1万もの項目を追加すると時間はかかるでしょうが、
更新中に描画を行わなければそれ程の時間になるとも思えません。
# 実測したわけではありませんが...
そのために更新中は描画を止め(SetRedraw(FALSE))、データを更新して
から、描画を再開(SetRedraw(TRUE))するくらいで十分なのです。

まあせっかく作った処理ですし、中断ができることはユーザメリットにつ
ながるので、このやり方でUIを更新することは悪いやり方ではないでしょ
うね。
# 個人的は「鶏を割くに焉んぞ牛刀を用いん」という気がしますが。

> なので別途スレッドでログの読込みと、SendMessge()でうまくメイン
> スレッドにメッセージを飛ばすことで、うまくメッセージがディスパッチ
> され、画面がフリーズすること無く処理が完了すると思っていました。
貴方が試した通りSendMessge()ではうまくいきません。PostMessageでないと
うまくゆきません。

編集 削除
ryo  2010-06-07 23:59:43  No: 71707  IP: [192.*.*.*]

デバックモードで動かし、SetItemでブレイクし、
F11をつかって、MFCのソースがどうなってるか見ると面白いです

編集 削除
maru  2010-06-08 12:30:18  No: 71708  IP: [192.*.*.*]

今、別件でCtreeCtrlの性能を測ったんだけど、約15万件の項目を追加して
3秒以下。OnInitDialogの中なので、実際の表示にいく前の処理。スタイル
も+マークと線だけのシンプルなものなので、一概に比べられないだろうが。
ログデータを表示するくらいのことでそれほどの時間がかかるとも思えな
い。やはりオーバースペックなのではないでしょうか。

編集 削除
maru  2010-06-08 12:38:11  No: 71709  IP: [192.*.*.*]

何を言いたいのか今一不明ですが、
> デバックモードで動かし、SetItemでブレイクし、
> F11をつかって、MFCのソースがどうなってるか見ると面白いです
もしかして SetItem は SendMessage のマクロだって言いたいの?

私が言っているのはスレッド間通信としてSendMessageを使うのはよくない
って言ってるんであって、単純にSendMessageがだめって言ってるわけじゃ
ないですよ。

編集 削除
ryo  2010-06-08 14:11:28  No: 71710  IP: [192.*.*.*]

maruさんにいってるのではなく
マサさんにいってる

>ワーカススレッドからTreeCtrlの操作は、SendMessage(PostMessage)かでメインスレッドにメッセージを渡して、
>そこから操作する方法が正しいということでよろしいでしょうか?
ここに対して
「SetItemではなく、メッセージならOKですよね?」→「SetItemの中身みてみよう」
って感じ

編集 削除
maru  2010-06-08 14:27:45  No: 71711  IP: [192.*.*.*]

ああそうか、「メッセージならOKですよね?」に対して、SetItemは結局
SendMessageだよ。それでNGなわけだから、SendMessageではだめってこと
がわかるね、ってことですか。行間が読めず、申し訳ありません。

編集 削除
ロマ  2010-06-08 14:32:55  No: 71712  IP: [192.*.*.*]

スレッド間のSendMessageは
1)送信スレッド:SendMessageを呼び、SendMessageの終了を待機
2)受信スレッド:GetMessageがメッセージを受取り、WndProcを呼び出す
3)受信スレッド:WndProc処理後、GetMessageはメッセージの戻り値を
                送信スレッドに返す
4)送信スレッド:SendMessageは戻り値を取り出し、処理を再開
の順に進みます。
受信スレッドがGetMessageを呼び出さない場合、送信側は待機のままです。

PostMessageは受信スレッドのメッセージキューにメッセージを置くだけですので、
送信スレッドは受信側の状態を気にする必要がありません。
ただし、デフォルトではPostMessageのキューの最大値は10000です。

# SendMessageで受信側のWndProcが送信側スレッドにSendMessageを送る場合は
# 関数を直接呼び出します。
# ここらは古いAdvancedWindowsが詳しいですが、最新版では削除されてしまいました

編集 削除
ロマ  2010-06-08 15:54:32  No: 71713  IP: [192.*.*.*]

追記
PostMessageを一万回送った後、
受信スレッドがWaitFor...から処理を再開する場合、
PostMessageキューのすべてのメッセージの処理が終わるまで
マウスやキーボードのイベントは処理されません。
(PostMessageキューはイベントキューより優先度が高いためです)

CListCtrl::SetCountとSetRedrawを使えば早くなるかも知れません。

編集 削除
ロマ  2010-06-08 21:18:42  No: 71714  IP: [192.*.*.*]

> CListCtrl::SetCountとSetRedrawを使えば早くなるかも知れません。
TreeControlですね。ぼけてました。この行取り消します。

編集 削除