16

終了時にハングする非常にスレッド集約的なアプリケーションを作成しています。

システム ユニットを調べたところ、プログラムが無限ループに入る場所が見つかりました。SysUtilsの19868 行にあります -> DoneMonitorSupport -> CleanEventList :

repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0;

オンラインで解決策を検索したところ、いくつかの QC レポートが見つかりました。

残念ながら、私はTThreadListTMonitorも使用していないため、これらは私の状況とは関係がないようです。

すべてのスレッドが作成/破棄カウントを保持するベース スレッドから継承されるため、すべてのスレッドが終了し、破棄されたことは確かです。

誰かが以前に同様の行動に出くわしたことがありますか? 根本原因がどこにあるのかを発見するための戦略を知っていますか?

4

4 に答える 4

15

ロックがどのようにTMonitor実装されているかを調べていて、ついに興味深い発見をしました。ちょっとしたドラマとして、まずロックがどのように機能するかを説明します。

で任意のTMonitor関数を呼び出すTObjectと、レコードの新しいインスタンスが作成され、そのインスタンスがオブジェクト自体の内部にTMonitor割り当てられます。MonitorFldこの割り当ては、 を使用してスレッドセーフな方法で行われますInterlockedCompareExchangePointer。このトリックのため、TObjectには をサポートするためのポインタ サイズのデータ​​が 1 つだけ含まれてTMonitorおり、完全な TMonitor 構造体は含まれていません。そして、それは良いことです。

このTMonitor構造には、多数のレコードが含まれています。FLockCount: Integerフィールドから始めましょう。最初のスレッドがTMonitor.Enter()任意のオブジェクトを使用すると、この結合されたロック カウンター フィールドの値は 0 になります。再びInterlockedCompareExchangeメソッドを使用してロックが取得され、カウンターが開始されます。これはすべてインプロセスで行われるため、呼び出しスレッドのロックやコンテキスト スイッチはありません。

2 番目のスレッドTMonitor.Enter()が同じオブジェクトにアクセスしようとすると、最初のロックの試行は失敗します。その場合、Delphi は次の 2 つの戦略に従います。

  • 開発者TMonitor.SetSpinCount()が「スピン」の回数を設定していた場合、Delphi はビジー待機ループを実行し、指定された回数だけスピンします。これは、コンテキスト切り替えを行わずにロックを取得できるため、小さなロックには非常に便利です。
  • スピン カウントの有効期限が切れた場合 (またはスピン カウントがなく、デフォルトでスピン カウントがゼロの場合) は、TMonitor.Enter()によって返されたイベントで Wait を開始しTMonitor.GetEvent()ます。つまり、CPU サイクルを浪費するビジーウェイトは発生しません。TMonitor.GetEvent()とても大事なことなので覚えておいてください。

ロックを取得したスレッドと、ロックを取得しようとしたが現在は によって返されるイベントを待機しているスレッドがあるとしTMonitor.GetEventます。最初のスレッドが呼び出すTMonitor.Exit()と、(フィールドを介してFLockCount) 少なくとも 1 つの他のスレッドがブロックされていることがわかります。そのため、通常は以前に割り当てられたイベント (呼び出しTMonitor.GetEvent()) であるはずのものをすぐにパルスします。TMonitor.Exit()しかし、呼び出した側と呼び出した側の 2 つのスレッドTMonitor.Enter()が実際にTMonitor.GetEvent()同時に呼び出す可能性があるため、操作の順序に関係なく、イベントが1 つだけTMonitor.GetEvent()割り当てられるようにするためのいくつかのトリックが内部にあります。

もう少し楽しい瞬間のために、私たちは今、TMonitor.GetEvent()仕組みを掘り下げます. このことはSystemユニット内に存在します (ご存知のように、再コンパイルして再生することはできません) が、System.MonitorSupportポインターを介して、イベントを実際に割り当てられた別のユニットに委任することが判明しました。TMonitorSupportこれは、 5 つの関数ポインターを宣言するタイプのレコードを指します。

  • NewSyncObject- 同期のために新しいイベントを割り当てます
  • FreeSyncObject- 同期のために割り当てられたイベントの割り当てを解除します
  • NewWaitObject- 待機操作用の新しいイベントを割り当てます
  • FreeWaitObject- その待機イベントの割り当てを解除します
  • WaitAndOrSignalObject-まあ..待機または合図。

