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/

10-12 04:58