Почему мое многопоточное приложение иногда зависает при закрытии?

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

AFAIK это все работает правильно, за исключением того, что иногда приложение зависает при выходе. Мне интересно, связано ли это с моим использованием критических разделов.

Есть ли правильный способ освободить объекты TCriticalSection в деструкторе?

Спасибо за ответы на все вопросы. Я снова просматриваю свой код с учетом этой новой информации. Ура!

8 ответов

Решение

Как говорит Роб, единственное требование - убедиться, что критический раздел в данный момент не принадлежит ни одному потоку. Даже нить, собирающаяся его уничтожить. Таким образом, для правильного уничтожения TCriticalSection, как такового, не существует шаблона. Только обязательное поведение, которое ваше приложение должно предпринять, чтобы обеспечить его выполнение.

Если ваше приложение блокируется, то я сомневаюсь, что ответственным является освобождение любого критического раздела. Как говорится в MSDN (по ссылке, размещенной Робом), DeleteCriticalSection() (что, в конечном счете, является то, что освобождает вызовы TCriticalSection) не блокирует никакие потоки.

Если бы вы освобождали критический раздел, к которому другие потоки все еще пытались получить доступ, вы получили бы нарушения доступа и другие непредвиденные поведения, а не взаимоблокировки, поскольку этот небольшой пример кода должен помочь вам продемонстрировать:

implementation

uses
  syncobjs;


  type
    tworker = class(tthread)
    protected
      procedure Execute; override;
    end;


  var
    cs: TCriticalSection;
    worker: Tworker;


procedure TForm2.FormCreate(Sender: TObject);
begin
  cs := TCriticalSection.Create;

  worker := tworker.Create(true);
  worker.FreeOnTerminate := TRUE;
  worker.Start;

  sleep(5000);

  cs.Enter;

  showmessage('will AV before you see this');
end;

{ tworker }

procedure tworker.Execute;
begin
  inherited;
  cs.Free;
end;

Добавьте в модуль реализации формы, исправив ссылку "TForm2" для обработчика событий FormCreate(), как требуется.

В FormCreate () это создает критический раздел, а затем запускает поток, единственная цель которого - освободить этот раздел. Мы вводим задержку Sleep (), чтобы дать потоку время на инициализацию и выполнение, затем мы сами пытаемся войти в критическую секцию.

Мы не можем, конечно, потому что это было бесплатно. Но наш код не зависает - он не заходит в тупик, пытаясь получить доступ к ресурсу, которым владеет что-то другое, он просто взрывается, потому что мы пытаемся получить доступ к ресурсу, которого больше нет.

Вы могли бы быть еще более уверенными в создании AV в этом сценарии, если бы NIL ссылался на критический раздел, когда он свободен.

Теперь попробуйте изменить код FormCreate () следующим образом:

  cs := TCriticalSection.Create;

  worker := tworker.Create(true);
  worker.FreeOnTerminate := TRUE;

  cs.Enter;
  worker.Start;

  sleep(5000);

  cs.Leave;

  showmessage('appearances can be deceptive');

Это меняет дело... теперь главный поток станет владельцем критического раздела - рабочий поток освободит критический раздел, пока он все еще принадлежит основному потоку.

Однако в этом случае вызов cs.Leave не обязательно вызывает нарушение прав доступа. Все, что происходит в этом сценарии (afaict), состоит в том, что потоку-владельцу разрешено "покидать" раздел, как он и ожидал (это, конечно, не происходит, потому что раздел ушел, но потоку кажется, что он имеет покинул раздел, в который он вошел ранее) ...

... в более сложных сценариях возможно нарушение прав доступа или другая ошибка, так как память, ранее использовавшаяся для объекта критического раздела, может быть перераспределена на какой-то другой объект к тому времени, когда вы вызываете его метод Leave(), что приводит к некоторому вызов другого неизвестного объекта или доступ к недействительной памяти и т. д.

