ワーカースレッドから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を使う
