2014年5月28日水曜日

GetWindowText のデッドロック

こんにちは。yu1rowです。

GetWindowText(SendMessage で WM_GETTEXT を使用した場合も同じ) でハマったので、その情報を備忘録として残します。
※回避コードだけ見るなら、一番下までスクロール。

マルチスレッドで EnumWindows で列挙した全 Window の文字列を GetWindowText やSendMessage を使って調べる処理を DLL に記述。
そしてこの DLL の関数を Excel VBA から呼び出すというケースで、スレッドがブロックしてしまっていつまでも終了しないことがある。

SendMessage を MSDN で確認すると、以下のように記述されていた。

複数のスレッド間で送信されたメッセージが処理されるのは、受信側スレッドがメッセージ取得コードを実行したときだけです。送信側スレッドは、受信側スレッドがメッセージの処理を終えるまで、ブロックされます(待機状態になります)。

しかし、日本語版に無い情報を英語版の MSDN で発見。
To prevent this, use SendMessageTimeout with SMTO_BLOCK set.For more information on nonqueued messages, see Nonqueued Messages.
MSDN に限ったことではないけれど、API リファレンスの英語版には日本語訳では見つけられない重要な文章がサラッと書かれている場合が見受けられる。英語版もたまには読んでみたほうがいい。

全ウィンドウの文字列を GetWindowText(SendMessage) で取得する処理を別スレッドで起動・終了待ちをする処理をExcel VBA で行うときに問題が発生した。
その別スレッドから Excel のウインドウ自体にも SendMessage することになるんだけど、そうするとずっと返ってこない。ブロックしちゃうってことらしい。

SendMessage でブロックされていてスレッドが終了しない場合、SendMessage の代わりにSendMessageTimeout を使用して、適切なタイムアウト時間を指定すれば良いとのこと。
以下コード例。
SendMessageTimeout(hWnd, WM_GETTEXT, 1024, (LPARAM)text, SMTO_BLOCK, 100, &dwRet);
  • 上記 100 となっている値は適切なタイムアウト時間を設定すること
  • SMTO_NORMAL、SMTO_BLOCK|、MTO_ABORTIFHUNG を必要に応じて組み合わせて使用すると良いと思う(オプションの詳細は MSDN 見てね)

ただし上記を使用すると、スレッド終了後の待機(WaitForSingleObject とか)でなんかちょっとだけ待機時間が発生して遅くなる。
上記の例であれば、「100msec×スレッド数」ぐらい?

これが気になる場合、「呼出元プロセスのウインドウに対して SendMessage するから止まる」 というデッドロックが元々の原因であるとすれば、上記の SendMessageTimeout を使用する前に、GetWindowThreadProcessId と GetCurrentProcessId を使用して呼出元プロセスのウインドウを回避するチェックを組み合わせるともっと良いかもしれない。
こうすると、ぅぉっなんか遅いってならない。ピュッピュッって終了してくれる。

以下上記を実装した例。呼出元プロセスのウィンドウは回避、ブロックしても 100ms で終わるようにしている。
DWORD pID;
GetWindowThreadProcessId(hWnd, &pID);
if (GetCurrentProcessId() == pID)
{
    return 0;
}

DWORD dwRet;
if (SendMessageTimeout(hWnd, WM_GETTEXT, 1024, (LPARAM)text, SMTO_BLOCK, 100, &dwRet) == 0)
{
    return 0;
}

ハマった。こんときゃ辛かった。

0 件のコメント:

コメントを投稿