пятница, 29 августа 2014 г.

Ссылка. Получение ресурса есть инициализация (RAII). И "немного от себя"

https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5_%D1%80%D0%B5%D1%81%D1%83%D1%80%D1%81%D0%B0_%D0%B5%D1%81%D1%82%D1%8C_%D0%B8%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F

Хочется написать "что-то умное", поэтому попробую добавить "немного от себя".

Приведу "надуманный пример". Но надеюсь, что он будет понят.

(Предвижу вопрос - "зачем передавать критическую секцию". Отвечу - "ни зачем". Но я и такое в реальном коде видал. В чужом)

Итак.

Пусть есть такой код:

type
 TA = class
  private
   fCS : TCriticalSection;
   procedure SomeInitCode;
   procedure SomeDoneCode;
  public
   constructor Create(aCS: TCriticalSection);
   destructor Destroy; overide;
 end;//TA
...
constructor TA.Create(aCS: TCriticalSection);
begin
 inherited Create;
 fCS := aCS;
 SomeInitCode;
 fCS.Enter;
end;

destructor TA.Destroy;
begin
 fCS.Leave;
 fCS := nil;
 SomeDoneCode;
 inherited;
end;

procedure TA.SomeInitCode;
begin
 raise Exception.Create('Some fake error');
end;

procedure TA.SomeDoneCode;
begin
 // - do nothing
end;
...
var
 A : TA;
begin
 A := TA.Create(SomeCriticalSection);
 ...
end;

Приведу ещё ссылки:

- http://www.delphimaster.net/view/14-1089121849/all
- http://www.rsdn.ru/forum/delphi/411835.flat
- http://objectmix.com/delphi/402814-exception-constructor.html

Из последней ссылки процитирую:

"No, the destructor is automatically called when an exception is raised
in the constructor."

Это написано и в документации по Delphi, но к сожалению ссылку в интернете - я не нашёл.

Поверьте мне "на слово".

Процитирую лишь место из help к Delphi XE6:

"When an exception is raised during the creation of an object, Destroy is automatically called to dispose of the unfinished object. This means that Destroy must be prepared to dispose of partially constructed objects. Because a constructor sets the fields of a new object to zero or empty values before performing other actions, class-type and pointer-type fields in a partially constructed object are always nil. A destructor should therefore check for nil values before operating on class-type or pointer-type fields. Calling the Free method (defined in TObject) rather than Destroy offers a convenient way to check for nil values before destroying an object."

- что мы тут получим?

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

И это вообще говоря - проблема. Подробности зависят от версии Windows.

Что можно сделать?

Можно например написать так:

type
 TA = class
  private
   fCS : TCriticalSection;
   procedure SomeInitCode;
   procedure SomeDoneCode;
  public
   constructor Create(aCS: TCriticalSection);
   destructor Destroy; overide;
 end;//TA
...
constructor TA.Create(aCS: TCriticalSection);
begin
 inherited Create;
 fCS := aCS;
 fCS.Enter;
 SomeInitCode;
end;

destructor TA.Destroy;
begin
 SomeDoneCode;
 fCS.Leave;
 fCS := nil;
 inherited;
end;

procedure TA.SomeInitCode;
begin
 raise Exception.Create('Some fake error');
end;

procedure TA.SomeDoneCode;
begin
 // - do nothing
end;
...
var
 A : TA;
begin
 A := TA.Create(SomeCriticalSection);
 ...
end;

- что мы тут сделали?

Мы поменяли местами строчки:
SomeInitCode;
fCS.Enter;

- тут "всё срастётся".

Но повторю - "пример надуманный".

В реальном коде - "может и не срастись". Да и не факт, что "ресурс не захвачен" и SomeInitCode как раз и приведёт к его освобождению.

Это тоже - "звучит бредом", но так в "реальной жизни" - тоже бывает.

Тут тоже можно сказать - "присваивайте fCS непосредственно перед fCS.Enter".

Можно!

Но повторю - "пример надуманный".

Как "я считаю" было бы правильно?

А вот примерно так:
(Об "оверхеде" на время - забудем)

type
 TLocker = class
  private
   fCS : TCriticalSection;
  public
   constructor Create(aCS: TCriticalSection);
   destructor Destroy; overide;
 end;//TLocker

 TA = class
  private
   fCS : TLocker;
   procedure SomeInitCode;
   procedure SomeDoneCode;
  public
   constructor Create(aCS: TCriticalSection);
   destructor Destroy; overide;
 end;//TA
...
constructor TLocker.Create(aCS: TCriticalSection);
begin
 inherited Create;
 fCS := aCS;
 fCS.Enter;
end;

destructor TLocker.Destroy;
begin
 fCS.Leave;
 fCS := nil;
 inherited;
end;

