Fixing TRTLCriticalSection

In practice, you may use a TCriticalSection class, or the lower-level TRTLCriticalSection record, which is perhaps to be preferred, since it would use less memory, and could easily be included as a (protected) field to any class definition.

Let's say we want to protect any access to the variables a and b. Here's how to do it with the critical sections approach:

var CS: TRTLCriticalSection;
    a, b: integer;
// set before the threads start
InitializeCriticalSection(CS);
// in each TThread.Execute:
EnterCriticalSection(CS);
try // protect the lock via a try ... finally block
  // from now on, you can safely make changes to the variables
  inc(a);
  inc(b);
finally
  // end of safe block
  LeaveCriticalSection(CS);
end;
// when the threads stop
DeleteCriticalSection(CS);

In newest versions of Delphi, you may use a TMonitor class, which would let the lock be owned by any Delphi TObject.
Before XE5, there was some performance issue, and even now, this Java-inspired feature may not be the best approach, since it is tied to a single object, and is not compatible with older versions of Delphi (or FPC).

Eric Grange reported some years ago - see this blog article - that TRTLCriticalSection (along with TMonitor) suffers from a severe design flaw in which entering/leaving different critical sections can end up serializing your threads, and the whole can even end up performing worse than if your threads had been serialized. This is because it's a small, dynamically allocated object, so several TRTLCriticalSection memory can end up in the same CPU cache line, and when that happens, you'll have cache conflicts aplenty between the cores running the threads.

The fix proposed by Eric is dead simple:

type
   TFixedCriticalSection = class(TCriticalSection)
   private
     FDummy: array [0..95] of Byte;
   end;

Introducing TSynLocker

Since we wanted to use a TRTLCriticalSection record instead of a TCriticalSection class instance, we defined a TSynLocker record in SynCommons.pas:

  TSynLocker = record
  private
    fSection: TRTLCriticalSection;
  public
    Padding: array[0..6] of TVarData;
    procedure Init;
    procedure Done;
    procedure Lock;
    procedure UnLock;
  end;

As you can see, the Padding[] array would ensure that the CPU cache-line issue won't affect our object.

TSynLocker use is close to TRTLCriticalSection, with some method-oriented behavior:

var safe: TSynLocker;
    a, b: integer;
// set before the threads start
safe.Init;
// in each TThread.Execute:
safe.Lock
try // protect the lock via a try ... finally block
  // from now on, you can safely make changes to the variables
  inc(a);
  inc(b);
finally
  // end of safe block
  safe.Unlock;
end;
// when the threads stop
safe.Done;

If your purpose is to protect a method execution, you may use the TSynLocker.ProtectMethod function or explicit Lock/Unlock, as such:

type
  TMyClass = class
  protected
    fSafe: TSynLocker;
    fField: integer;
  public
    constructor Create;
    destructor Destroy; override;
    procedure UseLockUnlock;
    procedure UseProtectMethod;
  end;

{ TMyClass }
constructor TMyClass.Create; begin fSafe.Init; // we need to initialize the lock end;
destructor TMyClass.Destroy; begin fSafe.Done; // finalize the lock inherited; end;
procedure TMyClass.UseLockUnlock; begin fSafe.Lock; try // now we can safely access any protected field from multiple threads inc(fField); finally fSafe.UnLock; end; end;
procedure TMyClass.UseProtectMethod; begin fSafe.ProtectMethod; // calls fSafe.Lock and return IUnknown local instance // now we can safely access any protected field from multiple threads inc(fField); // here fSafe.UnLock will be called when IUnknown is released end;

Inheriting from T*Locked

For your own classes definition, you may inherit from some classes providing a TSynLocker instance, as defined in SynCommons.pas:

  TSynPersistentLocked = class(TSynPersistent)
  ...
    property Safe: TSynLocker read fSafe;
  end;
  TInterfacedObjectLocked = class(TInterfacedObjectWithCustomCreate)
  ...
    property Safe: TSynLocker read fSafe;
  end;
  TObjectListLocked = class(TObjectList)
  ...
    property Safe: TSynLocker read fSafe;
  end;
  TRawUTF8ListHashedLocked = class(TRawUTF8ListHashed)
  ...
    property Safe: TSynLocker read fSafe;
  end;

