Ответ 1
Я смотрел, как блокировки TMonitor
реализованы, и я, наконец, сделал интересное открытие. Для немного драмы, я расскажу вам, как работают замки.
Когда вы вызываете любую функцию TMonitor
на TObject
, создается новый экземпляр записи TMonitor
и этот экземпляр присваивается MonitorFld
внутри самого объекта. Это назначение выполняется поточно-безопасным способом, используя InterlockedCompareExchangePointer
. Из-за этого трюка TObject
содержит только один размер данных указателя для поддержки TMonitor
, он не содержит полную структуру TMonitor. И это хорошо.
Эта структура TMonitor
содержит несколько записей. Начнем с поля FLockCount: Integer
. Когда первый поток использует TMonitor.Enter()
для любого объекта, это комбинированное поле счетчика будет иметь значение ZERO. Опять же, используя метод InterlockedCompareExchange
, происходит блокировка и инициируется счетчик. Не будет блокировки для вызывающего потока, нет контекстного переключателя, поскольку это все сделано в процессе.
Когда второй поток пытается TMonitor.Enter()
одного и того же объекта, первая попытка блокировки завершится с ошибкой. Когда это происходит, Delphi следует двум стратегиям:
- Если разработчик использовал
TMonitor.SetSpinCount()
для установки числа "спинов", то Delphi выполнит цикл "занятый-ждать", вращая заданное количество раз. Это очень приятно для крошечных замков, потому что позволяет получить блокировку без использования контекстного переключателя. - Если счетчик спинов истекает (или нет счетчика спина и по умолчанию отсчет спина нуля),
TMonitor.Enter()
инициирует ожидание события, возвращаемогоTMonitor.GetEvent()
. Другими словами, он не будет занят - ждите, теряя процессорные циклы. ПомнитеTMonitor.GetEvent()
, потому что это очень важно.
Скажем, у нас есть поток, который приобрел блокировку и поток, который пытался получить блокировку, но теперь ждет события, возвращаемого TMonitor.GetEvent
. Когда первый поток вызывает TMonitor.Exit()
, он замечает (через поле FLockCount
), что есть хотя бы одна другая блокировка потока. Таким образом, он немедленно запускает то, что обычно должно быть ранее выделенным событием (вызовы TMonitor.GetEvent()
). Но так как два потока, тот, который вызывает TMonitor.Exit()
, и тот, который вызвал TMonitor.Enter()
, может на самом деле называть TMonitor.GetEvent()
в то же время, tere - еще несколько трюков внутри TMonitor.GetEvent()
, чтобы убедиться, что только одно событие выделено, не имеет отношения к порядку операций.
Еще несколько интересных моментов мы рассмотрим, как работает TMonitor.GetEvent()
. Эта вещь находится внутри блока System
(вы знаете, тот, с которым мы не можем перекомпилировать, чтобы играть), но, оказывается, он делегирует обязанность фактически назначить событие другому устройству с помощью указателя System.MonitorSupport
. Это указывает на запись типа TMonitorSupport
, которая объявляет 5 указателей на функции:
-
NewSyncObject
- назначает новое событие для синхронизации. -
FreeSyncObject
- освобождает событие, назначенное для целей синхронизации -
NewWaitObject
- назначает новую операцию "Событие для ожидания" -
FreeWaitObject
- освобождает событие Wait -
WaitAndOrSignalObject
- ну.. ждет или сигналы.
Также оказывается, что объекты, возвращаемые функциями NewXYZ
, могут быть любыми, потому что они используются только для вызова WaitXYZ
и для соответствующего вызова FreeXyzObject
. Способ, которым эти функции реализованы в SysUtils
, предназначен для обеспечения этих блокировок минимальным количеством блокировки и переключения контекста; Из-за этого сами объекты (возвращаемые NewSyncObject
и NewWaitObject
) не являются непосредственно событиями, возвращаемыми CreateEvent()
, но указателями на записи в SyncEventCacheArray
. Это идет еще дальше, фактические события Windows не создаются до тех пор, пока это не потребуется. Из-за этого записи в SyncEventCacheArray
содержат пару записей:
-
TSyncEventItem.Lock
- это говорит, что Delphi скорее использует Lock для чего-либо прямо сейчас или нет, и -
TSyncEventItem.Event
- это содержит фактическое событие, которое будет использоваться для синхронизации, если требуется ожидание.
Когда приложение завершается, SysUtils.DoneMonitorSupport
просматривает все записи в SyncEventCacheArray
и ожидает, что Lock станет ZERO, то есть ожидает, что блокировка перестанет использоваться чем-либо. Теоретически, до тех пор, пока эта блокировка не равна нулю, по крайней мере один поток там может использовать блокировку - поэтому разумная задача - подождать, чтобы НЕ вызывать ошибки AccessViolations. И мы, наконец, дошли до нашего текущего вопроса: HANGING in SysUtils.DoneMonitorSupport
Почему приложение может зависать в SysUtils.DoneMonitorSupport, даже если все его потоки завершены правильно?
Поскольку по крайней мере одно событие, выделенное с использованием любого из NewSyncObject
или 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, который использовался для TMonitor.Enter()
.
Возможные решения:
К сожалению, мне это не нравится. Это не так, я имею в виду, что штраф за не освобождение маленького объекта должен быть небольшой утечкой памяти, а не висящим приложением! Это особенно плохо для приложений Service, где служба может просто зависать навсегда, а не полностью закрываться, но не может ответить на любой запрос.
Решения для команды Delphi? Они не должны вставлять код завершения модуля SysUtils
, неважно, что. Вероятно, они должны игнорировать Lock
и перейти к закрытию дескриптора события. На этом этапе (завершение работы блока SysUtils), если в каком-то потоке все еще работает код, он в очень плохом состоянии, так как большинство блоков завершено, оно не работает в среде, в которой он был предназначен для запуска.
Для пользователей delphi? Мы можем заменить MonitorSupport
на нашу собственную версию, которая не выполняет эти обширные тесты во время финализации.