Atomic Counters in Pascal

Our primary mechanism for managing state in Pascal is through shared variables. However, when dealing with multiple threads, we need to ensure that these shared variables are accessed safely. In this example, we’ll look at using atomic operations for thread-safe counters accessed by multiple threads.

program AtomicCounters;

uses
  SysUtils, Classes, SyncObjs;

var
  Ops: Int64;
  OpLock: TCriticalSection;
  WaitGroup: TCountdownEvent;

procedure IncrementCounter;
var
  i: Integer;
begin
  for i := 1 to 1000 do
  begin
    // To atomically increment the counter we use a critical section
    OpLock.Acquire;
    try
      Inc(Ops);
    finally
      OpLock.Release;
    end;
  end;
  WaitGroup.Signal;
end;

var
  i: Integer;
  Threads: array[1..50] of TThread;

begin
  Ops := 0;
  OpLock := TCriticalSection.Create;
  WaitGroup := TCountdownEvent.Create(50);

  try
    // We'll start 50 threads that each increment the counter exactly 1000 times.
    for i := 1 to 50 do
    begin
      Threads[i] := TThread.CreateAnonymousThread(IncrementCounter);
      Threads[i].Start;
    end;

    // Wait until all the threads are done.
    WaitGroup.WaitFor;

    // Here no threads are writing to 'Ops', but we still use the lock
    // to ensure safe reading of the value.
    OpLock.Acquire;
    try
      WriteLn('ops: ', Ops);
    finally
      OpLock.Release;
    end;

  finally
    OpLock.Free;
    WaitGroup.Free;
    for i := 1 to 50 do
      Threads[i].Free;
  end;
end.

We expect to get exactly 50,000 operations. If we had used a non-atomic integer and incremented it without synchronization, we’d likely get a different number, changing between runs, because the threads would interfere with each other.

To run this program:

$ fpc atomic_counters.pas
$ ./atomic_counters
ops: 50000

In this Pascal implementation, we use a TCriticalSection to ensure atomic access to our counter. This is similar to a mutex in other languages. We also use a TCountdownEvent as an equivalent to Go’s WaitGroup to wait for all threads to complete.

The TThread.CreateAnonymousThread method is used to create threads, which is similar to launching goroutines in Go. Each thread runs the IncrementCounter procedure 1000 times.

Note that Pascal doesn’t have built-in atomic operations like Go does, so we have to use explicit locking mechanisms. This makes the code a bit more verbose, but it achieves the same thread-safe counter functionality.

Next, we’ll look at mutexes, another tool for managing state in concurrent programs.