Часто можно встретить рекомендацию, что финализация должна быть всегда успешна.
Ну, в том смысле, что в процессе финализации не должно случаться исключений. А если встречаются, они там же (в процессе финализации) должны обрабатываться.
Пост о безуспешной попытке выйти за рамки ограничений...
Как видно, в процедуре Execute создаётся три связанных с интерфейсными переменными объектов, после чего для каждого из них вызывается Commit или Rollback - не существенно, поскольку реализация этих методов идентична - в True выставляется поле данных FFinished, на значение которого есть реакция в деструкторе: если FFinished = False - возбуждается исключение.
trs2.Commit обложено условной компиляцией, что позволит легко увидеть поведение как в штатном случае, когда Commit/Rollback вызваны для всех объявленных транзакций, так и в случае, когда это требование нарушено (trs2 в случае включённого определения TestTransactionLeak окажется "провисшей").
Теперь давайте рассмотрим, как программа сработает в обычной ситуации, когда определение TestTransactionLeak отсутствует. - Не произойдёт ничего интересного, вывод будет таким:
Всё ожидаемо, созданные потомки TInterfacedObject закрываются в порядке, обратном последовательности их определения в блоке локальных переменных.
Теперь посмотрим, как будет выглядеть вывод программы в случае, если определение TestTransactionLeak включено:
Здесь уже интереснее. При освобождении trs2 ожидаемо произошло исключение, но оно привело к тому, что экземпляр trs1 оказался не освобождён. Т.е. в данном случае мы имеем утечку памяти.
Ну, в том смысле, что в процессе финализации не должно случаться исключений. А если встречаются, они там же (в процессе финализации) должны обрабатываться.
Пост о безуспешной попытке выйти за рамки ограничений...
Проблема
Всем известно, что стартованная транзакция должна быть так или иначе завершена: подтверждена (COMMIT), либо отменена (ROLLBACK). К сожалению, если алгоритм изменения данных достаточно сложен и разветвлён,а главное - поскольку все мы люди, гарантировать завершение транзакции нельзя.
Если транзакция не завершена, она может стать причиной разного рода проблем, среди которых можно назвать и потерю данных и deadlock.
Соответственно, возникает желание выявлять такие "провисшие" транзакции в приложении, протоколировать обнаруженную проблему и, разумеется информировать о ней пользователя.
В свете этого возникло желание воспользоваться свойством интерфейсов, состоящим в том, что соответствующие им объекты (например, потомки TInterfacedObject) автоматически освобождаются при выходе за рамки области их определения.
Если таким объектом сделать транзакцию, то в случае, если она стартована, но до её освобождения к ней не применён Commit или RollBack, можно возбудить исключение, что код, в котором она оказалась задействована, написан некорректно.
Пост не о транзакциях, а о том, что технику основанную на исключениях, вероятно, не следует использовать для подобных целей.
Код
Рассмотрим следующий, предельно упрощённый пример.
{$apptype console} {$define TestTransactionLeak} program Project1; uses System.SysUtils; type ETransaction_LeakDetected = class(Exception); IDBTransaction = interface ['{DBFA2E38-22FA-4DBD-A849-2DA71FEE522A}'] procedure Commit; procedure Rollback; end; TDBTransaction = class(TInterfacedObject, IDBTransaction) private FName: String; FFinished: Boolean; procedure Log(const AMessage: String); public // <-- IDBTransaction procedure Commit; procedure Rollback; // IDBTransaction --> constructor Create(const AName: String); destructor Destroy; override; end; constructor TDBTransaction.Create(const AName: String); begin inherited Create; FName := AName; Log('created'); end; resourcestring STransactionWithName = 'Transaction "%s": %s.'; destructor TDBTransaction.Destroy; begin Log('destroy'); inherited; if not FFinished then raise ETransaction_LeakDetected.CreateFmt(STransactionWithName, [FName, 'leak detected']); end; procedure TDBTransaction.Log(const AMessage: String); begin WriteLn(Format(STransactionWithName, [FName, AMessage])); end; procedure TDBTransaction.Commit; begin FFinished := True; end; procedure TDBTransaction.Rollback; begin FFinished := True; end; procedure Execute; var trs1, trs2, trs3: IDBTransaction; begin trs1 := TDBTransaction.Create('1'); trs2 := TDBTransaction.Create('2'); trs3 := TDBTransaction.Create('3'); trs1.Commit; {$ifndef TestTransactionLeak} trs2.Commit; {$endif} trs3.Rollback; end; begin try Execute; WriteLn('Done.'); except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; ReadLn; end.
Как видно, в процедуре Execute создаётся три связанных с интерфейсными переменными объектов, после чего для каждого из них вызывается Commit или Rollback - не существенно, поскольку реализация этих методов идентична - в True выставляется поле данных FFinished, на значение которого есть реакция в деструкторе: если FFinished = False - возбуждается исключение.
trs2.Commit обложено условной компиляцией, что позволит легко увидеть поведение как в штатном случае, когда Commit/Rollback вызваны для всех объявленных транзакций, так и в случае, когда это требование нарушено (trs2 в случае включённого определения TestTransactionLeak окажется "провисшей").
Теперь давайте рассмотрим, как программа сработает в обычной ситуации, когда определение TestTransactionLeak отсутствует. - Не произойдёт ничего интересного, вывод будет таким:
Transaction "1": created. Transaction "2": created. Transaction "3": created. Transaction "3": destroy. Transaction "2": destroy. Transaction "1": destroy. Done.
Всё ожидаемо, созданные потомки TInterfacedObject закрываются в порядке, обратном последовательности их определения в блоке локальных переменных.
Теперь посмотрим, как будет выглядеть вывод программы в случае, если определение TestTransactionLeak включено:
Transaction "1": created. Transaction "2": created. Transaction "3": created. Transaction "3": destroy. Transaction "2": destroy. ETransaction_LeakDetected: Transaction "2": leak detected.
Здесь уже интереснее. При освобождении trs2 ожидаемо произошло исключение, но оно привело к тому, что экземпляр trs1 оказался не освобождён. Т.е. в данном случае мы имеем утечку памяти.
Предварительные выводы
Что мы имеем. В случае исключения в деструкторе объекта, освобождаемого интерфейсной ссылкой, в общем случае мы получим утечку памяти для других интерфейсных ссылок. Причём, не видится пути устранить утечку, кроме как писать "лесенку" try..finally и помещать в блок finally присваивание nil интерфейсным ссылкам.
Утечку памяти мы устраним, но это очевидно неудобно, да и логика работы с интерфейсными ссылками подразумевает управление ими на уровне компилятора.
Получается, компилятор подставляет код, обеспечивающий финализацию интерфейсных ссылок, но не гарантирует её, поскольку любое необработанное исключение приведёт прекращению процесса финализации.
Не знаю как Вам, но мне такое поведение не представляется правильным, поскольку компилятор выполняет только часть работы, и на то что он обеспечивает, нельзя полагаться.
В сущности, описанное поведение наводит на некоторые размышления.
1. Поскольку необработанные исключения в деструкторах столь разрушительны, что их нельзя допускать ни при каких обстоятельствах, это накладывает на разработчика обеспечение гарантий, что необработанные исключения в деструкторе не возникнут.
Это, в свою очередь, "легитимизирут" такую технику работы с ресурсами, в правильности которой я долго сомневался, хотя и использовал её очень часто. Обратите внимание на последовательные вызовы Free в блоке finally:
instance1 := nil; instance2 := nil; try instance1 := TSomeClass.Create; ... ... ... instance1 := TSomeClass.Create; ... ... ... finally instance2.Free; instance1.Free; end;
и даже более радикальный вариант:
var lvr: record instance1, instance2: TSomeClass; ... ... ... end; begin FillChar(lvr, SizeOf(lvr), 0); with lvr do try instance1 := TSomeClass.Create; ... ... ... instance1 := TSomeClass.Create; ... ... ... finally instance2.Free; instance1.Free; end; end;
Действительно, если исключений в деструкторах быть не должно ни при каких обстоятельствах, если даже механизмы уровня компилятора не избавляют от утечек ресурсов при необработанных исключениях в деструкторах, если разработчик должен гарантировать отсутствие таких необработанных исключений, значит - вполне можно "не заморачиваться" насчёт того, что два вызова Free идут последовательно в блоке finally.
2. Интерфейсные ссылки вполне подходят для того, чтобы контролировать инспекцию важных объектов, но надеяться на возможность генерации исключения о проблеме в самом, казалось бы, удобном для этого месте - не приходится.
Действительно, возвращаясь к примеру с транзакциями, ввиду невозможности безопасного (с точки зрения освобождения ресурсов) возбуждения исключения в деструкторе касса TDBTransaction, приходится заниматься разработкой инфраструктуры, которая должна будет обеспечить выдачу пользователю сообщения, а также генерации исключения при попытке продолжить изменение данных.
А это - не самая простая задача.
3. При написании деструкторов приходится постоянно помнить о том, что необработанные исключения в них неприемлемы. А забыть об этом очень легко.
Например, если бросить взгляд на TCustomConnection.Destroy, можно обнаржить там такой код:
destructor TCustomConnection.Destroy; begin inherited Destroy; SetConnected(False); FreeAndNil(FConnectEvents); FreeAndNil(FClients); FreeAndNil(FDataSets); end;
SetConnected - виртуальный метод, который может быть перекрыт в потомках, кроме того, этот метод может вызываться и вне контекста Destroy.
В сущности, упоминание его в деструкторе без соответствующего окружения try..except соответствует контракту, что этот метод не вызовет исключения, что очевидно, некорректное ограничение.
Системная проблема в том, что когда мы пишем деструктор, мы можем не знать, какое наше действие вызовет исключение. Подавлять все исключения без разбора - понятное дело - малоприемлемо.
Таким образом, недоработка в финализации объектов приводит к необходимости "городить огороды", чего по соображениям эргономики никто не станет, что снижает стабильность приложений, разработанных в Delphi.
Как должно было бы быть
Если говорить о финализации интерфейсных ссылок, оптимальным мне представлялся бы вариант, когда компилятором обеспечивалось гарантированное разрушение всех соответствующих объектов. Приблизительно так, как обрабатываются исключения в контсрукторах.
Конечно же, возникающие при финализации исключения ни в коем случае не должны "замалчиваться", просто их следует возбудить уже после того, как финализация завершена.
!!! "В сущности, упоминание его в деструкторе без соответствующего окружения try..except соответствует контракту, что этот метод не вызовет исключения, что очевидно, некорректное ограничение." :-)
ОтветитьУдалитьДа, Александр :-) Таково моё мнение.
УдалитьСпасибо, что обратили внимание на это сообщение.
Возможно, Вас позабавит следующий фрагмент из модуля System, который работает в наших условиях уже лет 15:
<code>
threadvar THREAD_VAR_ObjectBeforeFree: tObjectFunc;
function TObject.Free: Boolean;
var
CallDestroy: procedure(Self: Pointer; Options: Longint);
E: Pointer;
begin
Result := False;
E := nil;
if Self = nil then
Exit;
if Assigned(THREAD_VAR_ObjectBeforeFree) then
begin
try
Result := THREAD_VAR_ObjectBeforeFree(Self);
except
E := AcquireExceptionObject;
end;
SuspendException(E);
if not Result then
Exit;
end
else
Result := True;
asm
PUSH ECX
MOV ECX, Self
MOV ECX, [ECX]
MOV ECX, DWORD PTR [ECX] + VMTOFFSET TObject.Destroy
MOV CallDestroy, ECX
POP ECX
end;
try
try
try
BeforeDestruction;
finally
__FinallyCall;
CallDestroy(Self, $81);
end;
finally
__FinallyCall;
FreeInstance;
end;
except
E := AcquireExceptionObject;
end;
SuspendException(E);
end;
<\code>
Это не пример того, как следует поступать в данном случае - скорее, попытка защититься от реального положения дел. Тщетная к тому же...
Код писал не я, хотя в конце 90-х я оказал влияние на его появление. Увы, от исключений в деструкторах он, возможно, до какой-то степени и защищает, но это - не системное решение проблемы. Системно же, может оказаться, что от такого решения больше вреда чем пользы...
"Да, Александр :-) Таково моё мнение"
УдалитьТак ОЧЕНЬ правильное МНЕНИЕ :-) Его бы где-нибудь "в камне высечь". Например на уровне компилятора :-)