ロックがどのように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 イベントは必要になるまで作成されません。そのため、 のレコードにはいくつかのレコードが含まれています。WaitXYZ
FreeXyzObject
SysUtils
NewSyncObject
NewWaitObject
CreateEvent()
SyncEventCacheArray
SyncEventCacheArray
TSyncEventItem.Lock
-これは、ロックが現在何かに使用されているかどうかを Delphi に伝えます。
TSyncEventItem.Event
- これは、待機が必要な場合に同期に使用される実際のイベントを保持します。
アプリケーションが終了すると、SysUtils.DoneMonitorSupport
は 内のすべてのレコードをSyncEventCacheArray
調べて、ロックが 0 になるのを待ちます。つまり、ロックが使用されなくなるのを待ちます。理論的には、そのロックがゼロでない限り、少なくとも 1 つのスレッドがロックを使用している可能性があります。そして、ようやく現在の質問にたどり着きました。SysUtils.DoneMonitorSupport
すべてのスレッドが適切に終了しているにもかかわらず、アプリケーションが SysUtils.DoneMonitorSupport でハングするのはなぜですか?
NewSyncObject
またはのいずれかを使用して割り当てられた少なくとも 1 つのイベントが、対応するまたはNewWaitObject
を使用して解放されなかったためです。そして、日常に戻ります。割り当てられたイベントは、 に使用されたオブジェクトに対応するレコードに保存されます。そのレコードへのポインタは、そのオブジェクトのインスタンス データにのみ保持され、アプリケーションの存続期間中そこに保持されます。フィールドの名前を検索すると、ファイル内で次のことがわかります。FreeSyncObject
FreeWaitObject
TMonitor.GetEvent()
TMonitor
TMonitor.Enter()
FLockEvent
System.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
を、ファイナライズ時に広範なテストを行わない独自のバージョンに置き換えることができます。