質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
MFC

MFC (Microsoft Fouondation Class)とは、MicrosoftがVC++用に開発したWindows用アプリケーションのフレームワークです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

5回答

15839閲覧

とある状況でワーカースレッドが固まってしまう理由が知りたい

GrapProgrammer

総合スコア13

MFC

MFC (Microsoft Fouondation Class)とは、MicrosoftがVC++用に開発したWindows用アプリケーションのフレームワークです。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

0クリップ

投稿2017/05/30 13:52

###ワーカースレッドで作ったモーダルダイアログが表示されない

###発生している問題・エラーメッセージ

手順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ページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答5

0

MFCは、ワーカースレッドのdlg.DoModal()を行った時点で、メインウィンドウの入力を無効にするため、メインウィンドウにメッセージを送信します。
メインウィンドウ側は、OnBnClickedButton1イベントハンドラ内で無限ループしてますので、このメッセージを処理できません。
ワーカースレッドは、メインウィンドウのメッセージ処理が終わるまで待ちますので、ここで処理がプロックします。

投稿2017/05/31 06:36

Harahira

総合スコア243

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Harahira

2017/05/31 11:35

誤解があるようですが、ワーカースレッドでは、明示的に作成しない限りメッセージキューは存在しません。 GrapProgrammerさんのコードでは、メッセージキューは、メインスレッド上の1つになります。 その1つしかないメッセージキューがブロックされてるため、動作が止まってしまっています。 また、MFCを用いたプログラムでは、CreateWindowでウィンドウを作成したり、GetMessageでメッセージループを作成することはお勧めできません。 ワーカースレッドではなく、CWinThreadを用いてスレッドを作成すれば、Windows APIを直接呼ばずとも、新しいメッセージループを作成することができます。
GrapProgrammer

2017/05/31 12:06

ご指摘ありがとうございます CWinThreadでつくれば新しいメッセージループが作れるのですね。 AfxBeginThreadのことしか考えておりませんでした。 早速実験してみます、貴重なご指摘ありがとうございます
GrapProgrammer

2017/05/31 12:39

実験してみたのですが、CWinThreadのRunでDoModalしてもやっぱり止まってしまいました 新しいメッセージループ作ってもあんまり関係ないのでしょうか? もうひとつ実験でCWinThreadのInitInstanceでCFrameWndをCreateしてShowWindowしたら 固まらずに済みました。 ダイアログとウィンドウでも何か差があるんでしょうか? 解決済みにしたけど、またよくわらかなくなってきました。。。
GrapProgrammer

2017/05/31 12:44

一応、コードのせときます ↓これは固まります BOOL CTestThread::InitInstance() { SelfModal* pWnd = new SelfModal(); pWnd->DoModal(); return TRUE; } ↓これは固まりません BOOL CTestThread::InitInstance() { CFrameWnd* pWnd = new CFrameWnd; pWnd->Create(NULL, L"CWinThread Test"); pWnd->ShowWindow(SW_SHOW); pWnd->UpdateWindow(); m_pMainWnd = pWnd; return TRUE; }
Harahira

2017/05/31 13:06

DoModalで作成されるモーダルウィンドウは、親ウインドウの入力を無効にする必要があります。そうじゃないとモーダルではありませんから。 従って、親ウインドウに対してメッセージを投げる必要があります。 サブスレッドにメッセージキューがあっても、親ウインドウのメッセージキューがブロックされているので動作は止まります。 CFrameWndは、親ウインドウにメッセージを投げませんので、サブスレッドのメッセージキュー上で処理され、正常に動作します。
GrapProgrammer

2017/05/31 13:20

すみません、何でかわかりました SelfModalの親ウィンドウが固まってるメインウィンドウに勝手になっちゃってるからですね。 以下のように親ウィンドウを指定してやればうまくいきます、なんか妙にすっきりしました。 CFrameWnd* pWnd = new CFrameWnd; pWnd->Create(NULL, L"CWinThread Test"); SelfModal2 *dlg = new SelfModal2(CWnd::FromHandle(pWnd->GetSafeHwnd())); dlg->DoModal(); なんかわけわからない実装になりましたが・・・w
GrapProgrammer

2017/05/31 13:22

あ、すいません、すれ違いです >CFrameWndは、親ウインドウにメッセージを投げませんので、サブスレッドのメッセージキュー上で処理され、正常に動作します。  →これがダイアログとフレームウィンドウの違いですね、わかりました。
guest

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
hmmm

