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

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

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

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

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


person Shannon Matthews    schedule 30.12.2010    source источник
comment
Проверьте стек вызовов зависшего потока, чтобы определить, что на самом деле вызывает зависание. Не просто гадать.   -  person wj32    schedule 30.12.2010
comment
Спасибо wj32, я не знал, что могу проверить стек вызовов. Гораздо лучше угадать.   -  person Shannon Matthews    schedule 31.12.2010


Ответы (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 в этом сценарии, обнулив ссылку на критический раздел, когда он свободен.

Теперь попробуйте изменить код 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 не обязательно вызывает нарушение прав доступа. Все, что происходит в этом сценарии (афакт), заключается в том, что потоку-владельцу разрешено «оставить» раздел, как он и ожидал (это, конечно, не так, потому что раздел исчез, но кажется к нитке, что она покинула раздел, в который ранее заходила)...

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

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

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

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

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

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

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

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

person Deltics    schedule 30.12.2010
comment
-1, потому что использование экземпляра TCriticalSection в качестве глобальной переменной было сделано глобальным, что является очень плохим дизайном, тем более для некоторого примера кода, чтобы показать правильный способ кодирования. - person Arnaud Bouchez; 30.12.2010
comment
@A.Bouchez на самом деле объявление критических секций глобальными переменными часто является отличным выбором дизайна. - person David Heffernan; 30.12.2010
comment
@David: Нет, почти никогда, потому что CS должен иметь тесную связь с данными, которые он защищает, которые, вероятно, также не будут глобальными. Блокировка также должна быть как можно более тонкой, и для предотвращения взаимоблокировок необходим строгий контроль над иерархией блокировки. Все это, кажется, указывает на то, что CS (в целом) более локален. - person mghie; 30.12.2010
comment
@mghie @A.Bouchez Я предлагаю пример общесистемной структуры ведения журнала или, возможно, ленивой инициализации с использованием блокировки с двойной проверкой для глобального синглтона. Некоторые вещи действительно лучше глобальных! Как бы то ни было, -1 на основе этого глупо, потому что код предназначен для иллюстрации другой точки зрения, а отсутствие глобальной переменной просто запутает сделанную точку. Давайте, ребята, посмотрите на большую картину здесь! - person David Heffernan; 30.12.2010
comment
@David: я согласен с тем, что голосование против было неуместным, однако я остаюсь при своем мнении. Я не согласен с вашим аргументом, что объявление критических секций глобальными переменными часто является отличным выбором дизайна - это может быть необходимо в редких случаях (например, глобальная структура ведения журнала не является одной из них), но тогда это неизбежное зло, не хороший выбор. - person mghie; 30.12.2010
comment
@mghie Как реализовать глобальную структуру ведения журналов без глобальных переменных? И, пожалуйста, не делайте вид, что переменные класса не являются глобальными переменными! - person David Heffernan; 30.12.2010
comment
@David: Ну, моя глобальная структура ведения журналов состоит из синглтона, в котором можно регистрировать и отменять регистрацию целей журнала. Объект создается и уничтожается в секциях initialization и finalization соответственно. Он содержит элемент CS для защиты списка целей журнала, но не может быть найден глобальный CS... - person mghie; 30.12.2010
comment
@mghie Существует глобальная блокировка, она содержится внутри вашего глобального синглтона, вы сами так сказали. - person David Heffernan; 30.12.2010
comment
@David: Разве вы не согласны с тем, что существует заметная разница между объявлением критических секций в качестве глобальных переменных и наличием критической секции в качестве (частного) члена класса, данные которого он защищает? Могут быть или не быть экземпляры этого класса, которые являются глобальными переменными, но это совершенно не относится к делу. - person mghie; 30.12.2010
comment
@mghie Когда защищаемая вещь становится более чем тривиально сложной, разумно сгруппировать все связанные части в объект. Я думаю, что частные/общедоступные проблемы обычно более важны для объектов синхронизации, использование которых должно тщательно контролироваться. Я лично не зацикливаюсь на глобальном/локальном. Посмотрите Application и Screen в VCL. Они глобальны, но так и должно быть, потому что они синглетоны. - person David Heffernan; 30.12.2010
comment
@ A Bouchez - код должен был продемонстрировать механизм, а не показать хороший выбор дизайна для критических секций в качестве глобальных переменных. - person Deltics; 30.12.2010
comment
@ All — в Delphi нет такой вещи, как глобальная переменная. Максимально возможная видимость — unit-interface. Это достаточно близко к тому, что другие языки называют глобальными, поэтому его часто путают как таковое. То, на что люди идут, чтобы скрыть тот факт, что иногда данные должны иметь такую ​​​​высокую видимость, позволяя себе спать по ночам, притворяясь, что они не используют глобальную переменную, смехотворно ... жаль бедных людей, которые должны поддерживать код и де-обфускировать спагетти одноэлементного кода управления только для того, чтобы понять, что Аааааа, это глобально! - person Deltics; 30.12.2010
comment
@All - дополнение ... обратите внимание, что шаблон singleton является синонимом ГЛОБАЛЬНОГО объекта (по определению - одна и только одна и все ссылки относятся к одному и тому же). Часто цитируемый шаблон синглтона на самом деле не требует наличия класса со сложным кодом управления временем жизни — такие вещи необходимы только в языках, которые специально не предоставляют переменные в первую очередь. Это усложнение шаблона, порожденное необходимостью в таких языках, стало путаться с правильным способом реализации шаблона, имхо. - person Deltics; 30.12.2010
comment
ГЛОБАЛЬНЫЕ переменные - ЗЛО! А если вы хотите, чтобы один и тот же процесс выполнялся параллельно? Сделать критический раздел локальным — хорошая практика. Только в целях тестирования может иметь смысл иметь 100% параллельную версию ваших данных, а затем запускать их в параллельном доступе в качестве стресс-теста. Во всех моих предыдущих проектах, когда я использовал какую-то ГЛОБАЛЬНУЮ переменную, позже выяснилось, что это был плохой выбор дизайна. Некоторые языки даже не позволяют этого. Что касается синглтона, это также обходной путь (доступ к консоли C из C++) сделал функцию. Плохая характеристика ИМХО. - person Arnaud Bouchez; 31.12.2010
comment
@A.B Я так понимаю, у вас есть политика никогда не использовать большое количество синглетонов в VCL. Нет, я так не думал. Посмотрите хотя бы на свой код. Что такое общедоступная переменная интерфейса в SynGdiPlus? - person David Heffernan; 31.12.2010
comment
@AB - Запрещение глобальных переменных - это плохой выбор дизайна (для языка), потому что он накладывает ограничения на разработчиков приложений: обратите внимание на синтаксическую многословность, которую этот выбор дизайна затем накладывает на такие языки, чтобы объявлять простые вещи, такие как символические константы, требующие произвольного класса контейнера . Глобалы сами по себе не хороши и не плохи. Только конкретное их использование может быть помечено как таковое. Догматически отвергать все их употребления как плохие просто лень, отходя в сторону от занятия мозгом, чтобы дать надлежащую оценку... - person Deltics; 01.01.2011
comment
@AB продолжение ... В результате часто получается код, который придерживается догматической идеи о хорошем дизайне, но на самом деле совсем не такой. Но автор может сразу отвергнуть любую подобную критику, потому что они не использовали плохие дизайнерские решения. Я работал с такими людьми - они никогда не используют with, они никогда не используют глобальные переменные, они никогда не делают всяких вещей. Включая написание приличного, понятного, поддерживаемого кода. - person Deltics; 01.01.2011
comment
@David Действительно, признаюсь, я использую глобальную, но это было обусловлено конструкцией внешней библиотеки GDI plus. Для чистой архитектуры Delphi вам придется избегать глобальных переменных. - person Arnaud Bouchez; 01.01.2011
comment
@Deltics Я согласен с тобой, конечно. Это зависит от цели... Дэвид заметил, что я использую глобальные переменные в некоторых своих модулях. Так что, как и в жизни, напишу НИКОГДА ЭТОГО НЕ ДЕЛАЙ, но все равно сделаю. Но реже, чем если бы я говорил себе, что могу это сделать. Как писал Сан-Паул: «Чего делаю, того не позволяю; чего хочу, того не делаю; а что ненавижу, то делаю. Римлянам 7:15 - person Arnaud Bouchez; 01.01.2011
comment
@Deltics @A.Bouchez Да, я тоже ненавижу догмы! Есть множество ситуаций, когда глобальные переменные ужасны, и ситуаций, когда они являются правильным выбором. Вам просто нужно использовать правильное решение для любой проблемы. Константы Java и C# представляют собой синтаксическую путаницу. - person David Heffernan; 01.01.2011
comment
@A.Bouchez @Deltics, похоже, у вас также есть патологическое отвращение к проверке вызовов GDI+ API на наличие ошибок. Это очень плохая практика. Вы должны перестать беспокоиться о вещах, которые не имеют значения (например, о догматических мнениях об использовании глобальных переменных), и сосредоточиться на вещах, которые имеют значение (например, о проверке кодов ошибок!) - person David Heffernan; 01.01.2011
comment
@ Дэвид Х - Ва? При чем здесь мой подход к GDI+ и кодам ошибок? И как это вообще актуально?! - person Deltics; 02.01.2011
comment
@deltics, ага, этот комментарий не был направлен непосредственно на вас, он должен был стать для вас копией! - person David Heffernan; 02.01.2011
comment
@David H: позвольте мне процитировать это еще раз За то, что я делаю, я не позволяю: за то, что я хотел бы, я этого не делаю; а что ненавижу, то делаю. Римлянам 7:15 В данном конкретном случае глобальные переменные вообще не нужны. Насчет проверки ошибок в SynGdiPlus вы совершенно правы: мы обрабатывали только основные ошибки (такие как отсутствующий файл или сбой создания объекта). Но здесь не место говорить об этом. Большинство оболочек GDI+ просто обновляют глобальное свойство состояния, не вызывая исключений для первого вида ошибок, и большая часть пользовательского кода не проверяет его. Тоже не идеальное решение. Добро пожаловать на наш специализированный форум. - person Arnaud Bouchez; 02.01.2011

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

person Rob Kennedy    schedule 30.12.2010

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

person himself    schedule 30.12.2010

В использовании 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 не повредит ясности вашего кода, но, возможно, поможет выявить скрытые ошибки.

person Eldar Isayev    schedule 30.12.2010
comment
трудно понять, как это может помочь. Код будет таким же ясным в любом случае. Как бы вы это ни делали, пара входа/выхода должна быть защищена блоком Try finally. - person David Heffernan; 30.12.2010
comment
Я повторяю, что Дэвид, а также устранение TCriticalSection удаляет один дополнительный инструмент отладки — возможность обнулить ссылку, чтобы выделить использование ссылки после того, как она была освобождена. - person Deltics; 30.12.2010

Вам НЕОБХОДИМО защитить все критические разделы с помощью блока try..finally.

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

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

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

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

  1. сделайте ваш TRTCriticalSection свойством класса;
  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;
person Arnaud Bouchez    schedule 30.12.2010
comment
+1 используйте Try/Finally, это важно, -1 замените TCriticalSection на TRTLCriticalSection, это вопрос личных предпочтений. - person David Heffernan; 30.12.2010
comment
Допустимо ли, чтобы один поток получал блокировку, а второй поток освобождал ее? - person Shannon Matthews; 31.12.2010
comment
@Shannon Проверьте официальную документацию LeaveCriticalSection: если поток вызывает LeaveCriticalSection, когда он не владеет указанным объектом критического раздела, возникает ошибка, которая может привести к тому, что другой поток, использующий EnterCriticalSection, будет ждать бесконечно. Так что это однозначно НЕ приемлемо. - person Arnaud Bouchez; 31.12.2010
comment
@David Как насчет объявления этого в классе, обрабатывающем данные? - person Arnaud Bouchez; 31.12.2010
comment
@AB, это часто хорошая практика, но сложность с такими простыми примерами, как этот, заключается в том, что трудно понять, где возникает необходимость в блокировке. - person David Heffernan; 31.12.2010
comment
@Shannon: Если вы зададите вопрос, допустимо ли, чтобы один поток получал блокировку, а второй поток освобождал ее? тогда вам следует на самом деле найти время, чтобы узнать больше об основах многопоточности. Если вы хотите, чтобы какой-то объект для сигнальных целей заглядывал в события и семафоры, критическая секция используется для совершенно других целей. Если вместо этого вы выпускаете CS из другого потока, отличного от того, в котором он был получен, это действительно может быть причиной ваших зависаний. - person mghie; 01.01.2011
comment
Хорошо, все критические разделы теперь защищены операторами Try/Finally. Предложение Mghie в другом комментарии помогло реорганизовать мой код. - person Shannon Matthews; 01.01.2011
comment
@Шеннон: поздравляю! Добро пожаловать в многопоточный мир. Всегда есть возможность улучшить дизайн. Это действительно хороший момент, когда вы спрашиваете совета, делаете свою идею из эксперимента, а затем исправляете свой код! :) - person Arnaud Bouchez; 02.01.2011
comment
@A.Bouchez: Спасибо! Мне еще многому нужно научиться, но я наслаждаюсь процессом. :) - person Shannon Matthews; 03.01.2011

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

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

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

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

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

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

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

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

