スレッドの超難問 スレッドで分岐した処理の中でダイアログを閉じるとエラーになる

[戻る]
村田製 2005/07/07(木) 11:53:52
VC++6.0 MFC WindowsXP sp2

DoModalで立ち上げたクラスで以下の処理を行っています。
OnInitDialogでスレッド分岐したDeleteFunc関数の中で
OnDlgEndを呼んで処理を終了させています。
その時、PostThreadMessageでスレッドを終了すると
自分自身を終了するので処理が止まってしまいます。
また、PostThreadMessageとWaitForSingleObjectをコメントにすると
スレッドが終了しないままダイアログを閉じるので
エラーになってしまいます。
なんとか上手い方法で正常終了させたいです。
DoModalで立ち上げているのが、難しさをさらにupさせています。

誰か教えてください。


XXXXDlg::XXXXDlg( CWnd* pParent /*=NULL*/)
    : CDialog(XXXXDlg::IDD, pParent)
{
    m_WindowText    = "";
}

BOOL XXXXDlg::OnInitDialog()
{
    CDialog::OnInitDialog();
    
    m_bCancel = FALSE ;
    
    m_thread = AfxBeginThread( DeleteFunc, this, THREAD_PRIORITY_NORMAL);
    
    return FALSE;  
}

void    ProgressBarDlg::OnDlgEnd(){

    // スレッドを削除
    ::PostThreadMessage(m_thread->m_nThreadID, WM_QUIT, 0, 0);
    ::WaitForSingleObject(m_thread->m_hThread, INFINITE);

    CDialog::OnCancel();
}

static UINT DeleteFunc( LPVOID    pParam)
{
    ProgressBarDlg* pDlg = (ProgressBarDlg*)pParam;

    for( int i = 0; i < FullPath.size(); i++ ){
         処理
        :    
    }

    pDlg->ShowWindow(FALSE);
    pDlg->OnDlgEnd();
    return 0;
}

村田製 2005/07/07(木) 11:55:58
少しソースを間違えました。
これが正規のソースです。

XXXXDlg::XXXXDlg( CWnd* pParent /*=NULL*/)
    : CDialog(XXXXDlg::IDD, pParent)
{
    m_WindowText    = "";
}

BOOL XXXXDlg::OnInitDialog()
{
    CDialog::OnInitDialog();
    
    m_bCancel = FALSE ;
    
    m_thread = AfxBeginThread( DeleteFunc, this, THREAD_PRIORITY_NORMAL);
    
    return FALSE;  
}

void    XXXXDlg::OnDlgEnd(){

    // スレッドを削除
    ::PostThreadMessage(m_thread->m_nThreadID, WM_QUIT, 0, 0);
    ::WaitForSingleObject(m_thread->m_hThread, INFINITE);

    CDialog::OnCancel();
}

static UINT DeleteFunc( LPVOID    pParam)
{
    XXXXDlg* pDlg = (XXXXDlg*)pParam;

    for( int i = 0; i < FullPath.size(); i++ ){
         処理
        :    
    }

    pDlg->ShowWindow(FALSE);
    pDlg->OnDlgEnd();
    return 0;
}

Ban 2005/07/07(木) 12:17:39
WM_QUIT は自分で Postしてはいけないメッセージです。(MSDN参照)
終了要求用に別のメッセージをそのスレッドに送りつけて、
スレッド内からPostQuitMessage で自壊すればよさそうに見えます。

村田製 2005/07/07(木) 12:46:52
Banさん ご意見ありがとうございます。
しかし、私の理解力では意味がわかりません。
もう少し具体的な箇所を教えてください。
何の関数を変更すればよいのでしょうか?

・・・ 2005/07/07(木) 13:38:45
(WIN32系MFCの)マルチスレッドには2種類あり、1つは「ワーカー(作業)スレッド」
で
CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID 
pParam, ・・・ );

もう1つは「ユーザー インターフェイスレッド」です。
CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, ・・・ );

村田製さんのコードを拝見すると、テストプログラム的な雰囲気を感じます。
#外していたら、ごめんなさい
で、「ワーカー(作業)スレッド」として生成していますが。
終了は「ユーザー インターフェイス スレッド」で終わろうとしてませんか?


「スレッドの正常終了」の引用です。(MSDN参照)↓
ワーカー スレッドでは、スレッドの正常終了は単純です。制御関数を終了し、終了理由を呼び
出し側に返すだけです。これには関数 AfxEndThread を使うことも、return ステートメント
を使うこともできます。通常、0 を返して正常終了を通知しますが、返す値はプログラマが決め
ることができます。
ユーザー インターフェイス スレッドの正常終了処理も単純です。終了するスレッドから、関
数 ::PostQuitMessage を呼び出すだけです。引数はスレッドの終了コードだけです。ワーカ
ー スレッドの場合と同じように、通常は 0 を返して正常終了を通知します。

村田製 2005/07/07(木) 13:47:55
・・・さん ありがとうございました。
スレッドに関しては初期の頃より理解が深まりました。

修正箇所ですが、下記の通りです。
しかし、これを実行しますとエラーになります。
何がいけないのでしょうか?

void    XXXXDlg::OnDlgEnd(){
  <<=====消去
    CDialog::OnCancel();
}

static UINT DeleteFunc( LPVOID    pParam)
{
    XXXXDlg* pDlg = (XXXXDlg*)pParam;

    for( int i = 0; i < FullPath.size(); i++ ){
         処理
        :    
    }

    pDlg->ShowWindow(FALSE);
    pDlg->OnDlgEnd();
  AfxEndThread(0); <<=====追加
    return 0;
}

村田製 2005/07/07(木) 13:51:31
エラー箇所です。

wincore.cpp 3475行目


int CWnd::RunModalLoop(DWORD dwFlags)
{
    ASSERT(::IsWindow(m_hWnd)); // window must be created
    ASSERT(!(m_nFlags & WF_MODALLOOP)); // window must not already be in 
modal state

    // for tracking the idle time state
    BOOL bIdle = TRUE;
    LONG lIdleCount = 0;
    BOOL bShowIdle = (dwFlags & MLF_SHOWONIDLE) && !(GetStyle() & 
WS_VISIBLE);
    HWND hWndParent = ::GetParent(m_hWnd);
    m_nFlags |= (WF_MODALLOOP|WF_CONTINUEMODAL);
    MSG* pMsg = &AfxGetThread()->m_msgCur;

    // acquire and dispatch messages until the modal state is done
    for (;;)
    {
        ASSERT(ContinueModal());

        // phase1: check to see if we can do idle work
        while (bIdle &&
            !::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
        {
            ASSERT(ContinueModal());

            // show the dialog when the message queue goes idle
            if (bShowIdle)
            {
                ShowWindow(SW_SHOWNORMAL);
                UpdateWindow();
                bShowIdle = FALSE;
            }

            // call OnIdle while in bIdle state
            if (!(dwFlags & MLF_NOIDLEMSG) && hWndParent != 
NULL && lIdleCount == 0)
            {
                // send WM_ENTERIDLE to the parent
                ::SendMessage(hWndParent, WM_ENTERIDLE, 
MSGF_DIALOGBOX, (LPARAM)m_hWnd);
            }
            if ((dwFlags & MLF_NOKICKIDLE) ||
                !SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, 
lIdleCount++))
            {
                // stop idle processing next time
                bIdle = FALSE;
            }
        }

        // phase2: pump messages while available
        do
        {
            ASSERT(ContinueModal()); <<=====エラー箇所

            // pump message, but quit on WM_QUIT
            if (!AfxGetThread()->PumpMessage())
            {
                AfxPostQuitMessage(0);
                return -1;
            }

            // show the window when certain special messages 
rec'd
            if (bShowIdle &&
                (pMsg->message == 0x118 || pMsg->message 
== WM_SYSKEYDOWN))
            {
                ShowWindow(SW_SHOWNORMAL);
                UpdateWindow();
                bShowIdle = FALSE;
            }

            if (!ContinueModal())
                goto ExitModal;

            // reset "no idle" state after pumping "normal" 
message
            if (AfxGetThread()->IsIdleMessage(pMsg))
            {
                bIdle = TRUE;
                lIdleCount = 0;
            }

        } while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE));
    }

ExitModal:
    m_nFlags &= ~(WF_MODALLOOP|WF_CONTINUEMODAL);
    return m_nModalResult;
}

TRD 2005/07/07(木) 14:30:30
>    CDialog::OnCancel();

をダイアログが所属しているのとは別のスレッドが実行したらだめ。
ダイアログの所属スレッドに EndDialog(IDCANCEL) を実行してもらうように。

>    AfxEndThread(0); <<=====追加

は要らない。制御関数を return で抜ければ、スレッドは自然に消滅するから。

村田製 2005/07/07(木) 14:57:20
つまり、こういう事ですか?
これを実行しても先ほど書いたwincore.cpp 3475行目で落ちます。
なにがいけないのでしょうか?

void    XXXXDlg::OnDlgEnd(){
    EndDialog(IDCANCEL);
}

static UINT DeleteFunc( LPVOID    pParam)
{
    XXXXDlg* pDlg = (XXXXDlg*)pParam;

    for( int i = 0; i < FullPath.size(); i++ ){
         処理
        :    
    }

    pDlg->ShowWindow(FALSE);
    pDlg->OnDlgEnd();
    return 0;
}

