明らかに英語の意思疎通が私の能力では不可能/回答を得るのは困難ですのでこちらで再
度質問させて下さい。( http://stackoverflow.com/questions/22250904/wndproc-is-
called-from-in-shellexecuteex-fifth)
よろしくおねがいします。
ShellExecuteExからなぜかWndProcが呼ばれます。しかも何故かShellExecuteExが5回目
の時に限ってです。
これはShellExecuteExのバグなのでしょうか?それとも私のコードのバグなのでしょうか?
どうしても私だけでは判断がつかなかったため、こちらで質問させてもらいました。
ソース: https://code.google.com/p/jpgcutter/
MainDlg.h先頭の #define ENABLE_SHELLEXECUTEBUG を定義(コメントアウト)してください
再現環境:
Windows 8.1 x64 + VC2010
再現方法:
適当にjpgを20個ほどすばやく2回放り込んでください。(jpgは改変されます。支障の
ないファイルを使用してください)
現象:
OnIdleの5回めのShellExecuteExの呼び出し中にさらにWndProcのタイマーなどの処理が
呼ばれます。またその中のShellExecuteEx5回めで更にWndProcが呼ばれます。さら
に・・・(10回ほどループ)
デバッグでコンパイルしてTRACEを確認すると確認できます。
理想的には
+OnIdle
-OnIdle
+WM_TIMER
-WM_TIMER
...(ループ)
なのですが、
実際には・・・
+OnIdle1
+ WM_TIMER(スレッドID同じ)
+ WM_TIMER(スレッドID同じ)
+ WM_TIMER(スレッドID同じ)
- WM_TIMER
- WM_TIMER
- WM_TIMER
-OnIdle1
といった感じになってしまいます。
同一スレッドのためMutexもCriticalSectionもセマフォも効かないため、グローバル変数
をカウンタにすることで回避しました。回避はできたのですが、なぜこのようなことが起
こるのか大変モヤモヤしています。
何かわかる方がいたらぜひ教えてください。よろしくお願いいたします。
汚いソースですみません。少しでも足しになるように簡単にプログラムの説明をしておき
ます。
・jpgをjpegtran.exeで処理して、サイズを縮小するプログラムです。ファイルがドロッ
プされると処理開始します。
・処理(jpegtran.exe)は最大10個同時に動きます。起動はShellExecuteExを使って行われ
ます。
・処理はOnTimer(50ns)、またはOnIdleハンドラから呼び出されます。ConvProc()です。
・通常はCMessageLoopのRunからOnTimer, OnIdleともに呼び出されます。1スレッドなの
でOnIdleの終わる前にOnTimerが呼び出されるなんてことは無い・・・と理解していたの
ですが、呼び出されているようですorz
・TRACEを見ていると、ConvProc: グローバル変数カウンタ値, ThreadId=0x%08X、で同じ
スレッドIDのOnTimerがOnIdle途中(150行の 7で止まる)から呼び出されていくのがわ
かるかと思います。
再補足です。
最初はOutPutDebugStringの出力が順序逆転してるのかなというのも疑いました。しかし
177行のグローバル変数の値が1~10まで上昇していくので、それは無いと判断しました。
うーーん、
回避方法は分かりませんが、
> 適当にjpgを20個ほどすばやく2回放り込んでください。
> (jpgは改変されます。支障のないファイルを使用してください)
これは、異常なソフトの使い方なのでは?
2回放り込まれても2回目は無視するとかしないとまずいと思います。
ファイル20個も多いのでは?
まず、
クライアントの要求とtamachanさんのソフトの許容範囲を明確にしたほうがいいと思
います。
えーと・・・。2回放り込んでも大丈夫なように書きました。
その後いろいろ動作テストして、1回で10個ほどのjpgを放り込むだけでも起こりま
す。テストしてないですがたぶん6個以上なら起こるんじゃないかと(ShellExecuteEx5
回目のコールがおかしいので)
ShellExecuteEx は、DDE 通信などのために内部でメッセージループが回ります。
スレッドの設計に関わらず。Windowsでは以下の要請があります。
1.ウインドウプロシージャでのメッセージに応答する関数群は
リエントラントに設計されていなければならない。
2.又は、再入禁止措置が必要。
基本中の基本なのですが、この内容は大丈夫ですか。
ebitaiさん、中澤さんアドバイスありがとうございました。
>ShellExecuteEx は、DDE 通信などのために内部でメッセージループが回ります。
なるほど。DDEは気づきませんでしたorz。
>1.ウインドウプロシージャでのメッセージに応答する関数群は
> リエントラントに設計されていなければならない。
これは本当ですか?できればMSDNかなにかの情報源を貼ってもらえると嬉しいです。
ということは、中澤さんは全メッセージハンドラに下記のようなコードを書いていらっ
しゃるということなんでしょうか?(下記コードだと同一スレッドから複数呼び出された
場合、一部メッセージが破棄されるので不完全ですが・・・)
int g_count = 0;//Global変数。同一スレッドから呼び出された時のため。
CMainDlg{
CCriticalSection m_cs; //メンバ変数
OnHandler(/* ... */)
{
CCritSecLock lock(&m_cs);//ロック。別スレッドから呼び出された時のため
if(g_count==0)
{
g_count++
...
g_count--;
}
}
}
仲澤さんすみません。お名前の仲が中になっていました。訂正とともにお詫び申し上げます。
よく考えたら上のコードだめですね。同一スレッドから再呼び出しされたらクリティカル
ロックが再発行されて、もしかしたら上書きまでされちゃうかも・・・
同一スレッド/別スレッド、両方からロックするって以外に難しい・・・
度々横から失礼しますが、
> 中澤さんは全メッセージハンドラに下記のようなコードを書いていらっ
しゃるということなんでしょうか?
仲澤@失業者さんは
「ウインドウプロシージャは再入可能なように設計しなければならない。
どうしても再入されたくないなら自分で再入禁止措置をとらなければならない」
と言っている訳で、再入禁止措置をやらなければならないのは再入を禁止したいあなただ
けですよ。
ウインドウプロシージャは再入可能なように設計するのが普通で、
あなたが特異な要求をしているだけです。
まあ、ウィンドウプロシージャを再入禁止にするより、
::ShellExecuteEx() の代わりに ::CreateProcess() を使えば良さそうですけどね。
forty-fiveさんアドバイスありがとうございました。
> あなたが特異な要求をしているだけです。
なんとなくですが・・・書いてること矛盾してません?
というのは現実的に、再入可能なように設計する ≒ 再入禁止措置 なような気がする
んです。
ごくごく一部の単純な、すごーく短いハンドラならともかく・・・普通にソフト作る場合
のほとんどのハンドラは再入禁止措置しか私には解が思いつきません・・・orz
>::ShellExecuteEx() の代わりに ::CreateProcess() を使えば良さそうですけどね。
前にコマンドライン引数の最大文字数の制限で、深層の日本語名ファイルが失敗するなん
てことがあって、できればDDE使いたい/引数の引数がLPTSTR(constが無い!!)という2
つのなんとな~くな理由でShellExecuteExを使ってました。
原因がわかって解決策を見つけられたため解決チェックしました。
数々の有り難いアドバイスいただき、大変有難うございました。
解決策案
1. ShellExecuteExをCreateProccess/Exで置き換える
2. 別スレッド(ShellExecuteEx起動専用)に処理を置き換える(ファイルリストを渡し
たり、GUIへ完了通知したりするのが非常に面倒そうですが・・・orz)
3. jpegtran.exeを結合してしまう。関数化する。(ShellExecuteExを使わない)
全ての関数がリエントラントであることが要求されるわけではありません。
特にcallbackといわれる、システムから呼ばれる関数は、リエントラント性が
強く要求されます。
最近はPCが早くなったので、ほとんど問題になることはありませんが、
Windows2.1 Windows 3.0、3.1のSDKには、このことが明記されていました。
現在この件についてのM$さんのズバリといった公式な文書は見つかりませんでしたが、
原理的に簡単に類推できます。
最も想像しやすい例としては、ウインドウコールバックで、何らかの
メッセージを処理中に、自身にSendMessage()する例が挙げられます。
想像しにくい、但し厳然とした事実としては、WM_PAINTメッセージの
処理中に、BeginPaint()を実行すると(普通そうなりますし、MFCも
そうなってますが)、BeginPaint()から制御が「戻る前」にWM_ERASEBKGND
メッセージが、自身に届きます。
また、このことは、BeginPaint()のマニュアルに明記されています。
興味があったら試してみてください。