Закрытие в 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.
Ответы
Ответ 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.
Будучи в пуле, поток обычно сохраняется в живых до тех пор, пока пул не будет уничтожается, что для пула по умолчанию означает во время завершения Блок. Таким образом, последние выполненные задачи не освобождаются до тех пор, пока программа завершается.