Подтвердить что ты не робот

Закрытие в TTask.Run(AnonProc) не выпущено после завершения AnonProc

Анонимные методы в Delphi создают закрытие, которое сохраняет "окружающие" локальные переменные в контексте, пока анонимный метод не завершится. Если использовать переменные интерфейса, то они уменьшат свой экземпляр ссылок до того, как анонимный метод завершится. Пока все хорошо.

При использовании 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 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.
4b9b3361

Ответ 1

Я сделал еще один анализ этой ошибки, чтобы узнать реальную причину, по которой ITask проводился в TThreadPool.TQueueWorkerThread.Execute, как упоминалось в вопрос.

Следующая невидимая строка кода - проблема:

Item := ThreadPool.FQueue.Dequeue;

Почему это так? Поскольку TQueue<T>.Dequeue помечен как встроенный, и теперь вы должны знать, что компилятор не применяет так называемую оптимизацию возвращаемого значения для возврата встроенных функций управляемый тип.

Это означает, что строка до того, как действительно будет переведена (я очень упростил это) в этот код компилятором. tmp - это сгенерированная компилятором переменная - она ​​резервирует пространство в стеке в прологе метода:

tmp := ThreadPool.FQueue.Dequeue;
Item := tmp;

Эта переменная завершается в end метода. Вы можете поместить там точку останова и одну в TTask.Destroy, а затем вы увидите, что, когда приложение заканчивается, когда оно достигает конца метода, это приведет к уничтожению последнего экземпляра TTask, потому что временная переменная, сохраняющая ее, очищается.

Я использовал немного взлома, чтобы устранить эту проблему локально. Я добавил эту локальную процедуру, чтобы исключить временную переменную, пробивающуюся в метод TThreadPool.TQueueWorkerThread.Execute:

procedure InternalDequeue(var Item: IThreadPoolWorkItem);
begin
  Item := ThreadPool.FQueue.Dequeue;
end;

а затем изменил код внутри метода:

InternalDequeue(Item);

Это все равно приведет к тому, что Dequeue создаст временную переменную, но теперь она живет только внутри метода InternalDequeue и очищается после ее выхода.

Изменить (09.11.2017): Это было исправлено в 10.2 в компиляторе. Теперь он вставляет блок finally после назначения переменной temp в реальную, поэтому временная переменная не вызывает дополнительную ссылку больше, чем она должна.

Ответ 2

Это известная проблема: Рабочий поток TThreadPool содержит ссылку на последнюю выполненную задачу

Временная переменная в TThreadPool.TQueueWorkerThread.Execute поддерживает ссылка на последний выполненный рабочий элемент (задача), который только освобождается при завершении метода Execute.

Будучи в пуле, поток обычно сохраняется в живых до тех пор, пока пул не будет уничтожается, что для пула по умолчанию означает во время завершения Блок. Таким образом, последние выполненные задачи не освобождаются до тех пор, пока программа завершается.