4

非常にマルチスレッド化された Delphi 6 アプリケーションがあります。TWinControl から派生したコンポーネントを作成しました。私が最初にそれを構築したとき、私は非表示のウィンドウを使用しました.AllocateHwnd()で割り当てられたメッセージを処理するのはWndProcです。最近、コード内の WndProc のクリーンアップを開始し、補助的な WndProc() を削除することにしました。コンポーネントを変更して、代わりに基本クラスの WndProc() メソッドをオーバーライドし、そこからカスタム Windows メッセージ処理を行いました。その WndProc() では、継承されたハンドラーを最初に呼び出してから、カスタム メッセージ (WM_USER オフセット) を処理し、カスタム メッセージの 1 つが見つかって処理された場合は、メッセージの Result フィールドを 1 に設定しました。

重要な注意事項が 1 つあります。現在のスレッド ID が VCL メイン スレッドでない場合に例外をスローするコード行を WndProc() オーバーライドの先頭に追加しました。WndProc() がメイン VCL スレッドのコンテキストでのみ実行されるようにしたかったのです。

これを行ってプログラムを実行した後、本当に奇妙に思えることに遭遇しました。プログラムを通常どおり実行し、さまざまなタスクをエラーなしで実行しました。次に、TWinControl の子孫と同じページにある TMemo コントロールに移動したとき。その TMemo コントロール内をクリックすると、WndProc() オーバーライドのメイン スレッド チェックがトリガーされます。ブレークポイントが設定されていて、コール スタックに移動したとき、WndProc() オーバーライドの上には何もありませんでした。

私が知る限り、再確認したところ、WndProc() オーバーライドを明示的に呼び出していません。それは私が決してすることではありません。しかし、私の TWinControl コンポーネントが他のすべてのコンポーネントと同様にメインの VCL スレッドで作成されることを考えると、バックグラウンド スレッドのコンテキストで WndProc() オーバーライドがどのように実行されるかはわかりません。マウスクリックが発生します。すべての子ウィンドウが最上位ウィンドウ WndProc() からぶら下がっているため、WndProc() が TMemo コントロールにどのように関連付けられているかを理解しています。少なくともそれは私の理解です。しかし、すべてのコンポーネント ウィンドウがメイン VCL スレッドで作成されているため、すべてのメッセージ キューもそのコンテキストで実行されているはずですよね?

では、WndProc() をバックグラウンド スレッドのコンテキストで実行するために、どのような状況を作り出すことができるでしょうか?

4

2 に答える 2

5

WndProc()ワーカー スレッドのコンテキストでメイン スレッド コンポーネントのメソッドを呼び出すには、次の 2 つの方法があります。

  1. WindowProcワーカー スレッドは、コンポーネントのプロパティまたはそのPerform()メソッドを直接呼び出します。

  2. ワーカー スレッドは、プロパティの安全でない使用により、コンポーネントのウィンドウの所有権を盗みTWinControl.Handleました。Handleプロパティ ゲッターはスレッド セーフではありません。Handleメインスレッドがコンポーネントのウィンドウを再作成しているのとまったく同じ瞬間に、ワーカースレッドがプロパティから読み取った場合(TWinControlウィンドウは永続的ではありません - さまざまなランタイム条件が UI ロジックの大部分に影響を与えずにウィンドウを動的に再作成できます)、ワーカー スレッドが独自のコンテキスト内で新しいウィンドウを割り当てることができる競合状態が存在します (そしてメイン スレッドが別のウィンドウをリークします)。これにより、メイン スレッドはそのコンテキスト内でメッセージの受信とディスパッチを停止します。ワーカー スレッドに独自のメッセージ ループがある場合、代わりにメッセージを受信して​​ディスパッチするためWndProc()、間違ったスレッド コンテキストでメソッドが呼び出されます。

ただし、呼び出しスタックが生成されていないのは奇妙だと思います。利用可能なある種のトレースが常にあるはずです。