All those classes will initialize and finalize their owned Safe instance, in their constructor/destructor.

So, we may have written our class as such:

type
  TMyClass = class(TSynPersistentLocked)
  protected
    fField: integer;
  public
    procedure UseLockUnlock;
    procedure UseProtectMethod;
  end;

{ TMyClass }
procedure TMyClass.UseLockUnlock; begin fSafe.Lock; try // now we can safely access any protected field from multiple threads inc(fField); finally fSafe.UnLock; end; end;
procedure TMyClass.UseProtectMethod; begin fSafe.ProtectMethod; // calls fSafe.Lock and return IUnknown local instance // now we can safely access any protected field from multiple threads inc(fField); // here fSafe.UnLock will be called when IUnknown is released end;

As you can see, the Safe: TSynLocker instance would be defined and handled at TSynPersistentLocked parent level.

Injecting IAutoLocker instances

If your class inherits from TInjectableObject, you may even define the following:

type
  TMyClass = class(TInjectableObject)
  private
    fLock: IAutoLocker;
    fField: integer;
  public
    function FieldValue: integer;
  published
    property Lock: IAutoLocker read fLock write fLock;
  end;

{ TMyClass }
function TMyClass.FieldValue: integer; begin Lock.ProtectMethod; result := fField; inc(fField); end;
var c: TMyClass; begin c := TMyClass.CreateInjected([],[],[]); Assert(c.FieldValue=0); Assert(c.FieldValue=1); c.Free; end;

Here we use dependency resolution - see Dependency Injection and Interface Resolution - to let the TMyClass.CreateInjected constructor scan its published properties, and therefore search for a provider of IAutoLocker. Since IAutoLocker is globally registered to be resolved with TAutoLocker, our class would initialize its fLock field with a new instance. Now we could use Lock.ProtectMethod to access the associated TSynLocker critical section, as usual.

Of course, this may be more complicated than manual TSynLocker handling, but if you are writing an interface-based service, your class may inherit from TInjectableObject for its own dependency resolution, so this trick may be very convenient.

Safe locked storage in TSynLocker

When we fixed the potential CPU cache-line issue, do you remember that we added a padding binary buffer to the TSynLocker definition? Since we do not want to waste resource, TSynLocker gives easy access to its internal data, and allow to directly handle those values. Since it is stored as 7 slots of variant values, you could store any kind of data, including complex TDocVariant document or array.

Our class may use this feature, and store its integer field value in the internal slot 0:

type
  TMyClass = class(TSynPersistentLocked)
  public
    procedure UseInternalIncrement;
    function FieldValue: integer;
  end;

{ TMyClass }
function TMyClass.FieldValue: integer; begin // value read would also be protected by the mutex result := fSafe.LockedInt64[0]; end;
procedure TMyClass.UseInternalIncrement; begin // this dedicated method would ensure an atomic increase fSafe.LockedInt64Increment(0,1); end;

Please note that we used the TSynLocker.LockedInt64Increment() method, since the following would not be safe:

procedure TMyClass.UseInternalIncrement;
begin
  fSafe.LockedInt64[0] := fSafe.LockedInt64[0]+1;
end;

In the above line, two locks are acquired (one per LockedInt64 property call), so another thread may modify the value in-between, and the increment may not be as accurate as expected.

TSynLocker offers some dedicated properties and methods to handle this safe storage. Those expect an Index value, from 0..6 range:

    property Locked[Index: integer]: Variant read GetVariant write SetVariant;
    property LockedInt64[Index: integer]: Int64 read GetInt64 write SetInt64;
    property LockedPointer[Index: integer]: Pointer read GetPointer write SetPointer;
    property LockedUTF8[Index: integer]: RawUTF8 read GetUTF8 write SetUTF8;
    function LockedInt64Increment(Index: integer; const Increment: Int64): Int64;
    function LockedExchange(Index: integer; const Value: variant): variant;
    function LockedPointerExchange(Index: integer; Value: pointer): pointer;

You may store a pointer or a reference to a TObject instance, if necessary.

Having such a tool-set of thread-safe methods does make sense, in the context of our framework, which offers multi-thread server abilities - see Thread-safety.

Feel free to continue the reading on the mORMot documentation, which may contain updated and additional information on this subject.