「処理中にアプリが白くなって(応答なし)と出る」「たまに固まるという問い合わせが来るが、開発機では再現しない」── Windowsの業務アプリで最も多い苦情のひとつが、この「応答なし」です。そして意外に知られていないのが、「応答なし」という表示を出しているのは固まったアプリ自身ではなく、OSであることです。
Windowsは、アプリが「固まった」ことをどうやって知るのか。白くかすんだあのウィンドウは何者なのか。この記事では、Windowsで業務アプリを作る開発者と、固まるアプリの問い合わせを受ける情シス担当者を対象に、「応答なし」判定の仕組みをメッセージループの基礎から解き明かし、固まる定番原因、固まらない設計、そして固まった瞬間の調査手順までを一次情報にもとづいて整理します。
1. まず結論
- 「応答なし」はOSの判定です。入力を待たず、起動処理中でもなく、5秒間メッセージの取り出し(
PeekMessage)を行っていないウィンドウ(とその所有GUIスレッド)を、OSは応答なしとみなします。判定はプロセス単位ではありません。1 - 白くなったウィンドウは「ゴーストウィンドウ」です。OSが元のウィンドウを隠し、同じ位置・サイズ・外観の偽物に差し替えています。できるのは移動・最小化・閉じるだけで、中身は動いていません。デバッガー接続中はゴーストウィンドウは生成されません。2
- 固まる原因はほぼ1つに帰着します。メッセージループを回すべきUIスレッドが、重い処理や待機で塞がっていることです。同期I/O・ネットワーク呼び出し・ロック待ち・スレッド間の
SendMessageが定番です。3 - 対策の原則は「UIスレッドで待たない・計算しない」です。重い処理はワーカースレッド(C#なら
async/await+Task.Run)に移し、UIスレッドは描画・進捗・キャンセル受付に専念させます。4 DoEventsやメッセージポンプの手動駆動は再入バグの温床です。応答なし表示は消えますが、処理の途中に任意のイベントが割り込む構造になります。回避ではなく分離が正攻法です。- 調査は「固まっている瞬間」の状態確保が第一です。ダンプを取ってUIスレッドのスタックを見れば、何を待って止まっているかはほぼ特定できます。
2. 前提: Windowsアプリはメッセージで動いている
「応答なし」を理解するには、まずWindowsのGUIアプリがイベント駆動であることを押さえる必要があります。GUIアプリは自分から入力を取りに行くのではなく、OSが届けるメッセージ(マウス、キーボード、再描画要求、タイマーなど)を受け取って動きます。3
ウィンドウを作った各スレッドはメッセージキューを持ち、次のようなメッセージループを回します。
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // ウィンドウプロシージャが呼ばれる
}
GetMessage でキューからメッセージを取り出し、DispatchMessage がそのウィンドウのウィンドウプロシージャ(メッセージ処理関数)を呼ぶ。ボタンのクリック処理も、再描画も、WinFormsやWPFのイベントハンドラーも、突き詰めればすべてこのループの1回転の中で実行されています。5
flowchart TB
accTitle: メッセージループの基本構造
accDescr: OSがマウスやキーボードなどの入力をスレッドのメッセージキューに入れ、UIスレッドのループがGetMessageで取り出してDispatchMessageでウィンドウプロシージャを呼び、処理が終わるとループの先頭に戻る
os["OS(入力・再描画要求・タイマー)"] --> q["スレッドのメッセージキュー"]
q --> gm["GetMessageで取り出す"]
gm --> dm["DispatchMessage"]
dm --> wp["ウィンドウプロシージャで処理"]
wp --> gm
図1: GUIアプリの心臓部はメッセージループで、イベント処理はすべてこのループの1回転として実行される。
この構造の重要な帰結がひとつあります。ウィンドウプロシージャ(イベントハンドラー)の中で時間のかかる処理をすると、その間ループは次のメッセージを取り出せないということです。クリックにも再描画要求にも反応できなくなる ── これが「固まる」の正体です。
メッセージの届き方には2経路あることも押さえておきます。PostMessage はキューに積んですぐ戻り、ループが順に取り出して処理します。一方 SendMessage はウィンドウプロシージャを直接呼び出し、処理が完了するまで呼び出し元に戻りません。36 この違いは4章のデッドロックの話にそのまま効いてきます。
flowchart TB
accTitle: メッセージの2つの配送経路
accDescr: PostMessageはキューに積んですぐ戻り、メッセージループが順に取り出して処理する。SendMessageはウィンドウプロシージャを直接呼び出し、処理が完了するまで呼び出し元に戻らない
pm["PostMessage"] --> q2["キューに積む(すぐ戻る)"]
q2 --> loop["ループが順に取り出して処理"]
sm["SendMessage"] --> direct["プロシージャを直接呼ぶ"]
direct --> w2["処理完了まで戻らない"]
図2: 同じ「メッセージを送る」でも、キュー経由のPostと完了を待つSendでは性質がまったく違う。
3. 「応答なし」判定の仕組み ── 5秒ルールとゴーストウィンドウ
では、OSはどうやって「このアプリは固まった」と知るのでしょうか。判定基準は公式に文書化されています。入力を待っておらず、起動処理中でもなく、5秒間 PeekMessage(メッセージの取り出し)を呼んでいないウィンドウを、OSは応答なしとみなします。1 つまりOSは「メッセージループがちゃんと回転しているか」を脈拍のように見ていて、5秒間脈がなければ応答なしと判定するわけです(この5秒という値は将来変わりうると明記されています)。判定の単位はウィンドウとその所有GUIスレッドで、複数のUIスレッドを持つアプリでは、1つが固まっても別スレッドのウィンドウは生きていることがあります。ダンプで見るべきは固まったウィンドウの所有スレッドです。
判定されたトップレベルウィンドウに起きることも文書化されています。OSは元のウィンドウを非表示にし、同じZオーダー・位置・サイズ・外観を持つ「ゴーストウィンドウ(ghost window)」に差し替えます。ユーザーがそれに対してできるのは移動・サイズ変更・(強制)クローズだけです。中身のアプリは実際には応答していないので、それ以外の操作はできません。2
flowchart TB
accTitle: 応答なし判定とゴーストウィンドウ差し替えの流れ
accDescr: UIスレッドが重い処理で塞がりメッセージの取り出しが5秒止まると、OSが応答なしと判定して元のウィンドウを隠し、同じ見た目のゴーストウィンドウに差し替え、ユーザーには移動・最小化・閉じる操作だけを提供する
busy["UIスレッドが重い処理で塞がる"] --> stop["メッセージ取り出しが止まる"]
stop --> judge{"5秒経過?"}
judge -->|"いいえ"| stop
judge -->|"はい"| ghost["ゴーストウィンドウに差し替え"]
ghost --> u1["タイトルに(応答なし)表示"]
ghost --> u2["白くかすむ・移動と閉じるのみ可"]
図3: 「応答なし」の表示も白い画面も、固まったアプリではなくOSが差し替えたゴーストウィンドウのもの。
タイトルバーに付く「(応答なし)」の文字列も、Aeroテーマで白くかすむ見た目も、すべてこのゴーストウィンドウのものです。ここから2つの実務的な帰結が導けます。
- 「応答なし」と表示された時点で、そのウィンドウの所有スレッドは少なくとも5秒間メッセージを処理していません。「表示が出るのが早すぎる」のではなく、UIスレッドが確実に塞がっています。
- デバッガーを接続しているとゴーストウィンドウは生成されません。2 「デバッグ実行だと応答なしにならないのに、リリースだと出る」ように見える場合、固まり方は同じで表示だけが違う、ということがあります。
なお、プロセス単位でこの差し替えを無効化する DisableProcessWindowsGhosting というAPIも存在します。7 キオスク端末など「OSに勝手にウィンドウを操作可能に見せてほしくない」特殊な場面のためのもので、呼べば「応答なし」表示は出なくなりますが、固まっている事実は変わりません。一般のアプリが応答なし対策として使うものではない、と理解してください。
4. なぜ固まるのか ── UIスレッドを塞ぐ定番パターン
原因は突き詰めれば「UIスレッドがメッセージループに戻ってこない」の一点ですが、実務で出会う形はいくつかの定番に分類できます。
flowchart TB
accTitle: UIスレッドを塞ぐ定番原因の分類
accDescr: 同期I/Oとネットワーク呼び出し、ロック待ち、スレッド間SendMessage、COMのSTA絡みという4系統の定番原因は、いずれもUIスレッドがメッセージループに戻れないという同じ一点に帰着する
c1["同期I/O・ネットワーク呼び出し"] --> core["UIスレッドがループに戻れない"]
c2["ロック待ち"] --> core
c3["スレッド間SendMessage"] --> core
c4["COMのSTA絡み"] --> core
core --> ar["応答なし判定へ"]
図4: 見た目の症状は同じでも、塞いでいる犯人は4系統に分類でき、それぞれ対策が異なる。
同期I/Oとネットワーク呼び出し。最多はこれです。ボタンのクリックハンドラーの中で、大きなファイルの読み書き、データベースクエリ、Web APIの呼び出し、ネットワークドライブ上のファイルアクセスを同期で行うパターン。開発機ではコンマ数秒で終わるため気づかず、本番のネットワーク遅延やファイルサーバーの不調で数十秒待ちになり、「たまに固まる」という問い合わせになります。ネットワークドライブは接続断時のタイムアウトが長く、症状を劇的に悪化させます。
ロック待ち。UIスレッドがワーカースレッドと共有するデータのロックを取ろうとして、そのロックを長時間握るワーカーを待ってしまうパターン。ロックの規律についてはマルチスレッド実務シリーズで詳しく扱っています。
スレッド間の SendMessage。SendMessage は、宛先ウィンドウのプロシージャが処理を終えるまで戻りません。6 別スレッドのウィンドウに送った場合、そのスレッドがメッセージを処理できる状態になるまで送信側は待たされます。ここで宛先スレッドも何かを待っていると、互いに待ち合うメッセージデッドロックが成立します。3 特に HWND_BROADCAST への送信は、応答しないウィンドウが1枚あるだけで巻き込まれます。待てない場面では SendMessageTimeout や、応答を待たない PostMessage を検討します。8
sequenceDiagram
accTitle: スレッド間SendMessageによるデッドロック
accDescr: UIスレッドがワーカースレッドの結果待ちでブロックしているところに、ワーカースレッドがUIスレッドのウィンドウへSendMessageを送ると、互いに相手の完了を待ち合ってデッドロックになる
participant U as UIスレッド
participant W as ワーカースレッド
U->>U: ワーカーの完了を待機(ブロック)
W->>U: SendMessage(処理完了まで戻らない)
Note over U: メッセージを処理できない(待機中)
Note over W: SendMessageから戻れない
Note over U,W: 互いに待ち合いデッドロック
図5: 「UIスレッドがワーカーを待ち、ワーカーがSendMessageでUIスレッドを待つ」は定番のデッドロック。
COMのアパートメント絡み。STAオブジェクトへの呼び出しはウィンドウメッセージとして配送されるため、UIスレッド(STA)が塞がると、他スレッドからのCOM呼び出しも巻き添えでブロックします。この構造はCOM STA/MTAの記事で解説しています。
「一瞬だから」の積み重ね。1回50msの同期処理でも、ループで100回呼べば5秒です。応答なしの閾値は5秒ですが、体感の「もっさり」は100ms程度から始まります。設計の目安は「UIスレッドを塞いでよいのはミリ秒単位まで」です。
5. 固まらない設計 ── 重い処理をUIスレッドから追い出す
対策の原則はひとつ、時間のかかる仕事をUIスレッドから追い出すことです。C#(WinForms/WPF)なら async/await が最短の正攻法です。
private async void RunButton_Click(object sender, EventArgs e)
{
runButton.Enabled = false;
try
{
// CPU負荷の高い処理・同期しかないAPIはTask.Runでワーカーへ
var result = await Task.Run(() => HeavyCalculation(input));
// I/OはネイティブにasyncなAPIを使う(スレッドも消費しない)
var data = await httpClient.GetStringAsync(url);
// awaitの後はUIスレッドに戻っているので、コントロールを直接触れる
resultLabel.Text = result + data.Length.ToString();
}
catch (Exception ex)
{
// async voidハンドラーから例外を漏らすとアプリが落ちる。ここで受け止める
MessageBox.Show($"処理に失敗しました: {ex.Message}");
}
finally
{
runButton.Enabled = true;
}
}
ポイントは3つあります。第一に、await の待機中、UIスレッドはメッセージループに戻っているので応答なしになりません。第二に、await の継続はUIスレッドに戻ってくるため、後続でコントロールを普通に触れます(ワーカースレッドからコントロールを直接触るのは禁止で、必要なら Control.Invoke / Dispatcher.InvokeAsync を使います)。4 第三に、実行中はボタンを無効化するなどして再入を設計で殺すことです。
Win32ネイティブの場合も構図は同じで、ワーカースレッドに仕事を渡し、完了を PostMessage で自作メッセージとしてUIスレッドに通知し、ウィンドウプロシージャで画面を更新します。PostMessage はキューに積むだけですぐ戻るため、ワーカー側がブロックすることもありません。6 ワーカースレッドの待ち合わせ自体は、条件変数の記事で扱った規律で書きます。
flowchart TB
accTitle: 固まらないアプリの役割分担
accDescr: UIスレッドは入力受付・進捗表示・キャンセル受付だけを担当し、重い処理はワーカースレッドが実行して、完了をPostMessageやawaitの継続でUIスレッドに返す
ui["UIスレッド:入力・進捗・キャンセル"] -->|"仕事を渡す"| w["ワーカースレッド:重い処理"]
w -->|"PostMessage / awaitの継続"| ui
ui -.-> ng["UIスレッドでの同期I/O・長い計算は禁止"]
図6: UIスレッドは「受付係」に徹し、重い仕事は必ずワーカーに渡して完了通知だけ受け取る。
避けたいのが、重い処理の合間に Application.DoEvents() や PeekMessage ループを差し込んで表示だけ生かす手法です。応答なしは回避できますが、処理の途中で任意のイベントハンドラーが再入します。ボタンの二度押し、処理中のフォームクローズ、タイマーの発火 ── どれも処理中のデータを壊しうるうえ、タイミング依存で再現しにくいバグになります。メッセージポンプを手で回すのは、モーダルな進捗ダイアログのような限定された構造の中だけにとどめ、原則は分離で解決してください。
sequenceDiagram
accTitle: DoEventsによる再入バグの時系列
accDescr: 重い処理の途中でDoEventsを呼ぶと溜まっていたクリックのイベントハンドラーが割り込んで実行され、処理中のデータを書き換えてから元の処理が再開するため、タイミング依存のデータ破壊が起きる
participant U as UIスレッド
U->>U: 重い処理を開始(データ加工中)
U->>U: DoEvents(溜まったメッセージを処理)
Note over U: ボタン再クリックのハンドラーが割り込む
U->>U: 割り込み処理がデータを書き換え
U->>U: 元の処理が再開(データは既に不整合)
図7: DoEventsは「応答なし」を消す代わりに、処理の真ん中へ任意のイベントを招き入れる。
長時間処理には進捗表示とキャンセルも設計に含めます。IProgress<T> で進捗をUIに送り、CancellationToken で中断を伝える形にしておくと、ユーザーは「動いている」ことが分かり、強制終了(それはしばしばデータ破損の原因になります)に手を伸ばさなくなります。
flowchart TB
accTitle: 長時間処理の進捗とキャンセルの流れ
accDescr: ワーカースレッドはIProgress経由で進捗をUIスレッドに送り、UIのキャンセル操作はCancellationTokenを通じてワーカーに伝わり、ワーカーは区切りのよい箇所で中断して後始末する
w3["ワーカー:長時間処理"] -->|"IProgressで進捗"| ui2["UI:進捗表示と中止ボタン"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["区切りで中断して後始末"]
図8: 進捗は「ワーカー→UI」、キャンセルは「UI→ワーカー」。双方向の細い連絡路を最初から設計に含める。
6. 固まった瞬間の調べ方
「たまに固まる」の調査で最も価値があるのは、固まっているまさにその瞬間のスレッド状態です。再起動してしまうと証拠は消えます。
ダンプを取る。タスクマネージャーの詳細タブで対象プロセスを右クリック→「ダンプ ファイルの作成」。これだけで全スレッドのスタックが入った完全ダンプが取れます。問い合わせを受ける情シス側に「固まったら閉じずにこれを取ってほしい」と伝えておくだけで、調査の成功率は大きく変わります。取得の仕組みづくりはクラッシュダンプ収集の記事を参照してください。
UIスレッドのスタックを見る。WinDbgでダンプを開き、メッセージループを回しているスレッド(通常はスレッド0)のスタックを見ます。同期I/Oなら ReadFile やネットワークAPI、ロック待ちなら WaitFor… 系、スレッド間 SendMessage なら SendMessage の内部で待つ姿が、そのまま写っています。読み方はWinDbg入門記事で解説しています。
生きたまま見る。Process Explorerならスレッド一覧とスタックをその場で確認できます。定常的にもっさりする場合は、WPRでトレースを取ってUIスレッドの待ちを時系列で分析します(WPR/WPA実践)。
flowchart TB
accTitle: 応答なし調査の基本手順
accDescr: 固まっている瞬間にダンプを取り、UIスレッドのスタックを見て、同期I/O・ロック待ち・スレッド間SendMessageのどれで止まっているかを特定し、該当する設計修正につなげる
hang["固まっている瞬間"] --> dump["ダンプを取る(閉じる前に)"]
dump --> stack["UIスレッドのスタックを見る"]
stack --> io["同期I/O・ネットワーク待ち"]
stack --> lock["ロック待ち"]
stack --> sm["スレッド間SendMessage"]
io -.-> fix["該当箇所をワーカーへ分離"]
lock -.-> fix
sm -.-> fix
図9: 調査の主役は「固まっている瞬間のダンプ」で、UIスレッドのスタックがそのまま原因の分類になる。
症状からの当たりの付け方も定番化できます。特定操作で必ず固まるなら、そのハンドラー内の同期I/Oをまず疑う。まれに固まり操作と相関がないなら、ロック順序やスレッド間 SendMessage のデッドロックを疑い、ダンプで両スレッドの待ち先を突き合わせる。特定環境でだけ固まるなら、ネットワークドライブ・プロキシ・ウイルス対策ソフトなど環境要因のタイムアウトを疑う、という流れです。
7. まとめ
- 「応答なし」は、5秒間メッセージを取り出さないアプリをOSが判定し、ゴーストウィンドウに差し替える仕組み。表示を出しているのはアプリではなくOS。
- 固まる原因は「UIスレッドがメッセージループに戻れない」の一点。同期I/O・ネットワーク・ロック待ち・スレッド間SendMessageが定番。
- 対策はUIスレッドから重い処理を追い出すこと。C#なら
async/await+Task.Run、Win32ならワーカースレッド+PostMessage。実行中の再入はボタン無効化などの設計で防ぐ。 DoEventsによる回避は再入バグと引き換え。DisableProcessWindowsGhostingは表示を消すだけ。どちらも根本対策ではない。- 調査は「固まっている瞬間」のダンプが最重要。UIスレッドのスタックに原因はほぼそのまま写っている。
「応答なし」は、ユーザーから見れば「壊れた」ですが、仕組みを知っていれば「UIスレッドが5秒戻ってこなかった」という正確な一文に翻訳できます。その一文から逆算すれば、原因の候補も、直し方も、調査の手順も自然に決まります。
関連記事
- スプリアスウェイクアップ ── 条件変数が「通知なしに目覚める」理由とWindowsでの正しい待ち方
- マルチスレッドの実務ベストプラクティス .NET編
- COM STA/MTA の基礎知識 - スレッドモデルとハングを避ける考え方
- WinDbg + SOSでクラッシュダンプを読む ── 収集した後の実務解析入門
- Process Explorer / Handle / VMMap実践 ── ハング・リーク・「ファイルが使用中」を今この瞬間の状態から追う
- アプリから見たWindowsのシャットダウン ── 終了通知・再起動・電源断を正しく生き延びる
関連する相談領域
合同会社小村ソフトでは、「たまに固まる」「応答なしになる」業務アプリの原因調査(ダンプ解析・トレース分析)、同期処理だらけのレガシーUIコードのasync/await化・ワーカースレッド分離の改修、フリーズしないUI設計のレビューを扱っています。再現手順が不明な段階からでも、証拠の取り方の設計からお手伝いできます。
参考リンク
-
Microsoft Learn, IsHungAppWindow function (winuser.h). アプリが「入力を待っておらず、起動処理中でもなく、内部タイムアウトである5秒の間PeekMessageを呼んでいない」場合に応答なしとみなされるという判定基準、この5秒という基準は変更されうること、ゴーストウィンドウに対しては常にTRUEが返ることについて。 ↩ ↩2
-
Microsoft Learn, GetMessage function (winuser.h). トップレベルウィンドウが数秒間メッセージに応答しなくなると、システムがそのウィンドウを応答なしとみなし、同じZオーダー・位置・サイズ・外観を持つゴーストウィンドウに差し替えること、ユーザーは移動・サイズ変更・クローズだけができること、デバッガー接続中はゴーストウィンドウが生成されないことについて。 ↩ ↩2 ↩3
-
Microsoft Learn, About Messages and Message Queues. Windowsアプリがイベント駆動であり、ウィンドウプロシージャがメッセージを処理する構造、キュー経由のメッセージと直接送信されるメッセージの区別、応答なしウィンドウのゴーストウィンドウへの差し替え、スレッド間でメッセージを送り合うことによるデッドロックの節について。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). WinFormsのコントロールが作成されたスレッド以外から安全に触れないこと、他スレッドからの更新にはInvoke/BeginInvokeを使うこと、async/awaitやBackgroundWorkerを使った安全な非同期パターンについて。 ↩ ↩2
-
Microsoft Learn, Using Messages and Message Queues. GetMessage・TranslateMessage・DispatchMessageによる標準的なメッセージループの実装例と、メッセージキューの調査方法について。 ↩
-
Microsoft Learn, SendMessage function (winuser.h). SendMessageが指定ウィンドウのウィンドウプロシージャを呼び出し、処理が完了するまで戻らないこと、別スレッドのウィンドウへの送信では相手スレッドがメッセージを処理するまで待たされること、応答を待たずキューに積むPostMessageとの違いについて。 ↩ ↩2 ↩3
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). 呼び出したGUIプロセスについて、応答なしウィンドウを最小化・移動・クローズ可能にするゴーストウィンドウ機能を無効化できること、無効化はプロセスの生存期間中続くことについて。 ↩
-
Microsoft Learn, SendMessageTimeout function (winuser.h). タイムアウト付きでメッセージを送信できること、応答しないウィンドウ(ハング判定されたウィンドウ)に対して待たずに戻るフラグ(SMTO_ABORTIFHUNG)が用意されていることについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
DllMainとローダーロック ── 「DLLの初期化で何もするな」と言われる本当の理由
DllMainでLoadLibraryやスレッド同期をしてはいけないのはなぜか。全DLL通知を直列化するローダーロックの仕組みから、デッドロックが成立する典型シナリオ、遅延初期化などの正しい設計、ハング調査の手順までを一次情報で解説します。
Windowsアプリ 外注・受託開発を依頼する前に整理したいこと
Windowsアプリの外注・受託開発を依頼する前に、既存ソフト改修、装置連携、COM/ActiveX、配布・更新、保守の整理ポイントを解説します。
Win32スレッドプールAPI ── CreateThreadpoolWorkで「スレッドを作らない」並行処理
ネイティブコードでCreateThreadを乱立させていませんか。Vistaで刷新されたWin32スレッドプールAPIのwork・timer・wait・ioの4オブジェクト、クリーンアップグループ、コールバックでの禁止事項までを一次情報にもとづいて解説します。
スリープ復帰で壊れるアプリ ── 電源イベントの仕組みと復帰に強い業務アプリの作り方
ノートPCを開いたら業務アプリの通信が切れていた――原因はスリープを想定しない設計です。WM_POWERBROADCASTによる通知の流れ、Modern Standbyの挙動、切断・再接続の設計、スリープ抑止と調査コマンドまでを一次情報で解説します。
スプリアスウェイクアップ ── 条件変数が「通知なしに目覚める」理由とWindowsでの正しい待ち方
条件変数のwaitは通知が来ていなくても目覚めることがあります(スプリアスウェイクアップ)。仕様がそれを許す理由をWindowsの実装から解き明かし、whileと述語で書く正しい待ち方をWin32・C++・C#のコードで示します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
UI スレッド / タイマーテーマ
WPF / WinForms、UI スレッド、async/await、タイマー設計を整理するトピックです。
不具合調査 / 長期稼働テーマ
再現しにくい不具合、通信停止、長期稼働障害、失敗パス検証を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
不具合調査・原因解析
再現しにくい障害、長期稼働後の不具合、通信停止などの調査を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- 「応答なし」はどういう条件で表示されるのですか?
- OSは、ウィンドウを持つアプリが「入力を待っておらず、起動処理中でもなく、5秒間メッセージの取り出し(PeekMessage)を行っていない」とき、そのウィンドウを応答なしと判定します。判定されたトップレベルウィンドウは非表示にされ、同じ位置・サイズ・外観の「ゴーストウィンドウ」に差し替えられます。タイトルバーの「(応答なし)」表示や白くかすむ見た目はこのゴーストウィンドウのもので、移動・最小化・閉じる操作だけができます。つまり「応答なし」はアプリ自身の表示ではなく、OSが肩代わりして出している画面です。
- 処理中に「応答なし」と表示されないようにする設定はありますか?
- DisableProcessWindowsGhostingを呼ぶと、そのプロセスに対するゴーストウィンドウへの差し替えを無効化できます。ただしこれは「固まっていることをユーザーから見えにくくする」だけで、ウィンドウが操作に反応しない事実は変わらず、ユーザーには移動も閉じることもできない完全な固まり方として見えます。根本対策は表示の抑止ではなく、UIスレッドを5秒どころか0.1秒単位でも塞がないよう、重い処理をワーカースレッドに移すことです。なおデバッガー接続中はOSがゴーストウィンドウを生成しないため、「デバッグ中は応答なしにならない」ように見える点にも注意してください。
- DoEvents(メッセージポンプの手動駆動)で応答なしを回避してもよいですか?
- 推奨しません。重い処理の途中でDoEventsやPeekMessageループを回すと、応答なし判定は回避できますが、処理の途中でボタンの再クリック・ウィンドウを閉じる操作・タイマーなど任意のイベントハンドラーが再入してきます。処理中のデータを別のハンドラーが書き換える、閉じたはずのフォームに触って例外になる、といった再入バグはタイミング依存で再現しにくく、応答なしより厄介です。正攻法は処理そのものをTask.Runなどでワーカースレッドに移し、UIスレッドは進捗表示とキャンセル受付だけを担当する形にすることです。
- ワーカースレッドから画面(コントロール)を更新するにはどうすればよいですか?
- WinFormsのコントロールやWPFの要素は、作成したスレッド(通常はUIスレッド)からしか触れません。ワーカースレッドから直接触ると例外や不定動作になります。C#ならasync/awaitを使うのが最も簡単で、awaitの継続は呼び出し元のUIスレッドに戻ってくるため、awaitの後で普通にコントロールを更新できます。明示的に切り替える場合はWinFormsではControl.Invoke/BeginInvoke、WPFではDispatcher.InvokeAsyncを使います。Win32ネイティブでは、ワーカースレッドからPostMessageで自作の完了メッセージをUIスレッドに送り、ウィンドウプロシージャ側で画面を更新するのが定石です。
- 「応答なし」になっているアプリの原因はどう調べればよいですか?
- 固まっている「その瞬間」に状態を取るのが重要です。まずタスクマネージャーの詳細タブから「ダンプファイルの作成」で完全ダンプを取り、WinDbgでUIスレッド(メッセージループを回しているスレッド)のスタックを見ます。同期I/O・ネットワーク待ち・ロック待ち・SendMessageによる他スレッド待ちのどれで止まっているかはスタックにそのまま現れます。生きた状態で見るならProcess Explorerのスレッド一覧とスタック表示、時系列で追うならWPRでのトレース採取が有効です。当サイトのWinDbg入門・Process Explorer実践・WPR/WPA実践の各記事も参考にしてください。