また、MainThreadId変数 (またはメイン スレッドを追跡するために使用しているもの) が単に誤って破損していないことを確認してください。現在の値が起動時の初期値と一致していることを確認してください。

もう 1 つすべきことは、デバッガーですべてのスレッド インスタンスに名前を付けることです (この機能は Delphi 6 で導入されました)。そうすれば、スレッドの検証が失敗した場合でも、デバッガーはWndProc()メソッドを呼び出しているスレッド コンテキストの正確な名前を表示でき (コール スタック トレースがなくても)、そのスレッドのコードのバグを探すことができます。

于 2012-01-30T06:46:19.550 に答える
1

Remy LeBeauの返信には、私が間違ったことの説明が含まれています。この更新プログラムを含めているので、バックグラウンドスレッドでVCLUIコントロールへの参照を保持しているときにエラーがどれほど微妙に発生するかを示す具体的なケースのトリッキーな詳細を確認できます。うまくいけば、この情報があなた自身のコードをデバッグするのに役立つはずです。

私のアプリケーションの一部には、TCustomControlから派生したVCLコンポーネントが含まれており、TCustomControlはTWinControlから派生しています。ソケットを集約し、そのソケットが外部デバイスからビデオを受信するためのバックグラウンドスレッドを作成します。

エラーが発生すると、そのバックグラウンドスレッドは、PostMessage()を使用して監査目的でTMemoコントロールにメッセージを投稿します。 PostMessage()で使用するウィンドウハンドル(HWND)はTMemoコントロールに属しているため、ここで間違いを犯しました。 TMemoコントロールは、私のコンポーネントと同じフォームにあります。

ビデオ接続が失われると、それを提供するソケットは閉じられて破壊されますが、それを提供するバックグラウンドスレッドはまだ終了していません。これで、ソケットが参照している無効なソケットで操作を実行しようとすると、#10038ソケットエラー(非ソケットでの操作)が発生します。ここからトラブルが始まります。

TMemoのハンドルを使用してPostMessage()を呼び出すと、TMemoは、オンデマンドでハンドルを再作成する必要がある状態になります。これは、Remyが説明する危険な問題の現象です。これは、再作成されたTMemoウィンドウのWndProc()がバックグラウンドスレッドのコンテキストで実行されていることを意味します

これはすべての証拠に適合します。上記のように、オーバーライドされたWndProc()でバックグラウンドスレッドの警告が表示されるだけでなく、TMemoウィンドウでマウスを使用して何かを行うと、#10038エラーメッセージのストリームがTMemoに表示されます。これは、TMemo、コンポーネントのオーバーライドされたWndProc()、およびバックグラウンドスレッドの間に緩く結合された循環条件が存在するために発生します。これは、そのスレッドのExecute()メソッドにGetMessageループがあるためです。

WindowsメッセージがTMemoコントロールに投稿されるたびに、たとえばマウスの動きなどから、TMemoの背後にあるウィンドウを現在所有しているため、バックグラウンドスレッドのメッセージキューに入れられます。バックグラウンドスレッドが終了しようとし、途中でソケットを閉じようとするため、閉じるたびに、TMemoに投稿される別の#10038メッセージが生成され、各PostMessage()は基本的に自己投稿であるため、ループが持続します。 。

その後、ソケットがデストラクタで呼び出すバックグラウンドスレッドを管理するオブジェクトに通知メソッドを追加し、スレッドが終了し、参照が無効であることを通知しました。ソケットが破棄中にバックグラウンドスレッドをシャットダウンするため、これまでそうすることを考えたことはありませんでしたが、バックグラウンドスレッドからの終了イベントを待ちません。もちろん、別の解決策は、バックグラウンドスレッドが終了するのを待つことです。そのアプローチを採用した場合、このシナリオは、TMemoコントロールで奇妙な動作を引き起こすのではなく、デッドロックに陥ることになります。

[Stack Overflowエディターへの注意-元のメッセージを変更する代わりに、この詳細を返信として追加しているので、解決策を含むRemyの回答をページのはるか下にプッシュしません。]

于 2012-01-30T18:16:56.487 に答える