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.