総合スコア818

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

GrapProgrammer

2017/05/31 10:02

ご回答ありがとうございます hmmmさん示していただいたコードを実験してみたところ、ひとつの解を導けました。 端的にいうとDoModalやCreateからのShowWindowではどうしてもメインスレッドとのメッセージやりとりが発生してしまうという事ですね。 Dialogクラスの提供機能を使わずに自分でウィンドウプロシージャから作成すると、メインスレッドの ウィンドウとはまったく関係ない独自のウィンドウが作れるんですね。(サテライトウィンドウともでいいますか) メッセージはスレッドごとに別管理になっているとこまではわかっていたのですが、メインスレッドとワーカースレッドで別々のウィンドウを作っているのに、何故相互作用してしまうのかが謎でした。
guest

0

MFC に限りませんが、UIはメインスレッドで処理して、時間のかかる処理をサブスレッドで処理するようにしないとアプリのみならずOS全体がぎくしゃくします。
Windows の設計思想に起因する根源的な問題なのでどうにもならない部分もあるのでまぁそういうものだと思うしかないでしょう。

なので、メインスレッド側(ユーザーのアクションで処理を行う最初のハンドラ)でモーダルダイアログを出して待機するようにします。

スレッド側には待機中を出すモーダルダイアログのウィンドウハンドルを渡して置き、処理が終わったら専用の処理終了メッセージを発行します。
待機ダイアログはユーザー操作で終了してしまわないように、OnOK, OnCancel の二つの仮想関数を空で用意しておきます(エンターキー、ESCキーの処理をブロックするための必須機能)。
そのうえで、専用の処理終了メッセージを受け取ったら、EndDialog(IDOK);など、なにかしら適当な終了コードを入れるようにしておきます。

あとは、待機ダイアログのWM_INITDIALOG(OnInitDialogで受けるのが望ましい)で、計算スレッドを起動してやれば、ぼさっと待ってるだけで全部段取りが終わります。

投稿2017/05/30 13:59

_Victorique__

総合スコア1392

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

GrapProgrammer

2017/05/30 14:09 編集

回答ありがとうございます。 仰っている内容は重々承知です。(マイクロソフトフォーラムでまったく同じ回答を先日みました) 解決策を求めているわけではなく、なぜこうなってしまうのかを知りたいので申し訳ありませんが この現象に至る理由を解説願えればと思います。
guest

0

うーん…もうかなり過去に学んだ話なのでうろ覚えですが。MFCもやってませんし。
ただ基本はMFCだろうとdelphi(他の会社が作ってるWindowsネイティブアプリの開発言語/環境)だろうと全部同じだったはずなので解説しますね。


メインスレッドにメッセージループがあるのは存じているでしょうか。
多分MFCプロジェクトを普通に作ると隠されていて見ることはできません。

アプリケーション実行中のすべてのウィンドウメッセージがこのメインループを通過します。
貴方が作るボタンクリック時のイベント等はこのメッセージループを経由しています。もしもマウスカーソルをアプリケーション上で動かすと、それらのイベントは全てこのメッセージループに集約されます。

Windowsではアプリケーションへの様々な命令を一旦このメッセージループに蓄積していくことで逐次処理しています。MFCでもそうだったと記憶していますが、Windows標準のアプリケーションは基本的にメッセージループでディスパッチャが働いています。
このような動作方法をデザインパターンでChain of Responsibilityパターンと言いますが、これには明確な欠点があります。連続する処理のいずれかが止まると後続の処理が行われなくなってしまうのです。

ウィンドウの移動などは全てこの「マウスがクリックされるメッセージ」、「ウィンドウを移動するメッセージ」というように分解されており、それがメッセージループから逐次ディスパッチされてそれぞれの処理が完了します。ということは、メッセージループが停止するだけでこの動作は完全に停止してしまいます。
「ウィンドウを表示する処理」も例外ではありません。

※調べなおしてたら勘違いな気がしてきたので括っておきます

で、ここからが本題です。 **サブスレッドからウィンドウを作っても新しくメッセージループは作成されません。** サブスレッド内の処理にはメッセージループがありませんよね? 別スレッドから画面を作成したとしても、Formの`DoModal`等の処理はメッセージループに依頼を投げた後、メッセージが処理されるのを待つことになります。下位のダイアログなどのクラスはそういった依頼を簡易化する処理だけで成り立っているはずです。このためにサブスレッドからウィンドウを表示しようとしても停止してしまいます。

