delphithread-safetytmonitor

Using TMonitor in Delphi to block access to global object


I have a global object in Delphi that reads some object definitions from disk and creates a cache of worker objects representing them.
There is also a method on the global object that returns an instance of a worker object - this method needs to be blocked if the global object is still inside the method that is reading the disk/creating the workers. This load/create method will be called on an intermittent basis (once per day) to force reloading of the xml files.

I've created a simplified version of the code so that I can get some feedback on the correct use of the TMonitor record. First the declarations

type
  // a simplified version of the worker, I just get it to return the timestamp of when it was created - in real life this actually does something :)
  TWorker = class
  private
    FValue: TDateTime;
  public
    property Value: TDatetime read FValue;
  end;

// An interface modelling the global object
  IStorage = interface
    ['{8FD599BE-4064-45DE-8FFC-96A9D2C812F1}']
    function GetWorker: TWorker;
    procedure ReLoad;
  end;

// a global pointer to a function that has access to a singleton instance of the worker. Creating this 
var
  Storage: function: IStorage;

Now the implementation

// a concrete implementation of the interface
type
  TStorage = class(TInterfacedObject, IStorage)
  private
    FIsLoaded: Boolean;  // flag to indicate if objects have been loaded
    FList: TObjectList<TWorker>;  // list of Worker objects (takes ownership)
  protected
    { IStorage }
    function GetWorker: TWorker;
    procedure Load;
  public 
    constructor Create;
    destructor Destroy; override;
  end;

// the method to "load/reload" from disk - I've just simplified this-in real life it would lock then clear the cache and recreate the objects
procedure TStorage.ReLoad;
begin
  TMonitor.Enter(Self);
  try
    FIsLoaded := False;
    for var I := 0 to 15 do
      Sleep(250);
    FList.Clear;
    FIsLoaded := true;
    TMonitor.PulseAll(Self);
  finally
    TMonitor.Exit(Self);
  end;
end;

// and the method that serves out instances of worker objects
// this is the one that is causing me to double check my understanding of TMonitor
function TStorage.GetWorker: TWorker;
begin
  if not FIsLoaded then
  begin
    TMonitor.Enter(Self);
    try
      while not FIsLoaded do
        TMonitor.Wait(Self, INFINITE);
    finally
      TMonitor.Exit(Self);
    end;
  end;
  Result := TWorker.Create;
  Result.FValue := Now;
end;

I'm using the TMonitor instance (locking the global object) to prevent a calling thread from proceeding with GetWorker until the boolean FIsLoaded has been set to true.

Am I on the right track ? In particular my use of the Wait and PulseAll methods.

I'm using Delphi 11.3 if that makes any difference, as google searches indicate some issues using TMonitor but around 10 years ago.


Solution

  • You're on the right track here. I would recommend using a private instance field for the lock rather than Self. This would stay away from the "locking this (Self) is bad" issue. Otherwise some external "bad actor" could take the same lock you're using internally.

    If you have the System.pas source, there should be a rather large comment describing TMonitor, including a link to an article describing its inspiration.