Anonimowe metody w Delphi tworzą zamknięcie, które zachowuje "otaczające" zmienne lokalne w kontekście, aż do zakończenia anonimowej metody. Jeśli używasz zmiennych interfejsów, to zmniejszą one ich instancję, do której odwołujesz się, jeszcze przed zakończeniem anonimowej metody. Jak na razie dobrze.Zamknięcie w TTask.Run (AnonProc) nie zostało wydane po zakończeniu AnonProc
Podczas korzystania z metody TTask.Run (AProc: TProc) za pomocą metody anonimowej oczekiwałbym, że zamknięcie zostanie zwolnione, gdy powiązany wątek roboczy zakończy wykonywanie "AProc". Wydaje się, że to się nie zdarza. W przypadku zakończenia programu, gdy pulę wątków (do której należy ten wątek generowany przez TTask) zostanie zwrócona, można w końcu zobaczyć, że te lokalnie przywoływane instancje zostaną zwolnione - tj. Zamknięcie zostanie najwidoczniej zwolnione.
Pytanie brzmi, czy jest to funkcja czy błąd? Czy mogę tu coś nadzorować?
Poniżej, po TTask.Run (...). Czekaj, oczekuję wywołania destruktora LFoo - co nie ma miejsca.
procedure Test3;
var
LFoo: IFoo;
begin
LFoo := TFoo.Create;
TTask.Run(
procedure
begin
Something(LFoo);
end).Wait; // Wait for task to finish
//After TTask.Run has finished, it should let go LFoo out of scope - which it does not apprently.
end;
Poniżej znajduje się pełna przypadek testowy, który pokazuje, że „prosty” sposób anonimowy działa zgodnie z oczekiwaniami (Test2), ale kiedy podawany do TTask.Run nie robi (Test3)
program InterfaceBug;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.Classes,
System.SysUtils,
System.Threading;
type
//Simple Interface/Class
IFoo = interface(IInterface)
['{7B78D718-4BA1-44F2-86CB-DDD05EF2FC56}']
procedure Bar;
end;
TFoo = class(TInterfacedObject, IFoo)
public
constructor Create;
destructor Destroy; override;
procedure Bar;
end;
procedure TFoo.Bar;
begin
Writeln('Foo.Bar');
end;
constructor TFoo.Create;
begin
inherited;
Writeln('Foo.Create');
end;
destructor TFoo.Destroy;
begin
Writeln('Foo.Destroy');
inherited;
end;
procedure Something(const AFoo: IFoo);
begin
Writeln('Something');
AFoo.Bar;
end;
procedure Test1;
var
LFoo: IFoo;
begin
Writeln('Test1...');
LFoo := TFoo.Create;
Something(LFoo);
Writeln('Test1 done.');
//LFoo goes out od scope, and the destructor gets called
end;
procedure Test2;
var
LFoo: IFoo;
LProc: TProc;
begin
Writeln('Test2...');
LFoo := TFoo.Create;
LProc := procedure
begin
Something(LFoo);
end;
LProc();
Writeln('Test2 done.');
//LFoo goes out od scope, and the destructor gets called
end;
procedure Test3;
var
LFoo: IFoo;
begin
Writeln('Test3...');
LFoo := TFoo.Create;
TTask.Run(
procedure
begin
Something(LFoo);
end).Wait; // Wait for task to finish
//LFoo := nil; This would call TFoo's destructor,
//but it should get called automatically with LFoo going out of scope - which apparently does not happen!
Writeln('Test3 done.');
end;
begin
try
Test1; //works
Writeln;
Test2; //works
Writeln;
Test3; //fails
Writeln('--------');
Writeln('Expected: Three calls of Foo.Create and three corresponding ones of Foo.Destroy');
Writeln;
Writeln('Actual: The the third Foo.Destroy is missing and is executed when the program terminates, i.e. when the default ThreadPool gets destroyed.');
ReadLn;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Mam nadzieję, że twoje odkrycia przyspieszą rozwiązanie tego problemu. –
Biorąc pod uwagę, że używasz inline dla wydajności, prawdopodobnie powinieneś używać "var" zamiast "out", to uniknie niejawnego wywołania IntfClear (które w przeciwnym razie pokonałoby cały punkt użycia inline) –
@EricGrange Dobry punkt - czasami Nadal zapominam o tym, że Delphi oczyszcza przekazany param przed wywołaniem. W każdym razie FWIW nie używam inline w tym przypadku, RTL ma;) –