How to Use Upgradeable Read Locks in Delphi and Free Pascal

A common shared-state operation begins by asking whether the value is already present. Only the missing-value case needs a write. Protecting everything with an exclusive lock works, but it prevents unrelated readers from inspecting the same stable state. Taking an ordinary read and then attempting to become a writer is not automatically safe either.

Sphere 10 Pascal Framework provides explicit read, write and upgradeable-read ownership in TReadWriteLock. An upgradeable read reserves the possibility of a later write for one thread. Our example checks a value, acquires a timed write only when it needs changing, releases that write and then releases the outer acquisition.

Shared reading and controlled exclusive access to a common resource.
Shared reading and controlled exclusive access to a common resource.

Contents

Classify the Protected Operation

Use a read acquisition for genuinely read-only inspection. If a helper lazily initializes a collection, moves a shared stream cursor or changes a counter, that path is a write even if its public name starts with Get. The lock mode follows the work performed through the whole call path.

Choose upgradeable read before an operation that may mutate after inspection. An ordinary read cannot silently become write or upgradeable ownership. Releasing an ordinary read and later taking a write is another valid design, but it requires rechecking the condition because another writer may have changed it in between.

Add the Write as a Separate Acquisition

The default no-recursion policy permits the documented promotion from upgradeable read to write. The write is additional ownership, so releasing it does not release the surrounding upgradeable read. Nested try/finally blocks make both responsibilities visible when the mutation raises.

TryAcquireWriteLock accepts milliseconds. A zero timeout tests immediately, a positive value bounds waiting, and minus one waits indefinitely. A failed timed acquisition retains the outer acquisition so the caller can decide whether to retry, cancel or report the timeout.


program ArticleSample;
{$IFDEF FPC}{$mode Delphi}{$H+}{$ENDIF}
uses SysUtils, UConcurrency;
var Gate: TReadWriteLock; Value: Integer;
begin
  Gate := TReadWriteLock.Create;
  try
    Value := 10;
    Gate.AcquireUpgradeableReadLock;
    try
      if Value = 10 then begin
        if not Gate.TryAcquireWriteLock(1000) then raise Exception.Create('Write timed out');
        try
          Value := 20;
        finally
          Gate.ReleaseWriteLock;
        end;
      end;
      if not Gate.IsUpgradeableReadLockHeld or Gate.IsWriteLockHeld then
        raise Exception.Create('Promotion ownership was not balanced');
    finally
      Gate.ReleaseUpgradeableReadLock;
    end;
    if not Gate.TryAcquireReadLock(0) then raise Exception.Create('Read not admitted');
    try
      WriteLn('Value=', Value);
    finally
      Gate.ReleaseReadLock;
    end;
    Gate.Dispose;
    WriteLn('Write released before upgradeable read; all owners released');
  finally
    Gate.Free;
  end;
end.

Observe the Ownership Boundary


Value=20
Write released before upgradeable read; all owners released

After the inner finally, write ownership is gone while upgradeable ownership remains. After the outer finally, a fresh ordinary read can inspect the value. The sample checks both transitions and explicitly disposes a lock with no remaining owners.

This is an ownership example, not a contention benchmark. It deliberately keeps the result deterministic on one thread. The Framework's separate lock fixtures exercise concurrent readers, blocked writers, timeouts, recursive policies and wrong-thread release. Use those contracts when designing a real multi-worker consumer.

Keep Lifetime and Thread Affinity Together

Release every successful acquisition on the thread that acquired it. Do not send a managed scope or an explicit release to a different worker merely because the protected value is shared. The lock's ownership counters describe the calling thread, not a transferable token.

Stop and join consumers before freeing their lock. Dispose rejects outstanding ownership or waiters rather than terminating their work. Release locks before joining a worker that may need them to finish, because holding those locks can prevent shutdown from completing.

⚠️ More concurrent readers does not automatically mean lower cost. Lock acquisition, protected work, writer pressure and memory usage all affect the result. Measure the application's workload before presenting a read/write lock as a universal speedup.

Use Managed Scopes Deliberately

The Framework also provides EnterReadScope, EnterWriteScope and EnterUpgradeableReadScope. These return managed TScope values for routine-scoped ownership. Keep the scope in a local variable and keep the lock alive until its last copy is finalized. The explicit version above makes the promotion sequence particularly easy to inspect.

The lock source , public examples and shared tests provide a compact route from the idea to real application code. Your application still determines lock order, cancellation and how long protected work may run. The Framework supplies a consistent ownership mechanism rather than guessing those policies for you.

Get the Framework and Run the Example

Get Sphere 10 Pascal Framework and follow its package instructions . Use the units directly, or build the optional Lazarus .lpk or Delphi runtime package. Pascal uses these source/package workflows. The C# Framework is distributed separately through NuGet.

The attached application sample was compiled and executed with Free Pascal 3.2.0 on Windows against this public source revision , with range and overflow checks. Its Build.ps1 accepts the Framework, compiler, Lazarus and output paths so the example does not need machine-specific paths. Lazarus UI examples require the LCL Win32 widgetset. A successful FPC run does not claim a new Delphi build. Follow the repository's separate Delphi/VCL project and resource instructions there.

Download the complete runnable sample.

sample.zip
Download the complete worked example and build instructions (ZIP).

Continue the Series

How to Give Pascal Resources Automatic Cleanup

How to Keep Old Background Results Out of a New View

For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .