###ワーカースレッドで作ったモーダルダイアログが表示されない
###発生している問題・エラーメッセージ
手順1.メインスレッドでワーカースレッドを作成する 手順2.メインスレッドは無限ループに入る 手順3、ワーカースレッドで作ったモーダルダイアログ表示されない
###該当のソースコード
C++
1void CMFCApplication3Dlg::OnBnClickedButton1() 2{ 3 AfxBeginThread(TestThreadProc, this); 4 5 while (true) 6 { 7 Sleep(1); 8 } 9 10} 11 12UINT TestThreadProc(LPVOID param) 13{ 14 SelfModal dlg = new SelfModal(); 15 16 dlg.DoModal(); 17 18 delete dlg; 19 20 return 0; 21}
###知りたいこと
メインスレッドとワーカースレッドはディスパッチしながら交互に動作すると考えています
なので、たとえメインスレッドが無限ループで絶え間なく稼動していてもワーカースレッドは動けると思うんですが
なぜかワーカスレッドが固まってしまい、ダイアログ表示ができません
メッセージキューが関係しているのでしょうか?
この現象の詳細がわかる方教えてください
###補足情報
この作りは、とある学校関係の既存アプリで非常に重い処理をメインスレッドでやっているためにUIが固まってしまうので苦肉の策で別ダイアログを表示しておき、進捗状況を示すためにやろうとしてのものです。
既存アプリを作り変えるほどの人力は無いのでこのまま何とかするしかない状態です。
この現象の詳細な原因がわかればと思います。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答5件
0
MFCは、ワーカースレッドのdlg.DoModal()を行った時点で、メインウィンドウの入力を無効にするため、メインウィンドウにメッセージを送信します。
メインウィンドウ側は、OnBnClickedButton1イベントハンドラ内で無限ループしてますので、このメッセージを処理できません。
ワーカースレッドは、メインウィンドウのメッセージ処理が終わるまで待ちますので、ここで処理がプロックします。
投稿2017/05/31 06:36
総合スコア245
0
ベストアンサー
VS2017で確認したところDoModal内部でEnableWindow、NtUserCallHwndParamLockと呼び出して固まっているようです。
NtUserCallHwndParamLockの中身がどうなっているかは分からないですが、
以下のコードのように単純にすればWindowが表示されメッセージループも動作します。
c++
1UINT TestThreadProc(LPVOID param) 2{ 3 HWND hwnd = ::CreateWindow( 4 TEXT("STATIC"), TEXT("test"),WS_CAPTION,100, 100, 200, 200, NULL, NULL,::GetModuleHandle(0), NULL); 5 6 if (hwnd == NULL) return 0; 7 8 ::ShowWindow(hwnd, SW_SHOW); 9 MSG msg{}; 10 BOOL bRet; 11 while ((bRet = GetMessage(&msg, hwnd, 0, 0)) != 0) 12 { 13 if (bRet == -1) 14 { 15 break; 16 } 17 else 18 { 19 TranslateMessage(&msg); 20 DispatchMessage(&msg); 21 } 22 } 23 24 return 0; 25}
いろいろ誤解があるような追加で記載しておきます。
MFCのDoModalのソースはVS2017であれば以下の場所にあります。
C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\VC\Tools\MSVC\14.10.25017\atlmfc\src\mfc\dlgcore.cpp
DoModalの処理を最大限端折ってコピペすると以下のようになっています。
c++
1INT_PTR CDialog::DoModal() 2{ 3 //略 4 5 // disable parent (before creating dialog) 6 HWND hWndParent = PreModal(); 7 ::EnableWindow(hWndParent, FALSE); 8 9 //略 10 CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), AfxGetInstanceHandle()); 11 //略 12 13 return m_nModalResult; 14}
CreateRunDlgIndirectがWindowを作成する処理です。今回のフリーズはその前のEnableWindowで固まっています。(バージョンによって違うかもしれませんが。)
CreateRunDlgIndirectはCreateDialogIndirectを呼び出してWindowを作成したのち、CWnd::RunModalLoopでメッセージループを回します。
CreateDialogIndirectは内部でCreateWindowExを呼んでいるとあります(The CreateDialogIndirectParam function uses the CreateWindowEx function to create the dialog box)
ですので、DoModalがメインスレッド上でWindowを作っているとかではありません。
他の方が指摘している「普通はメッセージループは一つにする」というのはあくまで、グローバル変数とかを使っていてActiveXとかサードパーティーのUIコントロールがマルチスレッド未対応だったりとかで、いろいろ問題があるのでUIフレームワークが1スレッドで操作されるのを前提としているのが「普通」というだけです。
EXPLORERをspyxx.exeで確認すると複数スレッドでメッセージループを回しているのが分かると思います。
ですので、MFCでは難しいかもしれないですが、固まっているUIスレッドに手を出さないように、マルチスレッドを注意しつつ進捗状況をメモリから直接取得し、Windowを作成して描画してあげればやりたいことが実現できると思います。
投稿2017/05/30 15:27
編集2017/05/31 05:48総合スコア818
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
MFC に限りませんが、UIはメインスレッドで処理して、時間のかかる処理をサブスレッドで処理するようにしないとアプリのみならずOS全体がぎくしゃくします。
Windows の設計思想に起因する根源的な問題なのでどうにもならない部分もあるのでまぁそういうものだと思うしかないでしょう。
なので、メインスレッド側(ユーザーのアクションで処理を行う最初のハンドラ)でモーダルダイアログを出して待機するようにします。
スレッド側には待機中を出すモーダルダイアログのウィンドウハンドルを渡して置き、処理が終わったら専用の処理終了メッセージを発行します。
待機ダイアログはユーザー操作で終了してしまわないように、OnOK, OnCancel の二つの仮想関数を空で用意しておきます(エンターキー、ESCキーの処理をブロックするための必須機能)。
そのうえで、専用の処理終了メッセージを受け取ったら、EndDialog(IDOK);など、なにかしら適当な終了コードを入れるようにしておきます。
あとは、待機ダイアログのWM_INITDIALOG(OnInitDialogで受けるのが望ましい)で、計算スレッドを起動してやれば、ぼさっと待ってるだけで全部段取りが終わります。
投稿2017/05/30 13:59
総合スコア1392
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
うーん…もうかなり過去に学んだ話なのでうろ覚えですが。MFCもやってませんし。
ただ基本はMFCだろうとdelphi(他の会社が作ってるWindowsネイティブアプリの開発言語/環境)だろうと全部同じだったはずなので解説しますね。
メインスレッドにメッセージループがあるのは存じているでしょうか。
多分MFCプロジェクトを普通に作ると隠されていて見ることはできません。
アプリケーション実行中のすべてのウィンドウメッセージがこのメインループを通過します。
貴方が作るボタンクリック時のイベント等はこのメッセージループを経由しています。もしもマウスカーソルをアプリケーション上で動かすと、それらのイベントは全てこのメッセージループに集約されます。
Windowsではアプリケーションへの様々な命令を一旦このメッセージループに蓄積していくことで逐次処理しています。MFCでもそうだったと記憶していますが、Windows標準のアプリケーションは基本的にメッセージループでディスパッチャが働いています。
このような動作方法をデザインパターンでChain of Responsibilityパターンと言いますが、これには明確な欠点があります。連続する処理のいずれかが止まると後続の処理が行われなくなってしまうのです。
ウィンドウの移動などは全てこの「マウスがクリックされるメッセージ」、「ウィンドウを移動するメッセージ」というように分解されており、それがメッセージループから逐次ディスパッチされてそれぞれの処理が完了します。ということは、メッセージループが停止するだけでこの動作は完全に停止してしまいます。
「ウィンドウを表示する処理」も例外ではありません。
※調べなおしてたら勘違いな気がしてきたので括っておきます
で、ここからが本題です。 **サブスレッドからウィンドウを作っても新しくメッセージループは作成されません。** サブスレッド内の処理にはメッセージループがありませんよね? 別スレッドから画面を作成したとしても、Formの`DoModal`等の処理はメッセージループに依頼を投げた後、メッセージが処理されるのを待つことになります。下位のダイアログなどのクラスはそういった依頼を簡易化する処理だけで成り立っているはずです。このためにサブスレッドからウィンドウを表示しようとしても停止してしまいます。
hmmmさんのサンプルはこういった簡易処理に頼っていません。
CreateWindow
やShowWindow
を使いブロックしない形で処理を続けた後、サブスレッド内でGetMessage
を呼び出しています。これが偽のメッセージループになっているわけです。
DispatchMessage
によって処理が行われますが、これは本来のメッセージループが行う処理を横取りしている形です。
通常こういうことは許されず、悪いマナーとされていたと思います…
(※こういうことはやろうと思わないので実際がどうかはわかりませんが。)
そのために「メインスレッドで描画処理だけを行い、重たい処理はサブスレッドに投げる」ということになっていると認識しています。
最新のライブラリだとUIスレッドが決まっていて、ユーザーが考えるメインスレッド=UIスレッドになっているので、そもそも単純にこういうことをできないものなんですが、MFCの動作はもっと単純なのでhmmmさんのサンプルコードのような形でも動作するのかもしれません。
幸い、メインスレッドが止まっているため別のメッセージループを設けても大丈夫だとは思いますが、メインスレッドの重たい処理が終わったらまずはサブスレッドのメッセージループを停止し、その完了を待ってからメインスレッドの処理を続行するようにしてください。
メッセージループについては下記が詳しいです。
https://msdn.microsoft.com/ja-jp/library/windows/desktop/ff381405(v=vs.85).aspx
#追記
追記勘違いっぽかったので削除しました。
やったことないことは回答に書いてはダメですね。
投稿2017/05/31 02:51
編集2017/05/31 05:16総合スコア1593
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/05/31 03:40
2017/05/31 05:24
2017/05/31 07:22
2017/05/31 08:00
2017/05/31 08:52
2017/06/01 04:00

0
こんにちは。
MFCは使ってないので知らないのですが、モーダル・ダイアログは通常内部でメッセージ・ループを回しています。(でないと戻ってきてしまいます。)
そして、GetMessageは、GetMessageを呼び出したスレッドのキューからメッセージを取ってきます。
つまり、サブ・スレッドのキューからとるわけです。そのキューへメッセージを送るウィンドウは存在していませんので、単純に無限ループになる筈です。
次に、モーダル・ダイアログのウィンドウは、通常はメイン・スレッドが生成しますので、そのウィンドウ・メッセージはメイン・スレッドのキューへ届きます。
メイン・スレッドがメッセージ・ループを回ってないのでそのウィンドウ・メッセージを処理するスレッドがありません。
そして、サブ・スレッドは無限にメッセージ待ちしています。
結果、ハングアップということと思います。
スマートな対策はhmmmさんの回答のようにサブ・スレッドでウィンドウを生成して、それに対してメッセージ・ループで処理することです。
MFC経由でも同様な処理はできるかも知れませんが、ちょっと私にはわかりません。
【追記】
次に、モーダル・ダイアログのウィンドウは、通常はメイン・スレッドが生成しますので、そのウィンドウ・メッセージはメイン・スレッドのキューへ届きます。
通常はこの通りですが、ご提示されたソースでは、サブスレッドで生成しようとしているようですね。
そして、DoModal()が親ウィンドウ(たぶんメイン・ウィンドウ)へEnableWindow()しており、親ウィンドウはメイン・スレッドで生成しているから応答できず、そこでハングアップということのようです。
この仕組みから、モーダル・ダイアログを、その親ウィンドウと異なるスレッドで生成する場合はメイン・スレッドはメッセージ・ループを回っていないといけないと言うことと思います。
対策するなら、以下でできそうな印象です。
①メイン・スレッドの長時間処理に入るところで、EnableWindow(.., false);
②サブ・スレッドでモードレスダイアログを表示して進捗表示
③メイン・スレッドの長時間処理を抜ける(終了やキャンセル)したところで、EnableWindow(..., true);
実際にやったことはないので外していたらごめんなさい。
モードレスダイアログがちゃんとメイン・ウィンドウより上に表示できないかも知れません。
HWND_TOPMOSTとか使わないといけないかも?
投稿2017/05/30 16:56
編集2017/05/31 08:31総合スコア23274
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/05/31 00:46
2017/05/31 06:16

あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/05/31 11:35
2017/05/31 12:06
2017/05/31 12:39
2017/05/31 12:44
2017/05/31 13:06
2017/05/31 13:20
2017/05/31 13:22