person David Heffernan    schedule 30.12.2010
comment
Я не понимаю, как я могу использовать блоки Try/Finally в моей ситуации. Я использую поток для загрузки больших файлов, чтобы избежать блокировки других частей приложения. Замок говорит, что этот файл сейчас загружается, не прерывайте и не используйте данные. - person Shannon Matthews; 31.12.2010
comment
@shannon Я не понимаю, как ты делаешь такой вывод. Как вы можете быть уверены, что ваше приобретение будет сопряжено с выпуском, если вы не используете Try/Finally? - person David Heffernan; 31.12.2010
comment
@Shannon: см. мой другой комментарий. Блокировка не говорит ничего подобного, если вы хотите, чтобы вы смотрели на события и семафоры, или используйте блокировку только для защиты переменной, которая говорит, что файл загружается. - person mghie; 01.01.2011
comment
@Дэйвид. Хорошая точка зрения. Я работал в предположении, что мой код всегда будет работать без исключений. - person Shannon Matthews; 01.01.2011
comment
@mghie. Использование блокировки для защиты переменной имеет смысл. - person Shannon Matthews; 01.01.2011

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

person Mike Taylor    schedule 03.01.2011

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

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

процедура someobject.deleteCritical();
начало
УдалитьВажноеРаздел(критическоеРаздел);
конец;

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

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

person Dimitris    schedule 28.04.2013
comment
Не могли бы вы привести пример кода? Это облегчило бы понимание вашего ответа. - person Simon MᶜKenzie; 29.04.2013