SendMessageTimeoutで応答待ちを制御するWindows API実装
Windows APIで別スレッドや別プロセスへメッセージを送るとき、通常のSendMessageは受信側の処理が終わるまで呼び出し元を待機させます。相手のウィンドウが停止していたり、WindowProcが長時間処理を実行していたりすると、送信側の画面まで固まったように見えることがあります。
SendMessageTimeoutは、この同期メッセージ送信に待ち時間の上限を設定する関数です。指定した時間内に処理が完了しなければ呼び出し元へ戻れるため、GUIの応答性を維持しながら、既存のウィンドウメッセージ機構を利用できます。
ただし、これは処理そのものを強制終了するAPIではありません。受信側のWindowProcが実行中でも送信側だけが戻る仕組みなので、タイムアウト後に相手が処理を継続している可能性を考慮した設計が必要です。
同期送信が停止を招く仕組み
SendMessageは、同一スレッド内であれば対象のWindowProcを直接呼び出すように動作し、別スレッドや別プロセスではメッセージを相手側のメッセージ処理へ届けます。いずれの場合も、通常は受信側の処理が完了して戻り値が返されるまで、呼び出し側は先へ進みません。
例えば、受信側がファイルI/O、ロック待ち、ネットワーク処理などを実行していると、送信元のUIスレッドも待機します。WindowProcの役割やメッセージループとの関係は、WindowProcの仕組みを確認すると整理しやすくなります。
SendMessageTimeoutでは、受信側の処理を指定ミリ秒まで待ち、完了すればその戻り値を受け取ります。時間を超えた場合は呼び出し元に制御が戻りますが、受信側の処理が安全に中断されるわけではありません。この点が、スレッドを停止させる関数や非同期メッセージ送信との大きな違いです。
関数の引数と戻り値
Unicode版の基本的な宣言は次のとおりです。
LRESULT SendMessageTimeoutW(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam,
UINT fuFlags,
UINT uTimeout,
PDWORD_PTR lpdwResult
);
hWndには送信先ウィンドウハンドル、Msgにはメッセージ識別子を指定します。wParamとlParamはメッセージ固有のデータです。fuFlagsで待機方法を選び、uTimeoutにタイムアウト時間をミリ秒で設定します。受信側のWindowProcが返した値は、lpdwResultが指す領域へ格納されます。
戻り値が0以外なら、メッセージ処理が時間内に完了したと判断できます。0の場合は、タイムアウト、対象ウィンドウの終了、アクセス制限、その他のAPIエラーを区別して調査します。必要に応じてGetLastErrorを使いますが、呼び出し方やフラグによって得られる情報が異なるため、戻り値だけで原因を断定しないことが重要です。
待機フラグの使い分け
最もよく使われるフラグはSMTO_ABORTIFHUNGです。受信側のスレッドが応答不能と判定された場合、待機を打ち切りやすくなります。単純な一定時間待機にはSMTO_NORMALを使えますが、相手が停止している可能性を考える処理では、異常時の復帰方針と併せて選択します。
| フラグ | 主な意味 | 利用時の注意 |
|---|---|---|
SMTO_NORMAL |
通常のタイムアウト付き送信 | 一般的な同期送信に使う |
SMTO_BLOCK |
呼び出し側の再入を抑制 | UIスレッドでは固まり方に注意する |
SMTO_ABORTIFHUNG |
応答不能な相手への待機を中止 | ハング判定は即時の死活確認ではない |
SMTO_ERRORONEXIT |
受信側スレッド終了時に失敗扱い | 終了とタイムアウトの扱いを分けたい場合に使う |
SMTO_NOTIMEOUTIFNOTHUNG |
ハングしていない間は時間制限を適用しない | 無制限に近い待機となる可能性がある |
SMTO_BLOCKは、呼び出し側が別のメッセージを処理して再入することを避けたい場合に意味があります。ただし、GUIスレッドで使うと入力や再描画への影響が大きくなる場合があります。一般的なデスクトップアプリケーションでは、短いタイムアウトとSMTO_ABORTIFHUNGの組み合わせを検討し、長い処理は別の設計へ切り分けます。
SMTO_NOTIMEOUTIFNOTHUNGは名前の印象だけで選ぶと危険です。相手がハングしていない間は指定時間を実質的な上限として扱わないため、受信側の処理が長引けば、送信側も長時間待ち続ける可能性があります。
安全な引数とメッセージ設計
別プロセスへ送るメッセージでは、lParamに自プロセス内のポインターをそのまま渡さないようにします。相手プロセスから見たアドレス空間は異なるため、構造体へのポインターや文字列バッファは無効なアドレスになります。システム定義メッセージにはプロセス間転送の仕組みが用意されているものもありますが、独自メッセージでは明示的なデータ交換が必要です。
WM_USER未満のシステムメッセージと、WM_USER以上のアプリケーション定義メッセージでは、プロセス間マーシャリングの扱いが異なります。独自データを渡すなら、共有メモリ、名前付きパイプ、COM、ソケットなどを使い、メッセージには識別子やハンドルだけを渡す方式が堅実です。TCPとUDPの用途を整理する場合は、ソケット通信の基礎も関連する判断材料になります。
さらに、別ユーザーのプロセスや権限の異なるプロセスへ送る場合は、UIPIやセキュリティ境界によってメッセージが拒否されることがあります。送信先が存在することと、送信可能であることは別です。失敗時に代替経路を用意し、外部入力を受け取るメッセージではデータ長や内容を必ず検証します。
実装時に確認する項目
次のような確認を先に行うと、タイムアウトを単なる応急処置で終わらせず、障害時の動作まで設計できます。
- 送信先の
HWNDが有効で、想定したスレッドに属しているか uTimeoutがUI操作に対して長すぎないか- タイムアウト後も受信側処理が続く前提でデータを管理しているか
lParamやwParamに渡す値がプロセス境界を越えて安全か- 戻り値、
GetLastError、ログを使って失敗理由を追跡できるか
タイムアウト値は、処理の平均時間ではなく、利用者が待てる上限と障害時の復旧時間から決めます。数秒の待機でも、UIスレッドから繰り返し呼ぶと操作感が悪化します。送信をワーカースレッドへ移す方法もありますが、その場合も共有状態の競合や、タイムアウト後に到着する結果の扱いを定義しなければなりません。
代替手段との選択基準
SendMessageTimeoutは、既存のWindowsメッセージプロトコルを変えずに、呼び出し側の待機を制限したい場面に向いています。受信側が短時間で処理を終え、戻り値をその場で必要とするなら、同期性を保てる点が便利です。一方、処理が長い、結果を後から受け取れる、通信相手が別サービスであるといった条件では、別の方式が適しています。
| 方法 | 待機特性 | 適した用途 |
|---|---|---|
SendMessageTimeout |
上限付きの同期送信 | 既存WindowProcへの短時間問い合わせ |
PostMessage |
基本的に非同期 | 結果を要求せず通知だけ行う処理 |
| ワーカースレッド | UIと待機を分離 | 時間の読めない処理 |
| 名前付きパイプやCOM | データ交換を明示化 | プロセス間の継続的な連携 |
| TCP・UDPソケット | アプリケーション間通信 | 別マシンや独立サービスとの接続 |
PostMessageへ置き換えれば送信側はすぐ戻れますが、処理完了や失敗を直接知ることはできません。結果が必要なら、完了通知用の別メッセージやイベントを設けます。SendMessageTimeoutでタイムアウトした後に、同じ要求を無条件で再送すると二重実行になるため、要求IDや冪等性も考慮します。
最終的には、メッセージが短い制御通知なのか、結果を伴うRPCのような呼び出しなのかを区別することが大切です。短時間の同期問い合わせにはタイムアウト付き送信、長時間処理には非同期通知や専用のIPCを割り当てると、責務が明確になります。
実装では、SMTO_ABORTIFHUNGなどのフラグ、現実的な待機時間、プロセス境界で安全なデータ形式、タイムアウト後の重複処理を一つの設計として扱い、まずUIスレッドを長時間拘束しない構成にすることが実践的な基準になります。