また、関数によって返されるオブジェクトは、への呼び出しと対応するNewXYZへの呼び出しにのみ使用されるため、何でもかまいません。これらの関数の実装方法は、これらのロックに最小限のロックとコンテキスト切り替えを提供するように設計されています。そのため、オブジェクト自体 (およびによって返される) は、 によって返される直接のイベントではなく、内のレコードへのポインタです。さらに進んで、実際の Windows イベントは必要になるまで作成されません。そのため、 のレコードにはいくつかのレコードが含まれています。WaitXYZFreeXyzObjectSysUtilsNewSyncObjectNewWaitObjectCreateEvent()SyncEventCacheArraySyncEventCacheArray

  • TSyncEventItem.Lock-これは、ロックが現在何かに使用されているかどうかを Delphi に伝えます。
  • TSyncEventItem.Event- これは、待機が必要な場合に同期に使用される実際のイベントを保持します。

アプリケーションが終了すると、SysUtils.DoneMonitorSupportは 内のすべてのレコードをSyncEventCacheArray調べて、ロックが 0 になるのを待ちます。つまり、ロックが使用されなくなるのを待ちます。理論的には、そのロックがゼロでない限り、少なくとも 1 つのスレッドがロックを使用している可能性があります。そして、ようやく現在の質問にたどり着きました。SysUtils.DoneMonitorSupport

すべてのスレッドが適切に終了しているにもかかわらず、アプリケーションが SysUtils.DoneMonitorSupport でハングするのはなぜですか?

NewSyncObjectまたはのいずれかを使用して割り当てられた少なくとも 1 つのイベントが、対応するまたはNewWaitObjectを使用して解放されなかったためです。そして、日常に戻ります。割り当てられたイベントは、 に使用されたオブジェクトに対応するレコードに保存されます。そのレコードへのポインタは、そのオブジェクトのインスタンス データにのみ保持され、アプリケーションの存続期間中そこに保持されます。フィールドの名前を検索すると、ファイル内で次のことがわかります。FreeSyncObjectFreeWaitObjectTMonitor.GetEvent()TMonitorTMonitor.Enter()FLockEventSystem.pas

procedure TMonitor.Destroy;
begin
  if (MonitorSupport <> nil) and (FLockEvent <> nil) then
    MonitorSupport.FreeSyncObject(FLockEvent);
  Dispose(@Self);
end;

ここにあるレコードデストラクタへの呼び出し: procedure TObject.CleanupInstance.

つまり、最終的な同期イベントは、同期に使用されたオブジェクトが解放されたときにのみ解放されます!

OPの質問への回答:

使用された OBJECT の少なくとも 1 つTMonitor.Enter()が解放されていないため、アプリケーションがハングします。

可能な解決策:

残念ながら、私はこれが好きではありません。それは正しくありません。つまり、小さなオブジェクトを解放しないことのペナルティは、アプリケーションがハングするのではなく、小さなメモリ リークになるはずです! これは、サービスが単に永久にハングアップし、完全にシャットダウンするわけではなく、要求に応答できないサービス アプリケーションにとって特に悪いことです。

Delphi チームのソリューションは? SysUtils何があっても、ユニットのファイナライズ コードでハングするべきではありません。おそらく無視してLock、イベント ハンドルを閉じる必要があります。その段階 (SysUtils ユニットのファイナライズ) で、何らかのスレッドでまだコードが実行されている場合、ほとんどのユニットがファイナライズされており、実行するように設計された環境で実行されていないため、コードは非常に悪い形になっています。

デルフィユーザー向け?MonitorSupportを、ファイナライズ時に広範なテストを行わない独自のバージョンに置き換えることができます。

于 2013-01-09T00:39:31.153 に答える
1

Cosmin が提供する例を使用して、問題を再現できます。すべてのスレッドが完了したら、SyncObj を解放するだけで問題を解決することもできます。

私はあなたのコードにアクセスできないので、これ以上は言えませんが、TMonitor で使用されているオブジェクト インスタンスの一部が解放されていない可能性があります。

于 2013-01-09T00:18:15.047 に答える
1

Delphi XE5 では、Embarcaderoは 1 ミリ秒後に終了するようにループに追加(Now - Start > 1 / MSecsPerDay) orすることでこれを解決しました。次に、 であったかどうかに関係なく、イベントを削除します。repeat untilCleanEventListLock0

于 2016-02-10T01:37:54.130 に答える