Closure in TTask.Run(AnonProc) not released after AnonProc has finished Closure in TTask.Run(AnonProc) not released after AnonProc has finished multithreading multithreading

Closure in TTask.Run(AnonProc) not released after AnonProc has finished


I did some more analysis of this bug to find out the real reason why the ITask was being held in TThreadPool.TQueueWorkerThread.Execute as mentioned in the known issue.

The following innocent looking line of code is the problem:

Item := ThreadPool.FQueue.Dequeue;

Why is that the case? Because TQueue<T>.Dequeue is marked as inline and now you have to know that the compiler does not apply the so called return value optimization for inlined functions returning a managed type.

This means the line before really gets translated (I very much simplified this) into this code by the compiler. tmp is a compiler generated variable - it reserves space on the stack in the prologue of the method:

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

This variable gets finalized in the end of the method. You can put a breakpoint there and one into TTask.Destroy and then you see that when the application ends once it reaches the end of the method this will trigger the last TTask instance getting destroyed because the temp variable keeping it alive is being cleared.

I used some little hack to fix this issue locally. I added this local procedure to eliminate the temporary variable sneaking into the TThreadPool.TQueueWorkerThread.Execute method:

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

and then changed the code inside the method:

InternalDequeue(Item);

This will still cause the Dequeue to produce a temporary variable but now this only lives inside the InternalDequeue method and is being cleared once it exits.

Edit (09.11.2017): This has been fixed in 10.2 in the compiler. It now inserts a finally block after the assignment of the temp variable to the real one so the temp variable does not cause an additional reference any longer than it should.


This is known issue: TThreadPool worker thread holds reference to last executed task

A temporary variable in TThreadPool.TQueueWorkerThread.Execute keeps a reference to the last executed work-item (task), which is only released when the Execute method ends.

Being in a pool the thread is usually kept alive until the pool is destroyed, which for the Default pool means during finalization of the unit. Thus the last executed tasks are not released until the program terminates.