The TLockToken provides RAII-style locking through interface reference counting, ensuring the lock is released when the enumerator is destroyed. This is a key part of the thread-safe iteration mechanism.
The RAII (Resource Acquisition Is Initialization) pattern is a powerful concept in object-oriented programming where resources are automatically released when an object is destroyed. In the context of thread-safe collections, TLockToken ensures that locks are released when the enumerator is destroyed, preventing deadlocks and simplifying resource management.
- RAII Pattern
TLockToken = class(TInterfacedObject, ILockToken)
private
FLock: TCriticalSection;
public
constructor Create(ALock: TCriticalSection); // Acquires lock
destructor Destroy; override; // Releases lock
end;
- Automatic Lock Management through Interface
FLockToken: ILockToken; // In TEnumerator
- When FLockToken goes out of scope, the interface reference count drops to 0
- This automatically triggers TLockToken's destructor
- Lock is released automatically, even if an exception occurs!
- Clear Iterator Implementation
constructor TThreadSafeDeque.TEnumerator.Create(ADeque: TThreadSafeDeque);
begin
inherited Create;
FDeque := ADeque;
FLockToken := FDeque.Lock; // Lock is acquired and managed automatically
FCurrentNode := nil;
end;
This pattern provides:
- Exception safety
- No need for
try..finally
blocks in the iterator - Self-cleaning resources
- Thread-safe iteration
It's a very neat solution that leverages Free Pascal's interface reference counting to manage thread synchronization!
The RAII pattern is commonly used in C++, and this specific implementation is also seen in Free Pascal within Delphi/FPC codebases and documentation. The key insight is that Free Pascal's interfaces (IInterface) provide automatic reference counting and cleanup, similar to C++'s RAII mechanism.
While the RAII pattern through interface counting is not commonly seen in Pascal codebases, it provides significant benefits:
- Automatic cleanup through reference counting
- Exception safety without explicit
try..finally
blocks - Clear resource ownership semantics
Our implementation in ThreadSafeCollections demonstrates these benefits, as shown by the test results across all collections.
The beauty of using ILockToken for this purpose remains valid:
- Reference counting is automatic
- Cleanup is guaranteed even if an exception occurs
- The scope of the lock is clear and explicit
- No need for explicit
try..finally
blocks
Our test suite includes specific tests for the RAII locking mechanism across all collections:
TThreadSafeListTest.TestLockingMechanism: 33.159 ms
TThreadSafeDictionaryTest.TestLockingMechanism: 32.749 ms
TThreadSafeHashSetTest.Test14_LockingMechanism: 32.497 ms
TThreadSafeDequeTest.TestLockingMechanism: 31.239 ms
Each test:
- Creates 4 concurrent threads
- Each thread performs 1000 lock/unlock cycles
- Verifies that all 4000 lock operations succeed
- Measures time taken for lock acquisition and release
The consistent timing (~32ms) across different collections demonstrates:
- Reliable lock acquisition and release
- No lock leaks or deadlocks
- Consistent performance regardless of collection type
- Effective automatic cleanup through interface reference counting
The test code shows how simple the locking mechanism is to use:
procedure TLockTestThread.Execute;
var
I: Integer;
LockToken: ILockToken;
begin
for I := 1 to FIterations do
begin
try
// Get lock token - automatically managed!
LockToken := FList.Lock;
// Simulate some work
Sleep(Random(2));
// Lock will be automatically released when LockToken goes out of scope
Inc(FLockCount);
except
on E: Exception do
WriteLn('Lock failed: ', E.Message);
end;
end;
end;
Even in this stress test scenario with multiple threads competing for locks, the RAII pattern through interface counting ensures:
- No resource leaks
- Proper lock release even with exceptions
- Clean, maintainable code without explicit lock management
{ TThreadSafeList.TEnumerator }
constructor TThreadSafeList.TEnumerator.Create(AList: specialize TThreadSafeList<T>);
begin
inherited Create;
FList := AList;
FLockToken := FList.Lock;
FIndex := -1;
end;
destructor TThreadSafeList.TEnumerator.Destroy;
begin
FLockToken := nil; // Release lock
inherited;
end;
function TThreadSafeList.TEnumerator.MoveNext: Boolean;
begin
Inc(FIndex);
if FIndex < FList.FCount then
begin
FCurrent := FList.FList[FIndex];
Result := True;
end
else
Result := False;
end;
function TThreadSafeList.GetEnumerator: TEnumerator;
begin
Result := TEnumerator.Create(Self);
end;
function TThreadSafeList.Lock: ILockToken;
begin
Result := TLockToken.Create(FLock);
end;
The RAII lock reference counting happens through several parts:
- Lock Acquisition - In the constructor:
constructor TThreadSafeList.TEnumerator.Create(AList: specialize TThreadSafeList<T>);
begin
inherited Create;
FList := AList;
FLockToken := FList.Lock; // Here! Lock() returns ILockToken
FIndex := -1;
end;
- Lock Creation - In the Lock method:
function TThreadSafeList.Lock: ILockToken;
begin
Result := TLockToken.Create(FLock); // Creates TLockToken which acquires the lock
end;
- Reference Counting - Through ILockToken interface:
// In ThreadSafeCollections.Interfaces.pas
TLockToken = class(TInterfacedObject, ILockToken)
private
FLock: TCriticalSection;
public
constructor Create(ALock: TCriticalSection); // Acquires lock
destructor Destroy; override; // Releases lock
end;
- Lock Release - In the destructor:
destructor TThreadSafeList.TEnumerator.Destroy;
begin
FLockToken := nil; // Here! Setting to nil decrements ref count
inherited; // When ref count hits 0, TLockToken is destroyed
end; // Which releases the lock
The magic happens because:
FLockToken
is an interface type (ILockToken)- When we set it to
nil
, the reference count drops to 0 - This triggers
TLockToken.Destroy
- Which releases the lock via
FLock.Release
This is all automatic due to Pascal's interface reference counting!