hmmmさんのサンプルはこういった簡易処理に頼っていません。
CreateWindowShowWindowを使いブロックしない形で処理を続けた後、サブスレッド内で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
haru666

総合スコア1591

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

haru666

2017/05/31 03:15 編集

※コードだったので本文に移動
Chironian

2017/05/31 03:40

> 通常こういうことは許されず、悪いマナーとされていたと思います… 普通はやらない方が良いことに同意です。 しかし、特殊なケースでは使うことあります。 以前、理由は忘れたのですが、確かデバイス・チェンジ系のメッセージを受け取るためにサブスレッドで非可視なウィンドウを作ってメッセージ・ループを回したことあります。これはこれで正当な処理です。決して「偽」ではありませんし、メイン・スレッドのメッセージを横取りしているわけでもありません。 http://eternalwindows.jp/windevelop/message/message01.html
haru666

2017/05/31 04:38

あれ、こんな感じでしたっけ… 開発環境に頼りすぎてメッセージループ関連について勘違いしていたかもしれません。基本的にやらない、って学んできたのもありますが。 new FormをやるとMFCライブラリ…はどうかわかりませんが、delphiだとアプリケーション側のコードに組み込まれた気がします。 ただ、自前でCreateWindowを呼び出すことが無いため、これも勘違いかもしれませんが。
haru666

2017/05/31 05:03 編集

DoModalしたらParentWindow不在でModalじゃなくなってしまう問題は見つかりましたが、フリーズするっていうのは書かれてないですね… 何等かの形で別スレッドの親ウィンドウが決定されたりするとモーダルにする過程でchironianさんがいうような経路でフリーズ…もあるんですかね。。
Chironian

2017/05/31 05:24

確かにSelfModalクラスのコンストラクタとDoModal()の中身次第ですね。 これらの中でCreateWindow等を呼び出してウィントウを作っていたら、説明付かないです。 症状からして、SelfModalは既に生成されているウィンドウをモーダルにするためのクラスではないかなと推測してます。
haru666

2017/05/31 06:13

一番早いのはやっぱり重いオペレーションを丸ごとラップしてサブスレッド送りにすることですね… これの方が書き換え難しいって理由はそうないはずなんですが…。
haru666

2017/05/31 06:59

これ、近いですかね。 ttps://oshiete.goo.ne.jp/qa/4242370.html Harahiraさんの言うようにサブスレッド内でダイアログ作ってもメインスレッド側にメッセージ入るんでしょうね。
can110

2017/05/31 07:22

横から失礼します。 SelfModalは明示されていませんが、CDialog派生(=普通のダイアログ)クラスです。 そのDoModal内部でメイン側のダイアログにEnableWindow呼出=SendMessageするも、メイン側がwhileループしているので(メインのメッセージループまわらず)無限待ち状態になっている…のが原因です。 つまりHarahiraさんの回答のとおりです。
Chironian

2017/05/31 08:00

can110さん、ありがとうございます。 hmmmさんの追加から理解しました。恐らく、EnableWindowがWM_CANCELMODEをSendMessageしていてそこでメイン・スレッドが応答できないので、ハングアップですね。
can110

2017/05/31 08:52

そうですね。よって、ボタンのwhile(true){...}の中に while(Peek~){Translate,Dispatch~}のメッセージループを入れることによりメイン側のメッセージ処理されるので、ワーカー側でダイアログ表示されることが確認できます。
haru666

2017/06/01 02:26 編集

いくつか見て回った検索結果と一致しないことを考えると、バージョン違いもあるのかもしれませんね… DoModalがメッセージを送ることは経験上明らかだったんですが、固まらないって話も見かけたので首をかしげてました。
can110

2017/06/01 04:00

検索先ではボタンハンドラ関数から抜けるような記述をしていると思われます。 ハンドラ抜けるとメッセージループが回るので問題ありません。 今回はスレッド開始後にハンドラ内に無限ループがあるのでこのような現象になっています。
haru666

2017/06/01 05:39 編集

ああ、なるほど…確かにそうですね。ありがとうございます。
guest

0

こんにちは。

