ワーカースレッドから SendMessage で UI を更新したら、アプリがそのまま固まってしまった、という相談は珍しくありません。
この記事では、PostMessage と SendMessage の違いを、同期/非同期・戻り値・別スレッド呼び出し時のデッドロック発生条件の観点でまとめます。
最初に見る軸:「送ったあと、戻ってきていいか?」
Post と Send で迷ったら、まずここを切り分けます。
- 処理されたかどうかを待たずに先に進んでよい →
PostMessage - 処理された結果(戻り値)を受け取ってから次に進みたい →
SendMessage
この 1 行だけで 9 割は決まります。以降は、その挙動がコードの上でどう観測できるかと、別スレッドから呼んだときの落とし穴を順に確認していきます。
PostMessage:キューに積んで、すぐ戻る
PostMessage は、メッセージを送信先スレッドのメッセージキューに積んで、呼び出し元にすぐ戻ります。ウィンドウプロシージャ(WndProc)が呼ばれるのは、送信先スレッドがメッセージループで DispatchMessage するタイミングです。
- 戻り値は キューに積めたかどうか(
TRUE/FALSE)だけ。処理結果は取れない - メッセージが処理される時刻は、送信先スレッドの メッセージポンプに依存する
- 呼び出し元スレッドは相手の処理を待たないので、デッドロックは起こらない
ワーカースレッドから UI スレッドへ状態変更を伝える、バックグラウンド処理の完了通知、他ペインへの再描画要求など、「投げっぱなし」で問題ない通知系は PostMessage の出番です。
SendMessage:処理が終わるまで戻らない
SendMessage はキューを使いません。同じスレッドに送る場合は、WndProc を普通の関数呼び出しと同じ形で直接呼び、戻り値を持って返ります。
別のスレッドに送る場合は、送信先スレッドがメッセージを受け取って処理し終えるまで、呼び出し元をブロックします。
- 戻り値は
WndProcが返したLRESULT(処理結果)そのまま - 同一スレッドなら、関数呼び出しと時間的には変わらない
- 別スレッドなら、送信先が
GetMessageやPeekMessageを回していないとそこで止まる
「コントロールの現在値が欲しい」「WM_GETTEXT で文字列を取り出したい」のように戻り値が必要なときは SendMessage を使います。
比較表
| 観点 | PostMessage | SendMessage |
|---|---|---|
| 呼び出し元の挙動 | すぐに戻る(非同期) | 処理が終わるまで戻らない(同期) |
| メッセージキュー | 使う | 使わない(直接呼び出し/スレッド間は別経路) |
| 戻り値 | 投入成否のみ | WndProc が返した LRESULT |
| 同一スレッド | メッセージポンプを回すまで配送されない | その場で WndProc が走る |
| 別スレッド | ブロックしない | 送信先が処理するまでブロック |
| デッドロックのリスク | 基本的にない | 別スレッド呼び出しでは起こり得る |
別スレッドからの SendMessage でデッドロックになる条件
別スレッドへ SendMessage しただけで必ずデッドロックになるわけではありません。危ないのは、送信側が相手の処理完了を待っている最中に、受信側も送信側の完了や送信側が握っている資源を待つケースです。典型例は次の 2 パターンです。
- ワーカースレッドが UI スレッドに
SendMessageしている最中に、UI スレッドがWaitForSingleObjectなどで ワーカーの終了や結果を待っている - ワーカースレッドがロックを保持したまま UI スレッドへ
SendMessageし、受信側のハンドラが その同じロックを取りに行く
今回の再現サンプルは 2 つ目です。ワーカーが CCriticalSection を保持したまま UI へ SendMessage し、UI 側の OnWorkerSend が同じロックを取りに行くので、ワーカーは SendMessage から戻れず、UI もロック待ちで止まります。
デバッガで見ると、送信側であるワーカースレッドのコールスタックに
SendMessage系フレームが積まれ、受信側である UI スレッドはOnWorkerSendから同じロックを取りに行って待機します。つまり、SendMessageが見えるのは UI スレッド側ではなくワーカー側です。このサンプルでは Threads ウィンドウで見つけやすいように、実行時にArticle136 Worker/Article136 UIというスレッド名を付けています。

※ ワーカーから 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 を処理中

見るべき点は 3 つです。
PostMessageの「直前」と「直後」が 連続して並んでいる:WndProcが走る前に呼び出し元が先に進んでいるSendMessageの「直前」と「直後」の間にWndProcの出力が挟まっている:同期呼び出しである- 最後に初めて
WM_POSTの処理が出ている:キューに残っていたことがわかる
まとめ
- [ ] 判断軸は 「戻り値を待つか待たないか」。待たなくていいなら
PostMessage - [ ]
PostMessageはキューに積むだけで戻る。処理タイミングは送信先のメッセージポンプ次第 - [ ]
SendMessageは同期呼び出し。戻り値が取れる代わりに、別スレッド経由ではブロックする - [ ] ワーカー → UI は
PostMessageを基本にするだけで、典型的なデッドロックの大半は避けられる - [ ] 応答が必要で固まりたくないときは
SendMessageTimeoutを使う
