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.