【MFC】PostMessage vs SendMessage の決定的違い

【MFC】PostMessage vs SendMessage の決定的違い

ワーカースレッドから SendMessage で UI を更新したら、アプリがそのまま固まってしまった、という相談は珍しくありません。

この記事では、PostMessage と SendMessage の違いを、同期/非同期・戻り値・別スレッド呼び出し時のデッドロック発生条件の観点でまとめます。


目次

最初に見る軸:「送ったあと、戻ってきていいか?」

Post と Send で迷ったら、まずここを切り分けます。

  • 処理されたかどうかを待たずに先に進んでよい → PostMessage
  • 処理された結果(戻り値)を受け取ってから次に進みたい → SendMessage

この 1 行だけで 9 割は決まります。以降は、その挙動がコードの上でどう観測できるかと、別スレッドから呼んだときの落とし穴を順に確認していきます。


PostMessage:キューに積んで、すぐ戻る

PostMessage は、メッセージを送信先スレッドのメッセージキューに積んで、呼び出し元にすぐ戻ります。ウィンドウプロシージャ(WndProc)が呼ばれるのは、送信先スレッドがメッセージループで DispatchMessage するタイミングです。

  • 戻り値は キューに積めたかどうかTRUE/FALSE)だけ。処理結果は取れない
  • メッセージが処理される時刻は、送信先スレッドの メッセージポンプに依存する
  • 呼び出し元スレッドは相手の処理を待たないので、デッドロックは起こらない

ワーカースレッドから UI スレッドへ状態変更を伝える、バックグラウンド処理の完了通知、他ペインへの再描画要求など、「投げっぱなし」で問題ない通知系PostMessage の出番です。


SendMessage:処理が終わるまで戻らない

SendMessage はキューを使いません。同じスレッドに送る場合は、WndProc を普通の関数呼び出しと同じ形で直接呼び、戻り値を持って返ります。

別のスレッドに送る場合は、送信先スレッドがメッセージを受け取って処理し終えるまで、呼び出し元をブロックします。

  • 戻り値は WndProc が返した LRESULT(処理結果)そのまま
  • 同一スレッドなら、関数呼び出しと時間的には変わらない
  • 別スレッドなら、送信先が GetMessagePeekMessage を回していないとそこで止まる

「コントロールの現在値が欲しい」「WM_GETTEXT で文字列を取り出したい」のように戻り値が必要なときは SendMessage を使います。


比較表

観点PostMessageSendMessage
呼び出し元の挙動すぐに戻る(非同期)処理が終わるまで戻らない(同期)
メッセージキュー使う使わない(直接呼び出し/スレッド間は別経路)
戻り値投入成否のみWndProc が返した LRESULT
同一スレッドメッセージポンプを回すまで配送されないその場で WndProc が走る
別スレッドブロックしない送信先が処理するまでブロック
デッドロックのリスク基本的にない別スレッド呼び出しでは起こり得る

別スレッドからの SendMessage でデッドロックになる条件

別スレッドへ SendMessage しただけで必ずデッドロックになるわけではありません。危ないのは、送信側が相手の処理完了を待っている最中に、受信側も送信側の完了や送信側が握っている資源を待つケースです。典型例は次の 2 パターンです。

  1. ワーカースレッドが UI スレッドに SendMessage している最中に、UI スレッドが WaitForSingleObject などで ワーカーの終了や結果を待っている
  2. ワーカースレッドがロックを保持したまま UI スレッドへ SendMessage し、受信側のハンドラが その同じロックを取りに行く

今回の再現サンプルは 2 つ目です。ワーカーが CCriticalSection を保持したまま UI へ SendMessage し、UI 側の OnWorkerSend が同じロックを取りに行くので、ワーカーは SendMessage から戻れず、UI もロック待ちで止まります。

デバッガで見ると、送信側であるワーカースレッドのコールスタックに SendMessage 系フレームが積まれ、受信側である UI スレッドOnWorkerSend から同じロックを取りに行って待機します。つまり、SendMessage が見えるのは UI スレッド側ではなくワーカー側です。このサンプルでは Threads ウィンドウで見つけやすいように、実行時に Article136 Worker / Article136 UI というスレッド名を付けています。