TRD 2005/07/07(木) 15:08:37
OnDlgEnd() を実行しているのがワーカースレッドだから、
OnDlgEnd() の中に書かれている EndDialog() もワーカースレッドが
実行してしまいます。

EndDialog() はダイアログを作成した側のスレッドが、
ワーカースレッドの終了を検知した時点で実行するようにすればいいです。

村田製 2005/07/07(木) 15:45:05
完全ではありませんが、なんとなくわかりました。
DeleteFuncの中の処理でOnDlgEndを呼んではいけないという事ですよね?

しかし、OnInitDialogで分岐しているので、
日々更新される関数でも無い限り、スレッドが終了したかどうかの確認は
取れないと思います。

うーん。難しいです。
CDialogの中にON_UPDATE_COMMAND_UIみたいな関数があればそこに
終了したかどうかのフラグをチェックする処理を起きたいのですが。
そんなメソッドありませんよね。

シャノン 2005/07/07(木) 15:58:06
#外してたらごめん
ワーカースレッドからダイアログに、WM_CLOSE でも投げればいいんでわ?

dairygoods 2005/07/07(木) 16:02:56
DeleteFuncスレッドからメインスレッドに
「終わったよー」と通知すればよいのでは?

通知はPostMessage等でできます。


> 日々更新される関数でも無い限り、スレッドが終了したかどうかの確認は
> 取れないと思います。

こっちの方法でももちろん可能です。
たとえば、WM_TIMER。
MFC独自のWM_KICKIDLEというのもあります。

TRD 2005/07/07(木) 16:08:25
ワーカースレッドが return の直前にダイアログ宛に「終了するよ」という
ユーザー定義メッセージをポストしてやればいいです。
ダイアログ宛のメッセージはダイアログを作成したスレッドに届けられます。
メッセージハンドラはダイアログを作成したスレッドで実行されるので、
そこで EndDialog() を呼び出してやる。

ただし、メッセージを受け取った時点でワーカースレッドが本当に return まで
実行し、消滅している保証は無いです。
その後、本当に終了するまで待ち合わせるには WaitForSingleObject() などを使う
必要があります。まぁ、待つといってもほんの数ミリ秒でしょうけど。

村田製 2005/07/07(木) 16:11:00
AfxGetMainWnd()->PostMessage(WM_CLOSE);
を追加したのですが、メインのプログラムごと終了してしまいました。
ダイアログのみを終了させるには、どうすればよいのでしょうか?

static UINT DeleteFunc( LPVOID    pParam)
{
    XXXXDlg* pDlg = (XXXXDlg*)pParam;

    for( int i = 0; i < FullPath.size(); i++ ){
         処理
        :    
    }

    pDlg->ShowWindow(FALSE);
    //pDlg->OnDlgEnd();
    AfxGetMainWnd()->PostMessage(WM_CLOSE); <<========== 追加
    return 0;
}

dairygoods 2005/07/07(木) 16:47:49
ダイアログに送りたいのだから、
pDlg->PostMessage(WM_CLOSE);
では?

Ban 2005/07/07(木) 16:51:44
> AfxGetMainWnd()->PostMessage(WM_CLOSE);

メインウィンドウに WM_CLOSE を送れば、
普通のWindowsアプリがデフォルトで終わるのは当然かと。

メッセージの送り先はダイアログにするべきだと思います。
また、別にWM_CLOSE である必要もないので独自メッセージでもかまいません。
ダイアログがそのメッセージを受け取ったときに自壊できればいいだけです。

村田製 2005/07/07(木) 17:03:52
[[解決]]
みなさんありがとうございました。
一人で考えていたら絶対に出来なかったと思います。
感謝しています。

以下が最終のソースになります。


XXXXDlg::XXXXDlg( CWnd* pParent /*=NULL*/)
    : CDialog(XXXXDlg::IDD, pParent)
{
}

BOOL XXXXDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    SetTimer(ID_TIMER, 100, NULL);//0.1秒の設定 <<===追加
  EndFlg = FALSE;<<===追加
    
    m_thread = AfxBeginThread( DeleteFunc, this, THREAD_PRIORITY_NORMAL);
    
    return FALSE;  
}

void    XXXXDlg::OnDlgEnd(){

    EndFlg = TRUE;<<---追加
}

static UINT DeleteFunc( LPVOID    pParam)
{
    XXXXDlg* pDlg = (XXXXDlg*)pParam;

    for( int i = 0; i < FullPath.size(); i++ ){
         処理
        :    
    }

    pDlg->ShowWindow(FALSE);
    pDlg->OnDlgEnd();
    return 0;
}

// 
void XXXXDlg::OnTimer( UINT nIDEvent ){

    if(EndFlg == TRUE){
        EndDialog(IDCANCEL);
    }
    CDialog::OnTimer(nIDEvent); 
}

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