Приложение зависает в SysUtils → DoneMonitorSupport при выходе - программирование
Подтвердить что ты не робот

Приложение зависает в SysUtils → DoneMonitorSupport при выходе

Я пишу очень интенсивное приложение, зависающее при выходе.

Я проследил системные единицы и нашел место, где программа входит в бесконечный цикл. Он находится в строке SysUtils 19868 → DoneMonitorSupport CleanEventList:

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

Я искал решение в Интернете и нашел пару отчетов по контролю качества:

К сожалению, это не похоже на мою ситуацию, поскольку я не использую ни TThreadList, ни TMonitor.

Я уверен, что все мои потоки закончены и были уничтожены, так как все они наследуются от базового потока, который содержит счетчик create/destroy.

Кто-нибудь сталкивался с подобным поведением раньше? Знаете ли вы о каких-либо стратегиях для обнаружения того, где может быть основная причина?

4b9b3361

Ответ 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 на нашу собственную версию, которая не выполняет эти обширные тесты во время финализации.

Ответ 2

Я мог бы воспроизвести вашу проблему, используя пример, предоставленный Cosmin. Я также мог бы решить эту проблему, просто освободив SyncObj после завершения всех потоков.

Поскольку у меня нет доступа к вашему коду, я не могу сказать больше, но, вероятно, какой-то экземпляр объекта, используемый TMonitor, не освобождается.

Ответ 3

Я работал над ошибкой следующим образом:

Скопируйте System.SysUtils, InterlockedAPIs.inc и EncodingData.inc в каталог приложений и измените следующий код в системе .SysUtils

  procedure CleanEventList(var EventCache: array of TSyncEventItem);
  var
    I: Integer;
  begin
    for I := Low(EventCache) to High(EventCache) do
    begin
      if InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0 then
         DeleteSyncWaitObj(EventCache[I].Event);
      //repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0;
      //DeleteSyncWaitObj(EventCache[I].Event);
    end;
  end;

Я также добавил эту проверку в верхней части System.SysUtils, чтобы напомнить мне обновить файл System.SysUtils, если я изменю версии Delphi:

{$IFNDEF VER230}
!!!!!!!!!!!!!!!!
You need to update this unit to fix the bug at line 19868
See http://stackoverflow.com/questions/14217735/application-hangs-in-sysutils-donemonitorsupport-on-exit
!!!!!!!!!!!!!!!!
{$ENDIF}

После этих изменений мое приложение отключается правильно.

Примечание. Я попытался добавить "ReportMemoryLeaksOnShutdown", как предлагал LU RD, но при выключении мое приложение вошло в состояние гонки, вызывая многочисленные диалоги ошибок во время выполнения. Аналогичная ситуация случается, когда я пытаюсь использовать функцию утечки памяти EurekaLog.

Ответ 4

В Delphi XE5 Embarcadero решил это, добавив (Now - Start > 1 / MSecsPerDay) or в цикл repeat until в CleanEventList, чтобы он отказался после 1 миллисекунды. Затем он удаляет событие независимо от того, был ли Lock 0.