UIスレッド間でCMFCPopupMenu::m_pActivePopupMenuに同時アクセス

[戻る]
NOR 2011/08/02(火) 10:57:25
現在、VC2008で、UIスレッド(ためしにモードレスダイアログ)を別途持った
SDIアプリケーションを実装できるか調査しています。
終了時の処理などは後回しとして、動き自体はとりあえずできているのですが、

・フレームウィンドウ側でメニューを表示
・そのままメニューを消さずにUIスレッド側のダイアログをクリック

とすると、ASSERTが発生してしまいます。
コールスタックで確認してみたところ、

CDialogEx::PreTranslateMessage()
└CDialogImpl::PreTranslateMessage
 └CDialogImpl::ProcessMouseClick()

の中に

if (!CMFCToolBar::IsCustomizeMode() && CMFCPopupMenu::m_pActivePopupMenu != NULL
&& ::IsWindow(CMFCPopupMenu::m_pActivePopupMenu->m_hWnd)) …(1)
{
  CMFCPopupMenu::MENUAREA_TYPE clickArea =
CMFCPopupMenu::m_pActivePopupMenu->CheckArea(pt); …(2)

という箇所があり、(2)の箇所で
CMFCPopupMenu::m_pActivePopupMenuがNULLになっていました。
(1)の箇所を通過しているということは、その時点ではNULLではなかったはずです。

CMFCPopupMenu::m_pActivePopupMenu自体は、staticメンバ変数で、
その名の通り「現在表示しているメニュー」だと思われ、
CFrameWndEx::OnClosePopupMenu()の中でNULLに戻されていました。

そうなるとこの変数は、複数のスレッドから同時にアクセスされることを
全く想定していないということになってしまうのでしょうか。

また、上記の自分の推測が間違っていないとすると、
2008以降のMFC Feature Packは、
UIスレッドを作成することは不可能ということになるのでしょうか?

どなたかこのへんの調査をされたり
実際にUIスレッドを作成されたかたはいらっしゃいませんでしょうか。

ITO 2011/08/02(火) 19:37:06
うーん、
UIスレッド側のHWNDとフレームウィンドウ側のHWNDは別にないますよね。
現在、UI側のダイアログがクリックされているということは、HWNDはUI側ですね。
 しかしフレームウィンドウ側のメニューの項目はUIスレッド側のHWNDのリソースから
検索しようとしているため、見つからないはずですよね。
そのため、検索不能なので
> CMFCPopupMenu::m_pActivePopupMenuがNULLになっていました。
という結果になるのでは?

と思うのですがいかがでしょうか?
 おそらく、フレームウィンドウ側のHWNDでUI側のダイアログを作成すると、そこで
HWNDの関連付けができるのでリソースも検索できるようにならないか(?)と思います
がどうでしょうか。

AR 2011/08/03(水) 01:42:33
 他にもUIまわりの静的変数は多々あると思いますが、基本的に並列操作できないもの
が該当しているように思われます。
 (Feature Pack以降はアプリケーション全体の設定という意味合いの物も少なくあり
ませんが)

 今回のm_pActivePopupMenuの場合は、UI操作のマウスなりキーボードなりで、ポップ
アップメニューを出したまま他の操作を行うなんて事はできませんよね?
 よって、並列操作されることも無かろう・・・という設計思想なんじゃないかと推測
しております。

 ですから今回の事例ではCMFCPopupMenuではなく、CreatePopupMenuなどを使い、自前
で制御機構を実装すべきだと思います。

PATIO 2011/08/03(水) 13:09:29
私自身の考えとしては、ユーザーが同時に二つのウインドウを操作することは
無いと考えているのでUIスレッドを使ったマルチスレッドは殆ど使わないと
考えています。殆どの場合、UIスレッドがメインスレッドになり、
ワーカースレッドをサブスレッドにする構成で事足りると考えています。

ウインドウハンドルが必要になるからUIスレッドをと言うケースがあるかも
とは思うのですが、
実際の話、UIスレッドを二つ以上使ってマルチスレッドを実装するケースって
あるんでしょうか。

実験的な意味では試してみても良いとは思うんですが、
実際には実装が面倒になる割にはメリットが少ないような気がしています。

ryo 2011/08/03(水) 13:24:22
発生条件がよくわからないので、以下のように試してみました

●環境
Win7 VS2008sp1

●プロジェクト条件
MFCアプリケーション
SDI (ドキュメント/ビューあり) Unicode
MFC:(スタティックor共有) 

メニュー バーとツール バーを使用する

●ダイアログ
・CMainFrameのメンバ変数として管理するモードレスダイアログ
 継承元はCDialog or CDialogEx で2種(*1)
 ダイアログの親はCMainFrame(this)

・CWinAppExに、CWinThread派生のスレッドを持たせる
 (スレッド生成はnew→CreateThread(0)の2行)
 このスレッドのメンバ変数として管理するモードレスダイアログ
 こちらも継承元はCDialog or CDialogEx の2種
 ダイアログの親はNULL指定

以上、計4つのダイアログ

●メニュー
メインのウィンドウのメインメニュー or 右クリックポップアップメニュー
の2つ


作業内容
1:メニューを表示。
2:メニューの項目を選択せず、
  そのまま、マウスカーソルをダイアログに移動しクリック
3:フォーカス(アクティブウィンドウ)がダイアログにうつることで、
    メニューを自消滅させる

以上の動作を、試した結果
メニュー・ダイアログのどの組み合わせでも、ASSERTは発生しませんでした。
#(*1)の場合、クライアント領域へのクリックではすぐにはメニューは消えなかった
(ASSERTはしない)


NORさんが行った作業(最小限でASSERTが再現できるもの)は
どんな内容だったのでしょうか?

NOR 2011/08/03(水) 14:49:29
みなさん情報ありがとうございます。
ご意見もじっくり参考にさせていただきますが、
まずは以下に、問題が発生する最低限の記述を書かせていただきます。

なお、自分の環境はXPなのですが、
ryoさんの情報を元に、Windows7の上で実行してみたところ、
こちらはASSERTが発生しませんでした。
なぜ発生しないのかまでは調査しきれていません。

環境:XP VC2008
プロジェクト名:Test
アプリケーションの種類:SDI(ドキュメント/ビューあり)
プロジェクト形式:MFC標準
あとはデフォルトのまま

リソースビューで「ダイアログを挿入」し、IDD_TEST_DIALOGに変更し、
クラスの追加でCTestDialogを作成(基本クラスはあとからCDialogExに変更)

以下は最低限の記述のみに変更したソースです
(コンストラクタやデストラクタなども消していて、
 OKボタンやキャンセルボタンなどの対応もしていません)

TestDialog.h
----------------------------------------
#pragma once

class CTestDialog : public CDialogEx
{
  DECLARE_DYNAMIC(CTestDialog)

public:
  enum { IDD = IDD_TEST_DIALOG };
  BOOL Create(CWnd* pParentWnd) { return CDialogEx::Create(IDD, pParentWnd); }
};
----------------------------------------

TestDialog.cpp
----------------------------------------
#include "stdafx.h"
#include "Test.h"
#include "TestDialog.h"

IMPLEMENT_DYNAMIC(CTestDialog, CDialogEx)
----------------------------------------

CWinThread派生クラスCTestThreadを作成し、
CTestThread::InitInstance()に以下の2行を追加
(m_dlgTestはCTestDialog型のメンバ)

  m_dlgTest.Create(NULL);
  m_dlgTest.ShowWindow(SW_SHOWNA);

CMainFrame::OnCreate()の最後に以下の1行を追加

  AfxBeginThread(RUNTIME_CLASS(CTestThread));

ビルドして実行し、後ろに隠れているモードレスダイアログをどこかに移し、
CMainFrame側のメニューを出しっぱなしの状態で
モードレスダイアログをクリックすると、やはりASSERTが発生します。

ryo 2011/08/04(木) 13:17:45
XP(x86)+VS2008(sp1)で試してみたのですがやはり発生しませんでした。

ITO 2011/08/04(木) 14:10:10
ryoさんの示した作業内容の3番目をやってないですね。

>作業内容
>1:メニューを表示。
>2:メニューの項目を選択せず、
>  そのまま、マウスカーソルをダイアログに移動しクリック
------- ココカラ ---------
>3:フォーカス(アクティブウィンドウ)がダイアログにうつることで、
>    メニューを自消滅させる
------- ココマデ ---------

ryo 2011/08/04(木) 21:30:42
>ITOさんへ
その3番は(1〜3全部)、ビルドして完成したものに対して、
操作・実行するテスト項目です


・・と書いていてふと疑問にぶつかった

「CMFCPopupMenu」や「CMainFrame側のメニューを出しっぱなしの状態で」
という単語から、NORさんが問題としているメニューの表示というは
___________________
|ファイル(F) 編集(E)・・・・
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
のことではなく、
___________________
|ファイル(F) 編集(E)・・・・
|保存   | ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
|名前をつけ|
 ̄ ̄ ̄ ̄ ̄ ̄ ̄
と、ポップアップメニューが表示されている状態のことだと思います
この状態で、他のダイアログなどをいじれば、ポップアップ分が消えるはずだが
なぜかASSERTしてしまう

と、私は解釈したのですが、NORさん あってますでしょうか?

NOR 2011/08/04(木) 21:58:08
> ___________________
> |ファイル(F) 編集(E)・・・・
> |保存   | ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
> |名前をつけ|
>  ̄ ̄ ̄ ̄ ̄ ̄ ̄
> と、ポップアップメニューが表示されている状態のことだと思います
> この状態で、他のダイアログなどをいじれば、ポップアップ分が消えるはずだが
> なぜかASSERTしてしまう
> 
> と、私は解釈したのですが、NORさん あってますでしょうか?

そのとおりです。
こちらの説明がわかりにくかったようで大変失礼しました。
イラスト付きで問題を解説していただき、ありがとうございます。

そのように理解していただいた上で、ryoさんの環境では、
XPでも再現しないということになりますでしょうか?

ryo 2011/08/05(金) 09:40:25
>そのように理解していただいた上で、ryoさんの環境では、
>XPでも再現しないということになりますでしょうか?
はい。

仲澤@失業者 2011/08/08(月) 11:08:27
前提を確認する必要があるかもしれません。
一般にVS2005以降においては、ウイザードを使うとCMFCMenuBarクラスを
使用するようにコードされると思いますが、どうなってますでしょうか。
この場合、メインフレームのいわゆるメニューは一時的なオブジェクトに
なっており、表示されているメニューの実体はCMFCMenuBarクラスの
派生クラスが保持しているはずです。

何をしようとしているのかいまいち不明ですが、メインでないUIスレッドが、
メインのメニューをどうこうしたいのであれば、対象とすべきなのは、
実体のあるCMFCMenuBarクラスの派生クラスにすべきなのかもしれません。
もっとも、一般的に言うのであれば、下位の処理であるUIスレッドが、
メインメニューにアクセスしようとすること自体が、やや変な設計と言えるかも
しれません(vv;)。

ITO 2011/08/08(月) 11:47:32
>>ITOさんへ
> その3番は(1〜3全部)、ビルドして完成したものに対して、
> 操作・実行するテスト項目です
了解です。

NOR 2011/08/08(月) 11:49:47
> もっとも、一般的に言うのであれば、下位の処理であるUIスレッドが、
> メインメニューにアクセスしようとすること自体が、やや変な設計と言えるかも
> しれません(vv;)。

MFCのソースコードを見た限りの理解ですが、最初の書き込みの通り、
CDialogExのマウス処理(CDialogImpl::ProcessMouseClick())が、
ポップアップ表示中のCMFCPopupMenuにアクセスに行っているのであって、
自分自身で意図的にメインメニューにアクセスしているわけではありません。

なお、問題が発生するテストプログラムのプロジェクトを
以下の場所にアップロードしてみました。
どなたかお時間があれば、これを直接ビルドして
自分と同じ問題が発生するか試してみていただけませんでしょうか。
http://firestorage.jp/download/0a662321f3729db7b2a65205d13b22f6e88d1abb

「2011/08/03(水) 14:49:29」の書き込みのときに作成したもので、
VS2008が入っているXPマシン3台で試してみましたが、同じように発生しました。

仲澤@失業者 2011/08/08(月) 15:11:57
失礼しました。m(__)m
確かに、子スレッドのPreTranslateMessage()で、
メインフレームのAssertValid()が呼ばれちゃってますね(vv;)。
原因はCDialogExクラスのPreTranslateMessage()で、不用意に
CDialogImpl::PreTranslateMessage()を呼んでいるせいで、
困ったもんです。
従って、このクラスはUIスレッドのターゲット
(=メイン)ウインドウオブジェクトととしては使用できません。
あきらめて、CDialogクラスを使いましょう。

ryo 2011/08/08(月) 15:46:52
>CTestThread::InitInstance()に以下の2行を追加
>(m_dlgTestはCTestDialog型のメンバ)
>  m_dlgTest.Create(NULL);
>  m_dlgTest.ShowWindow(SW_SHOWNA);

このCreateを、スレッドのコンストラクタで行ってみたらどうなりますか?

NOR 2011/08/08(月) 19:26:51
> このCreateを、スレッドのコンストラクタで行ってみたらどうなりますか?

ありがとうございます。
コンストラクタとなぜかASSERTが発生しないようです。

恥ずかしながら原理がわからないのですが、
もしよろしければ、なぜここだと大丈夫なのか教えていただけませんでしょうか。

NOR 2011/08/22(月) 15:15:33
>> このCreateを、スレッドのコンストラクタで行ってみたらどうなりますか?
> ありがとうございます。
> コンストラクタとなぜかASSERTが発生しないようです。

テストしている最中に気づいたのですが、
上記の方法で作成したダイアログは、メインスレッド側に属するようで、
CMainFrameにSleep()などを入れてみたところ、
ダイアログも操作できずに固まってしまいました。

これだとスレッド化したことにはならないのではと思われるのですが、
ryoさんの環境で発生しないというのは、この作り方だったということでしょうか?
それとも、CTestThread::InitInstance()内に書いて
UIスレッドからダイアログを表示しても、やはり発生しないのでしょうか?

> 確かに、子スレッドのPreTranslateMessage()で、
> メインフレームのAssertValid()が呼ばれちゃってますね(vv;)。

仲澤@失業者さんの環境では、発生したということですね。
試していただきありがとうございます。

たしかに今回のテストプログラムに限れば
CDialogExではなくCDialogだと大丈夫なようですが、MFCのソース内を見ると
m_pActivePopupMenuを参照しているところが他にもいろいろあり、
そもそもUIスレッドを全く想定していないライブラリなのであれば、
早めに思いとどまって別の方法を探るべきなのかなとも思っています。

特に、フレームウィンドウを持ったUIスレッドはVC6の頃に作成したものがあり、
これを移植できるかということも調査している状況なのです。

なお、自分でもいろいろ調査を続けているのですが、
元となったBCGSoftの更新情報に、以下のような記述がありました。

http://www.bcgsoft.com/bcgcontrolbarpro-versions.htm
CBCGPPopupMenu has a new method: GetSafeActivePopupMenu - returns the active
popup menu in the current UI thread.

これってやはり、BCGSoft自身のライブラリも、
つい最近まではUIスレッドを想定していなかったということなのでしょうか。

ITO 2011/08/22(月) 19:01:46
> 恥ずかしながら原理がわからないのですが、
> もしよろしければ、なぜここだと大丈夫なのか教えていただけませんでしょうか。

 今回のNORさんのテストプログラムの親ウインドウはCMainFlameですよね。
 CTestDialogのウインドウはCMainFlameで起動してますよね。
 だとしたら、スレッドもCMainFlameで起動すべきだと思います。
 なぜ、スレッドのコンストラクタでよかったというと、

>CMainFrame::OnCreate()の最後に以下の1行を追加
>  AfxBeginThread(RUNTIME_CLASS(CTestThread));

 これをしていたからだと思います。

NOR 2011/08/25(木) 15:08:29
> なぜ、スレッドのコンストラクタでよかったというと、
>
>>CMainFrame::OnCreate()の最後に以下の1行を追加
>>  AfxBeginThread(RUNTIME_CLASS(CTestThread));
>
> これをしていたからだと思います。

ですから、スレッドのコンストラクタで作成したCTestDialogは、
メインスレッド側に属してしまいますよね?
それではUIスレッドの機能にはなっていないのです。

なお、マイクロソフトのフォーラムに、
同じようにUIスレッド絡みの問題に対する書き込みを見つけたのですが、
マイクロソフト自身が「今のMFCは複数のUIスレッドを想定していない」
と言ってしまっていました。
http://connect.microsoft.com/VisualStudio/feedback/details/523719/application-deadlock-toolbar-paint-failure-traces-error-cant-draw-toolbar

書き込んだ外人も「退化だろう」みたいに言われているのですが、
マイクロソフトの返答は2010/01/11時点のものなので、
VS2010でも対応されているものではないですよね。

ITO 2011/08/25(木) 15:38:45
> ですから、スレッドのコンストラクタで作成したCTestDialogは、
> メインスレッド側に属してしまいますよね?

うーーん、
多分UNIXもそうだと思いますが、(調べてないです(^^;))
 一つのウインドウが複数のウインドウを管理するようになっていないと、
うまくいかないと思います。

ryo 2011/08/25(木) 19:18:13
MSDNによると、UIスレッドをつくるときは
CWinThread::InitInstanceで生成しろとありますが
なぜ?なのか説明が見つけられませんでした。
MSDNのサンプルと同じように、CWinThread::m_pMainWndに入れてみても
特に変化が見られませんでした


>ryoさんの環境で発生しないというのは、この作り方だったということでしょうか?
このつくり方も試していました
この場合、メインのウィンドウににSleepいれたら、止まっちゃいました。

>それとも、CTestThread::InitInstance()内に書いて
>UIスレッドからダイアログを表示しても、やはり発生しないのでしょうか?
CWinThread::Run()を、オーバーライドし、
その中で自作でメッセージループで待つようにすれば、InitInstanceで生成しても
メニュー表示→ダイアログ のエラーが出ないようです。
♯普段、これを多用してるので

CWinThread::PumpMessageをオーバーライドでもいけるかもしません

NOR 2011/09/20(火) 09:31:05
返答が遅れてしまい申し訳ありません。
みなさんの方法を試してもうまくいかず、期限などの問題もあったため、
インシデントを1つ使う覚悟でマイクロソフトに問い合わせてみたところ、
上記のフォーラムと同じく、「今のMFCはUIスレッドには対応していない」
「将来のバージョンでは修正を検討する」とのことでした。

なお、同じようにUIスレッドの問題に
ハマってしまった日本人のかたもいらっしゃいました。
http://d.hatena.ne.jp/killswitch5375/20110818/p1

#納品間際に発覚してしまいました。
#昔のMFCのやりかたが通用しなくなっているのは厳しいですね。

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