понедельник, 1 декабря 2008 г.

Как мне определить, что я владею критической секцией, если я не могу смотреть во внутренние поля?

Это перевод How do I determine whether I own a critical section if I am not supposed to look at internal fields? Автор: Реймонд Чен. Примечание: в отличие от других постов, этот пост сильно отличается от оригинала. Произведено множество замен от C к Delphi.

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

Я использую SEH (исключения - прим. пер.) и у меня есть несколько блоков try/except, в которых код входит и покидает критические секции. Если возникает исключение, я не знаю, владею ли я сейчас критической секцией или нет. Даже обёртка кода в try/finally не решает мои проблемы.

Ответ: вы знаете, владеете ли вы критической секцией, потому что вы сами вошли в неё.

Метод 1: сделать вывод из строки кода.
"Если я сейчас в этой строке кода, то я должен быть внутри критической секции".
  try
    ...
    EnterCriticalSection(X);
    try    
      ...    // если исключение возникнет на этом участке, ...
    finally  // ...то убедимся, что мы вышли из критической секции
      LeaveCriticalSection(X);
    end;
    ...
  except
    ...
  end;
Заметим, что эта техника устойчива к вложенным вызовам EnterCriticalSection. Если вы собираетесь войти в критическую секцию ещё раз, тогда просто оберните вложенный вызов в собственный блок try/finally.

Метод 2: сделать вывод из локального состояния.
"Я запомню, входил ли я в критическую секцию".
var
  Entered: Integer;
...
  Entered := 0;
  try
    ...
    EnterCriticalSection(X);
    Inc(Entered);
    ...
    Dec(Entered);
    LeaveCriticalSection(X);
    ...
  except
    while Entered > 0 do
    begin
      LeaveCriticalSection(X);
      Dec(Entered);
    end;
    ...
  end;
Заметим, что эта техника также устойчива к вложенным вызовам EnterCriticalSection. Если вы хотите занять критическую секцию ещё раз, то просто увеличьте Entered ещё раз.

Метод 3: отслеживать объектом.
Оберните TCriticalSection в другой объект.

Это наиболее точно передаёт то, что Seth делает сейчас.
type
  TMyCriticalSection = class(TSynchroObject)
  private
    FOwner: Cardinal;
    FDepth: Integer;
    function GetOwned: Boolean;
  protected
    FSection: TRTLCriticalSection;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Acquire; override;
    procedure Release; override;
    function TryEnter: Boolean;
    procedure Enter; inline;
    procedure Leave; inline;
    property Owned: Boolean read GetOwned;
  end;

{ TMyCriticalSection }

constructor TMyCriticalSection.Create;
begin
  inherited Create;
  FSection.Initialize;
end;

destructor TMyCriticalSection.Destroy;
begin
  FSection.Free;
  inherited Destroy;
end;

procedure TMyCriticalSection.Acquire;
begin
  FSection.Enter;
  FOwner := GetCurrentThreadId;
  Inc(FDepth);
end;

procedure TMyCriticalSection.Release;
begin
  Dec(FDepth);
  if FDepth = 0 then
    FOwner := 0;
  FSection.Leave;
end;

function TMyCriticalSection.TryEnter: Boolean;
begin
  Result := FSection.TryEnter;
  if Result then
  begin
    FOwner := GetCurrentThreadId;
    Inc(FDepth);
  end;
end;

procedure TMyCriticalSection.Enter;
begin
  Acquire;
end;

procedure TMyCriticalSection.Leave;
begin
  Release;
end;

function TMyCriticalSection.GetOwned: Boolean;
begin
  Result := (FOwner = GetCurrentThreadId);
end;

...

  try
    Assert(not CS.Owned);
    ...
    CS.Enter;
    ...
    CS.Leave;
    ...
  except
    if CS.Owned then
      CS.Leave;
  end;
Заметим, что этот код не устойчив к повторной входимости (и, соответственно, код Seth-а тоже). Если вы войдёте в критическую секцию дважды, то обработчик исключения выйдет из неё только 1 раз.

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

Метод 4: отслеживать умным объектом.
Оберните TCriticalSection в умный объект.

Добавьте такой private-метод с public-свойством к предыдущему классу:
function TMyCriticalSection.GetDepth: Integer;
begin
  if Owned then
    Result := FDepth
  else
    Result := 0;
end;
Теперь вы можете корректно освобождать вложенные входы в критическую секцию:
var
  Depth: Integer;
...
  Depth := CS.Depth;
  try
    ...
    CS.Enter;
    ...
    CS.Leave;
    ...
  except
    while CS.Depth > Depth do
      CS.Leave;
  end;

Замечу, что я вообще скептически отношусь к изначальному вопросу.

Очистка после исключения, возбуждённого в коде, владеющим критической секцией, поднимает вопрос: "как вы узнаете, что именно безопасно очищать?". Вы завели критическую секцию для работы с какими-то данными. Критическая секция защищает ваши данные, пока вы изменяете их, т.е. переводите их в некоторое нестабильное состояние и не хотите, чтобы другие видели эти данные в такой несогласованной форме. Но если у вас появляется исключение во время работы внутри критической секции - ну, ваши данные в момент возбуждения исключения находятся в некорректном состоянии. Простой выход из критической секции оставит ваши данные в недопустимом состоянии, которое может привести к более трудно диагностируемым проблемам потом: "как это мой счётчик сумел сбиться?".

В одном из будущих постов я выскажу ещё претензии к исключениям.

Упражнение: почему нам не нужно использовать синхронизацию для защиты использования FDepth и FOwner?

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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