Опять же, изменение worker.Execute () таким образом, чтобы после освобождения он ссылался на критическую секцию NIL, гарантировало бы нарушение прав доступа при попытке вызвать cs.Leave(), поскольку Leave () вызывает Release () и Release () является виртуальным - вызов виртуального метода со ссылкой на NIL гарантированно AV (то же самое для Enter(), который вызывает виртуальный метод Acquire()) .

В любом случае:

В худшем случае: исключение или странное поведение

"Лучший" случай: владелец, кажется, считает, что он "покинул" раздел как обычно.

Ни в том, ни в другом случае не произойдет взаимоблокировка или зависание просто в результате того, что критическая секция свободна в одном потоке по сравнению с тем, когда другие потоки затем пытаются войти или выйти из этой критической секции.

Все это является грубым способом сказать, что у вас есть более фундаментальное состояние гонки в вашем поточном коде, не имеющее прямого отношения к освобождению ваших критических разделов.

В любом случае, я надеюсь, что моя небольшая следственная работа поможет вам выбрать правильный путь.

Просто убедитесь, что ничто все еще не владеет критическим разделом. В противном случае MSDN объясняет, что "состояние потоков, ожидающих владения удаленным критическим разделом, не определено". Кроме этого, позвоните Free на это, как вы делаете со всеми другими объектами.

Нет никакого волшебства в использовании TCriticalSection, а также в самих критических разделах. Попробуйте заменить объекты TCriticalSection простыми вызовами API:

uses
  Windows, ...

var
  CS: TRTLCriticalSection;

...

EnterCriticalSection(CS);
....
here goes your code that you have to protect from access by multiple threads simultaneously
...
LeaveCriticalSection(FCS);
...

initialization
  InitializeCriticalSection(CS);

finalization
  DeleteCriticalSection(CS);

Переход на API не повредит ясности вашего кода, но, возможно, поможет выявить скрытые ошибки.

AFAIK это все работает правильно, за исключением того, что иногда приложение зависает при выходе. Мне интересно, связано ли это с моим использованием критических разделов.

Да, это. Но проблема скорее всего не в разрушении. У тебя наверное тупик.

Тупиковые ситуации - это когда два потока ожидают двух эксклюзивных ресурсов, каждый из которых хочет иметь оба, а каждому - только один:

//Thread1:
FooLock.Enter;
BarLock.Enter;

//Thread2:
BarLock.Enter;
FooLock.Enter;

Чтобы бороться с этим, нужно заказать замки. Если какой-то поток хочет два из них, он должен вводить их только в определенном порядке:

//Thread1:
FooLock.Enter;
BarLock.Enter;

//Thread2:
FooLock.Enter;
BarLock.Enter;

Таким образом, тупик не возникнет.

Многие вещи могут вызвать тупик, не только ДВА критических раздела. Например, вы могли использовать SendMessage (синхронная отправка сообщений) или Delphi Synchronize AND один критический раздел:

//Thread1:
OnPaint:
  FooLock.Enter;
  FooLock.Leave;

//Thread2:
FooLock.Enter;
Synchronize(SomeProc);
FooLock.Leave;

Синхронизируйте и SendMessage отправляйте сообщения в Thread1. Чтобы отправить эти сообщения, Thread1 должен завершить любую работу, которую он выполняет. Например, обработчик OnPaint.

Но чтобы закончить рисование, ему нужен FooLock, который берется Thread2, который ждет, пока Thread1 завершит рисование. Тупик.

Способ решить эту проблему - либо никогда не использовать Synchronize и SendMessage (лучший способ), либо, по крайней мере, использовать их вне каких-либо блокировок.

Есть ли правильный способ освободить объекты TCriticalSection в деструкторе?

Неважно, где вы освобождаете TCriticalSection, в деструкторе или нет.

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

