Skip to content

Latest commit

 

History

History
226 lines (173 loc) · 6.51 KB

RAII-style-locking-through-interface-counting.md

File metadata and controls

226 lines (173 loc) · 6.51 KB

RAII-Style Locking Through Interface Counting

What is TLockToken?

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.

Why is it useful?

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.

  1. RAII Pattern
TLockToken = class(TInterfacedObject, ILockToken)
private
  FLock: TCriticalSection;
   public
     constructor Create(ALock: TCriticalSection);  // Acquires lock
     destructor Destroy; override;                 // Releases lock
  end;
  1. 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!
  1. 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!

More on TLockToken

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

Test Results on RAII Locking - 2024-11-30

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:

  1. Reliable lock acquisition and release
  2. No lock leaks or deadlocks
  3. Consistent performance regardless of collection type
  4. 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

Example of TLockToken in ThreadSafeCollections.List.pas

{ 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:

  1. 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;
  1. Lock Creation - In the Lock method:
function TThreadSafeList.Lock: ILockToken;
begin
  Result := TLockToken.Create(FLock);  // Creates TLockToken which acquires the lock
end;
  1. 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;
  1. 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:

  1. FLockToken is an interface type (ILockToken)
  2. When we set it to nil, the reference count drops to 0
  3. This triggers TLockToken.Destroy
  4. Which releases the lock via FLock.Release

This is all automatic due to Pascal's interface reference counting!