Destroying instance of object resolved by container with SingletonPerThread lifetime

Issue #201 open
Sergey Burov created an issue

it will be not bad if in TSingletonPerThreadLifetimeManager class add code, that destroy instanse of objects created per tread when thread is terminating

we have proposal to add subscibe for event OnTerminate of thread in function TSingletonPerThreadLifetimeManager.Resolve

procedure TSingletonPerThreadLifetimeManager.OnTerminate(sender: TObject);
var
  pThread : TThread;
  threadID: THandle;
  holder: TFunc<TValue>;
begin
  pThread := TThread(sender);
  threadID := pThread.ThreadID;
  if fInstances.TryGetValue(threadID, holder) then
  begin
    DoBeforeDestruction(holder);
    fInstances.Remove(threadID);
  end;

end;

function TSingletonPerThreadLifetimeManager.Resolve(
  const context: ICreationContext; const model: TComponentModel): TValue;
var
  threadID: THandle;
  instance: TValue;
  holder: TFunc<TValue>;
begin
  threadID := TThread.CurrentThread.ThreadID;
  MonitorEnter(Self);
  try
    if not fInstances.TryGetValue(threadID, holder) then
    begin
      instance := model.ComponentActivator.CreateInstance(context);
      holder := CreateHolder(instance, model.RefCounting);
      fInstances.AddOrSetValue(threadID, holder);
      TThread.CurrentThread.OnTerminate := OnTerminate;
      DoAfterConstruction(holder);
    end;
  finally
    MonitorExit(Self);
  end;
  Result := holder;
end;

we think that this proposal don't break anything

Comments (7)

  1. Stefan Glienke repo owner

    That approach fails as soon as you have more than one instances as singletonPerThread. Also not every thread does OnTerminate (afaik threads that are not created by the RTL for example).

    To solve this issue the singleton tracking inside the lifetime manager should be done differently - I just don't know how exactly. Maybe using weak reference might help as that allows handing out the same instance as long as it is alive but allow the instance to be destroyed by the ref counting mechanism at the end of the unit of work inside a thread.

  2. Natalie Vincent

    I have a couple of suggestions for detecting the end of the thread:

    • Use FiberLocalStorage FlsAlloc(), passing a callback function that gets called when the fiber exits (well, thread exits. Every thread has at least one fiber), making sure to call FlsSetValue with a non-nil value in the thread to make sure the callback gets called. This is not supported before Windows Vista… Does that matter?
    • Hook SystemThreadEndProc.

    A working example showing both techniques:

    program ThreadExitSpike;
    
    {$APPTYPE CONSOLE}
    
    {$R *.res}
    
    uses
      WinApi.Windows,
      System.Classes,
      System.SysUtils;
    
    var
      MNextSystemThreadEndProc: TSystemThreadEndProc;
    
    procedure NewSystemThreadEndProc(ExitCode: Integer);
    begin
      WriteLn(Format('Thread Exit. Thread %d', [GetCurrentThreadId]));
      if Assigned(MNextSystemThreadEndProc) then
        MNextSystemThreadEndProc(ExitCode);
    end;
    
    procedure FiberExit(AFLSData: pointer); stdcall;
    begin
      WriteLn(Format('Fiber Exit. Thread %d', [GetCurrentThreadId]));
    end;
    
    var
      LThread: TThread;
      LFlsIndex: cardinal;
      LProc: TProc;
    
    begin
      MNextSystemThreadEndProc := SystemThreadEndProc;
      SystemThreadEndProc := NewSystemThreadEndProc;
    
      WriteLn(Format('Main thread %d', [GetCurrentThreadID]));
    
      WriteLn('Setting up FLS');
      LFlsIndex := FlsAlloc(@FiberExit);
      if LFlsIndex = $FFFFFFFF then
      begin
        WriteLn('FLS allocation failed');
        Exit;
      end;
    
      LProc := procedure ()
        const
          CDummy = 1;
        begin
          WriteLn(Format('Setting FLS value. Thread %d', [GetCurrentThreadId]));
          FlsSetValue(LFlsIndex, pointer(CDummy));
        end;
    
      WriteLn('Starting thread 1');
      LThread := TThread.CreateAnonymousThread(LProc);
      LThread.Start;
    
      WriteLn('Sleeping for a bit');
      Sleep(40);
    
      WriteLn('Starting thread 2');
      LThread := TThread.CreateAnonymousThread(LProc);
      LThread.Start;
    
      WriteLn('Sleeping for a bit');
      Sleep(40);
    
      WriteLn('Exiting');
    end.
    

    Output:

    Main thread 9604
    Setting up FLS
    Starting thread 1
    Sleeping for a bit
    Setting FLS value. Thread 29840
    Thread Exit. Thread 29840
    Fiber Exit. Thread 29840
    Starting thread 2
    Sleeping for a bit
    Setting FLS value. Thread 30512
    Thread Exit. Thread 30512
    Fiber Exit. Thread 30512
    Exiting
    

  3. Log in to comment