Например, если ваш поток входит в этот раздел при отправке сетевого сообщения, вы должны убедиться, что сеть отключена и все ожидающие сообщения обрабатываются.

Невыполнение этого требования в большинстве случаев вызывает нарушения доступа, иногда ничего (если вам повезет) и редко тупики.

Если единственный явный код синхронизации в вашем приложении проходит через критические разделы, то не должно быть слишком сложно отследить это.

Вы указываете, что видели только тупик при завершении. Конечно, это не означает, что это не может произойти во время нормальной работы вашего приложения, но я предполагаю (и мы должны догадаться без дополнительной информации), что это важный ключ.

Я бы предположил, что ошибка может быть связана с тем, как потоки принудительно завершаются. Такая тупиковая ситуация, как вы описали, возникла бы, если бы поток завершил работу, продолжая удерживать блокировку, но затем другой поток попытался получить блокировку до того, как у него появилась возможность завершить работу.

Очень простая вещь, которая может решить проблему немедленно, - убедиться, как правильно сказали другие, что все варианты использования блокировки защищены Try/ Наконец. Это действительно важный момент.

В Delphi есть два основных шаблона управления временем жизни ресурсов:

lock.Acquire;
Try
  DoSomething();
Finally
  lock.Release;
End;

Другим основным шаблоном является получение / освобождение пары в Create / Destroy, но это гораздо реже в случае блокировок.

Предполагая, что ваш шаблон использования для блокировок, как я подозреваю (т. Е. Для получения и выпуска внутри одного и того же метода), можете ли вы подтвердить, что все варианты использования защищены Try/ Наконец?

Вы должны защитить все критические разделы, используя блок try..finally.

Используйте TRTLCriticalSection вместо класса TCriticalSection. Он кроссплатформенный, а TCriticalSection - всего лишь ненужная оболочка вокруг него.

Если какое-либо исключение происходит во время обработки данных, критический раздел не остается, и другой поток может блокироваться.

Если вам нужен быстрый ответ, вы также можете использовать TryEnterCriticalSection для какого-либо процесса пользовательского интерфейса или чего-то подобного.

Вот некоторые правила хорошей практики:

  1. сделайте свой TRTLCriticalSection свойством класса;
  2. вызовите InitializeCriticalSection в конструкторе класса, затем DeleteCriticalSection в деструкторе класса;
  3. использовать EnterCriticalSection()... попытаться... сделать что-то... наконец LeaveCriticalSection(); конец;

Вот пример кода:

type
  TDataClass = class
  protected
    fLock: TRTLCriticalSection;
  public
    constructor Create;
    destructor Destroy; override;
    procedure SomeDataProcess;
  end;

constructor TDataClass.Create;
begin
  inherited;
  InitializeCriticalSection(fLock);
end;

destructor TDataClass.Destroy;
begin
  DeleteCriticalSection(fLock);
  inherited;
end;

procedure TDataClass.SomeDataProcess;
begin
  EnterCriticalSection(fLock);
  try
    // some data process
  finally
    LeaveCriticalSection(fLock);
  end;
end;

Если ваше приложение зависает / блокируется только при выходе, проверьте событие onterminate для всех потоков. Если основной поток сигнализирует о завершении других потоков, а затем ожидает их, прежде чем освободить их. Важно не выполнять синхронизированные вызовы в событии on terminate. Это может привести к мертвой блокировке, так как основной поток ожидает завершения рабочего потока. Но вызов синхронизации ожидает основной поток.

Не удаляйте критические секции в деструкторе объекта. Иногда может привести к сбою приложения.

Используйте отдельный метод, который удаляет критический раздел.

процедура someobject.deleteCritical();
начать
DeleteCriticalSection(CriticalSection);
конец;

деструктор someobject.destroy();
начать
// Выполняйте свои задачи по выпуску здесь
конец;

1) Вы звоните удалить критический раздел
2) После освобождения (бесплатно) объекта

Другие вопросы по тегам