constructor TA.Create(aCS: TCriticalSection);
begin
 inherited Create;
 SomeInitCode;
 fCS := TLocker.Create(aCS);
end;

destructor TA.Destroy;
begin
 FreeAndNil(fCS);
 SomeDoneCode;
 inherited;
end;

procedure TA.SomeInitCode;
begin
 raise Exception.Create('Some fake error');
end;

procedure TA.SomeDoneCode;
begin
 // - do nothing
end;
...
var
 A : TA;
begin
 A := TA.Create(SomeCriticalSection);
 ...
end;

- тут "всё срастается".

Всё?

Да нет - не всё.

Напишем так:

type
 TLocker = class
  private
   fCS : TCriticalSection;
  public
   constructor Create(aCS: TCriticalSection);
   destructor Destroy; overide;
 end;//TLocker

 TA = class
  private
   fCS : TLocker;
   procedure SomeInitCode;
   procedure SomeDoneCode;
  public
   constructor Create(aCS: TCriticalSection);
   destructor Destroy; overide;
 end;//TA
...
constructor TLocker.Create(aCS: TCriticalSection);
begin
 inherited Create;
 fCS := aCS;
 fCS.Enter;
end;

destructor TLocker.Destroy;
begin
 fCS.Leave;
 fCS := nil;
 inherited;
end;

constructor TA.Create(aCS: TCriticalSection);
begin
 inherited Create;
 fCS := TLocker.Create(aCS);
 SomeInitCode;
end;

destructor TA.Destroy;
begin
 SomeDoneCode;
 FreeAndNil(fCS);
 inherited;
end;

procedure TA.SomeInitCode;
begin
 // - do nothing
end;

procedure TA.SomeDoneCode;
begin
 raise Exception.Create('Some fake error');
end;
...
var
 A : TA;
begin
 A := TA.Create(SomeCriticalSection);
 ...
end;

- что мы сделали?

Мы перенесли исключение из SomeInitCode в SomeDoneCode.

И опять переставили местами: SomeInitCode; fCS := TLocker.Create(aCS);

Как было рассказано "о решении проблемы" ранее.

- в чём проблема?

А в том, что до строчки:
FreeAndNil(fCS); - мы не дойдём.

И критическую секцию - мы не освободим.

Это понятно? Или я что-то напутал?

Как можно "поправить ошибку"?

Можно написать так:

destructor TA.Destroy;
begin
 try
  SomeDoneCode;
 finally
  try
   FreeAndNil(fCS);
  finally
   inherited;
  end;
 end;
end;

- так опять - "все срастётся".

Всё? Вроде - да.

Но! "Эта ужасная лестница из try".

Что можно сделать?

Однозначного рецепта у меня - нет.

Но!

Могу предложить - "лишь беглый взгляд". Он - далеко не совершенный.

Но это путь - "куда думать" и отчасти ответ на вопрос - "почему Embarcadero так настойчиво продвигает ARC". Хотя я сам с этим и не согласен (Про ARC).

Но! Один из вариантов:

type
 ILocker = interface(IUnknown)
 end;//ILocker

 TLocker = class(TInterfacedObject, ILocker)
  private
   fCS : TCriticalSection;
  public
   constructor Create(aCS: TCriticalSection);
   destructor Destroy; overide;
 end;//TLocker

 TA = class
  private
   fCS : ILocker;
   procedure SomeInitCode;
   procedure SomeDoneCode;
  public
   constructor Create(aCS: TCriticalSection);
   destructor Destroy; overide;
 end;//TA
...
constructor TLocker.Create(aCS: TCriticalSection);
begin
 inherited Create;
 fCS := aCS;
 fCS.Enter;
end;

destructor TLocker.Destroy;
begin
 fCS.Leave;
 fCS := nil;
 inherited;
end;

constructor TA.Create(aCS: TCriticalSection);
begin
 inherited Create;
 fCS := TLocker.Create(aCS);
 SomeInitCode;
end;

destructor TA.Destroy;
begin
 SomeDoneCode;
 fCS := nil;
 inherited;
end;

procedure TA.SomeInitCode;
begin
 // - do nothing
end;

procedure TA.SomeDoneCode;
begin
 raise Exception.Create('Some fake error');
end;
...
var
 A : TA;
begin
 A := TA.Create(SomeCriticalSection);
 ...
end;

Что мы получили?

Мы получили тот факт, что fCS - будет однозначно освобождён.
И как следствие - мы попадём в fCS.Leave;

Зачем написана вот эта строчка:
fCS := nil;
?

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

Криво? Да - "местами криво".

Но - "лучше, чем ничего".

Какое решение является серебряной пулей?

Я пока - не знаю.

Есть один вариант.

Он описан тут - Черновик. Написать о том как использование "шаблонов" и "примесей" избавляет от "косвенности" и лишнего распределения памяти

На что стоит обратить внимание?