MFCは使ってないので知らないのですが、モーダル・ダイアログは通常内部でメッセージ・ループを回しています。(でないと戻ってきてしまいます。)
そして、GetMessageは、GetMessageを呼び出したスレッドのキューからメッセージを取ってきます
つまり、サブ・スレッドのキューからとるわけです。そのキューへメッセージを送るウィンドウは存在していませんので、単純に無限ループになる筈です。

次に、モーダル・ダイアログのウィンドウは、通常はメイン・スレッドが生成しますので、そのウィンドウ・メッセージはメイン・スレッドのキューへ届きます。
メイン・スレッドがメッセージ・ループを回ってないのでそのウィンドウ・メッセージを処理するスレッドがありません。

そして、サブ・スレッドは無限にメッセージ待ちしています。
結果、ハングアップということと思います。

スマートな対策はhmmmさんの回答のようにサブ・スレッドでウィンドウを生成して、それに対してメッセージ・ループで処理することです。
MFC経由でも同様な処理はできるかも知れませんが、ちょっと私にはわかりません。


【追記】

次に、モーダル・ダイアログのウィンドウは、通常はメイン・スレッドが生成しますので、そのウィンドウ・メッセージはメイン・スレッドのキューへ届きます。

通常はこの通りですが、ご提示されたソースでは、サブスレッドで生成しようとしているようですね。
そして、DoModal()が親ウィンドウ(たぶんメイン・ウィンドウ)へEnableWindow()しており、親ウィンドウはメイン・スレッドで生成しているから応答できず、そこでハングアップということのようです。

この仕組みから、モーダル・ダイアログを、その親ウィンドウと異なるスレッドで生成する場合はメイン・スレッドはメッセージ・ループを回っていないといけないと言うことと思います。

対策するなら、以下でできそうな印象です。

①メイン・スレッドの長時間処理に入るところで、EnableWindow(.., false);
②サブ・スレッドでモードレスダイアログを表示して進捗表示
③メイン・スレッドの長時間処理を抜ける(終了やキャンセル)したところで、EnableWindow(..., true);

実際にやったことはないので外していたらごめんなさい。

モードレスダイアログがちゃんとメイン・ウィンドウより上に表示できないかも知れません。
HWND_TOPMOSTとか使わないといけないかも?

投稿2017/05/30 16:56

編集2017/05/31 08:31
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

hmmm

2017/05/31 00:46

話がおかしいです。「サブ・スレッドのキューからとるわけです。そのキューへメッセージを送るウィンドウは存在していません」存在させるためにサブスレッドでDoModalでWindowを作成しようとしたけどハングアップするのはなぜ?メインスレッドのメッセージループがフリーズしているのとは関係ないよね?という質問です。ここでハングしているのはDoModal内でメインスレッド側のWindowをEnableWindowでFALSEにしようとしてNtUserCallHwndParamLockで固まっているいう話です。内部実装は分かりませんがメインのメッセージループ内かメッセージループが動作していないタイミングでEnableWindowに該当する処理を行う必要があるためなんじゃないかと思われます。
Chironian

2017/05/31 03:27

DoModalはウィンドウを表示するだけで、生成はしていないと推測しています。・・・① この推測が外れていたら外しているのですが。 ウィンドウを生成した(CreateWindowを呼び出した)スレッドのスレッド・キューへウィンドウ・メッセージはポストされます。 http://eternalwindows.jp/windevelop/message/message01.html DoModalがウィンドウを生成していない場合、サブ・スレッドのスレッド・キューへウィンドウ・メッセージを送るウィンドウは存在していないことになります。 NtUserCallHwndParamLockに関しては検索しても情報がほとんどないので私にはわかりません。 ただ、もしかすると、DoModalが別スレッドからの呼び出しをサポートしている可能性はありそうですね。 しかし、その場合でも、①が外れてなければダイアログへの全てのウィンドウ・メッセージはメイン・スレッドへ送られますから、メイン・スレッドでメッセージ・ループを回っていなければ全てのウィンドウ・メッセージが処理されないため、どこかでハングアップするでしょう。それがNtUserCallHwndParamLockなのかも知れません。
hmmm

2017/05/31 06:16

DoModalについて私の回答に追記しておきました。
Chironian

2017/05/31 08:01

「親ウィンドウ」に対してEnableWindowを投げているのですね。 親ウィンドウをメイン・スレッドが生成しているのであれば、EnableWindowにメイン・スレッドが応答せず、ハングアップは納得できます。 can110さんのコメントとHarahiraさんの回答から、どうやらそのように動いているようですね。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問