Visual Studio の呼び出し履歴で Article136 Worker スレッドを選択し user32 SendMessageW と NtUserMessageCall が見えている画面

※ ワーカーから UI へは PostMessage を基本にするだけで、このパターンの 8 割は消えます。戻り値がどうしても必要なときだけ SendMessageTimeout にタイムアウトを付けて逃げ道を用意します。


使い分けの定番パターン

  • ワーカー → UI の進捗通知PostMessage(hUI, WM_APP + 1, ...)。UI 側は ON_MESSAGE で受ける
  • UI → コントロールの状態取得SendMessage(hCtrl, WM_GETTEXT, ...)。同一スレッドなので安全
  • 別プロセスへのデータ送信SendMessage(hTarget, WM_COPYDATA, ...)。相手が応答するまで同期待ちする仕様
  • 応答は欲しいが固まりたくないSendMessageTimeout(..., SMTO_ABORTIFHUNG, 1000, ...) でタイムアウト付きに切り替える

今回の再現サンプル

今回は、MFC アプリケーションウィザードに近いダイアログベース構成の検証アプリを使います。比較実行 で同一 UI スレッド内の順序差、デッドロック再現 でワーカー → UI の相互待ちを観察できます。

static const UINT WM_ARTICLE136_POST = WM_APP + 1;
static const UINT WM_ARTICLE136_SEND = WM_APP + 2;

class CPostMessageVsSendMessageDeadlockDlg : public CDialogEx
{
    afx_msg void OnBnClickedBtnRunCompare()
    {
        AppendLog(L"[UI] PostMessage 直前");
        PostMessage(WM_ARTICLE136_POST, 100, 0);
        AppendLog(L"[UI] PostMessage 直後 -- すぐに戻る");

        AppendLog(L"[UI] SendMessage 直前");
        LRESULT result = SendMessage(WM_ARTICLE136_SEND, 200, 0);

        CString line;
        line.Format(L"[UI] SendMessage 直後 -- 戻り値=%lld", (long long)result);
        AppendLog(line);
    }

    afx_msg LRESULT OnArticlePost(WPARAM wParam, LPARAM)
    {
        AppendLog(L"  [WndProc] WM_POST  を処理中");
        return 0;
    }

    afx_msg LRESULT OnArticleSend(WPARAM wParam, LPARAM)
    {
        AppendLog(L"  [WndProc] WM_SEND  を処理中");
        return 42;
    }
};

ログビューを見ると、次の順で並びます。

[UI] PostMessage 直前
[UI] PostMessage 直後 -- すぐに戻る
[UI] SendMessage 直前
  [WndProc] WM_SEND  を処理中
[UI] SendMessage 直後 -- 戻り値=42
  [WndProc] WM_POST  を処理中
MFC テストアプリのログビュー。PostMessage は WndProc を通さずに戻り、SendMessage は WndProc を挟んで戻り、PostMessage 分は比較ハンドラ終了後に遅れて配送されている

見るべき点は 3 つです。

  • PostMessage の「直前」と「直後」が 連続して並んでいるWndProc が走る前に呼び出し元が先に進んでいる
  • SendMessage の「直前」と「直後」の間に WndProc の出力が挟まっている:同期呼び出しである
  • 最後に初めて WM_POST の処理が出ている:キューに残っていたことがわかる

まとめ

  • [ ] 判断軸は 「戻り値を待つか待たないか」。待たなくていいなら PostMessage
  • [ ] PostMessageキューに積むだけで戻る。処理タイミングは送信先のメッセージポンプ次第
  • [ ] SendMessage同期呼び出し。戻り値が取れる代わりに、別スレッド経由ではブロックする
  • [ ] ワーカー → UI は PostMessage を基本にするだけで、典型的なデッドロックの大半は避けられる
  • [ ] 応答が必要で固まりたくないときは SendMessageTimeout を使う
目次