А вот на это:

constructor Tm3CustomHeaderStream.Create(const AStream: IStream;
                                         const AAccess: LongInt);
begin
 inherited;
 m2InitOperation(_Status,InitProc00000001($00000001));
 m2InitOperation(_Status,InitProc00000002($00000002));
 m2InitOperation(_Status,InitProc00000004($00000004));
 m2InitOperation(_Status,InitProc00000008($00000008));
 m2InitOperation(_Status,InitProc00000010($00000010));
end;
 
procedure Tm3CustomHeaderStream.Cleanup;
begin
 m2DoneOperation(_Status,$00000010,DoneProc00000010);
 m2DoneOperation(_Status,$00000008,DoneProc00000008);
 m2DoneOperation(_Status,$00000004,DoneProc00000004);
 m2DoneOperation(_Status,$00000002,DoneProc00000002);
 m2DoneOperation(_Status,$00000001,DoneProc00000001);
 inherited;
end;
...
procedure m2InitOperation(var   AStatus: LongWord;
                          const ABitMask: LongWord);
begin
 Assert((AStatus and ABitMask) = 0);
 AStatus:=AStatus or ABitMask;
end;

procedure m2DoneOperation(var   AStatus: LongWord;
                          const ABitMask: LongWord;
                          const AClassDoneProc: Tm2ClassDoneProc);
begin
 if ((AStatus and ABitMask) <> 0) then
 begin
  try
   AClassDoneProc();
  except
   m2ExcErrHandler();
  end;
  AStatus:=AStatus and not(ABitMask);
 end;
end;

Что тут написано?

А вот что:

В конструкторе вызываются процедуры вида InitProcXXX и они взводят в "маске состояния" биты означающие, что "этот метод был вызван".
А в деструкторе вызываются процедуры вида DoneProcXXX. И сбрасывают биты в "маске состояний".

Причём про DoneProcXXX есть два момента:

1. Они вызываются только если взведён соответствующий бит.
2. Они обрамлены блоком try..except. Т.е. они позволяют пройти следующим процедурам DoneProcXXX даже если в предыдущих произошло исключение.

Что сказать?

Этот вариант - железобетонный. И в нём - проблем нет.

И - не я его придумал. А другие умные люди.

Он реально - железобетонный.

Но!

Чем он мне не нравится?

А тем, что он не читабельный. Вообще.

Не знаю кому как, но от него мне лично - крышу рвёт. Но он - работает.

Когда подобный код "генерируется из UML" (Зачем UML) то это "куда ни шло.

А если это "руками писать" и потом читать - это - беда. Но зато - работает.

В общем - подведу итоги. Проблему я обозначил - "возбуждение исключений в конструкторах и деструкторах" и как следствие - неполная инициализация и деинициализация объектов. И как следствие - негарантированное получение/освобождение ресурсов.

Свои пути решения - я также перечислил. Они - не идеальны. Но! "Лучше чем ничего".

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

Спасибо за внимание. Надеюсь, что "что-то умное" написать таки - удалось.

P.S. Кстати есть "ещё два слова" - AfterConstruction и BeforeDestruction.

Процитирую документацию к Delphi:

"Responds after the last constructor has executed.
AfterConstruction is called automatically after the object's last constructor has executed. Do not call it explicitly in your applications.
The AfterConstruction method implemented in TObject does nothing. Override this method when creating a class that performs an action after the object is created. For example, TCustomForm overrides AfterConstruction to generate an OnCreate event."

"Responds before the first destructor executes.
BeforeDestruction is called automatically before the object's first destructor executes. Do not call it explicitly in your applications.
The BeforeDestruction method implemented in TObject does nothing. Override this method when creating a class that performs an action before the object is destroyed. For example, TCustomForm overrides BeforeDestruction to generate an OnDestroy event.
Note: BeforeDestruction is not called when the object is destroyed before it is fully constructed. That is, if the object's constructor raises an exception, the destructor is called to dispose of the object, but BeforeDestruction is not called. "
Внимание стоит обратить вот на что:

"Note: BeforeDestruction is not called when the object is destroyed before it is fully constructed. That is, if the object's constructor raises an exception, the destructor is called to dispose of the object, but BeforeDestruction is not called."

sic!

P.P.S. И вот ещё связанная вещь - BeforeRelease.

Ну и вот ещё ссылки:

http://18delphi.blogspot.ru/2013/04/3.html
http://18delphi.blogspot.ru/2013/04/iunknown.html

P.P.P.S. Есть ещё одна вещь - AutoPtr - они "сходны подсчёту ссылок", но имеют "особенное применение". Свои мысли об "умных указателях" для Delphi я попробую как-нибудь потом рассказать.

1 комментарий:

  1. Про ARC интересная мысль, действительно многое объясняет.

    ОтветитьУдалить