Delphi中的匿名方法创建一个闭包,该闭包将上下文中的“局部”变量保持在环境中,直到匿名方法完成为止。如果使用接口(interface)变量,则它们将在匿名方法完成之前减少其引用的实例。到目前为止,一切都很好。
当将TTask.Run(AProc:TProc)与匿名方法一起使用时,我希望当关联的工作线程完成执行“AProc”时,将释放闭包。不过,这似乎没有发生。在程序终止时,当释放线程池(此TTask生成的线程所属的线程池)时,您最终可以看到释放了这些本地引用的实例-即,显然释放了闭包。
问题是这是功能还是错误?还是我在这里监督什么?
下面,在TTask.Run(...)。wait之后,我希望LFoo的析构函数被调用-不会发生。
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;
以下是一个完整的测试用例,它显示了一个“简单”匿名方法可以按预期工作(Test2),但是当将其输入TTask.Run时却无法正常工作(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.
最佳答案
我对该错误进行了更多分析,以找出known issue中提到的将ITask
保留在TThreadPool.TQueueWorkerThread.Execute
中的真正原因。
以下无辜的代码行就是问题所在:
Item := ThreadPool.FQueue.Dequeue;
为什么会这样?因为
TQueue<T>.Dequeue
标记为内联,现在您必须知道编译器不会对返回托管类型的内联函数应用所谓的return value optimization。这意味着编译器将前一行真正转换为代码(非常简化)。
tmp
是编译器生成的变量-它在方法的序言中在堆栈上保留空间:tmp := ThreadPool.FQueue.Dequeue;
Item := tmp;
该变量在方法的
end
中完成。您可以在其中放置一个断点,然后在TTask.Destroy
中放置一个断点,然后您会看到,当应用程序到达方法末尾时,它将终止,这将触发最后一个TTask
实例被破坏,因为清除了保持其 Activity 状态的temp变量。我使用了一些小技巧来本地解决此问题。我添加了此本地过程,以消除临时变量潜入
TThreadPool.TQueueWorkerThread.Execute
方法中:procedure InternalDequeue(var Item: IThreadPoolWorkItem);
begin
Item := ThreadPool.FQueue.Dequeue;
end;
然后更改方法中的代码:
InternalDequeue(Item);
这仍然会导致
Dequeue
产生一个临时变量,但是现在它仅存在于InternalDequeue
方法中,并且一旦退出便被清除。编辑(09.11.2017):此问题已在编译器的10.2中修复。现在,在将temp变量分配给实际变量之后,将插入一个finally块,因此temp变量不会导致额外的引用超出其应有的时间。
关于multithreading - AnonProc完成后未释放TTask.Run(AnonProc)中的关闭,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/40653204/