4

I was assuming that if a shared variable between threads has native type, atomicity should do the job.

But as per output of the code below, it is not the case, at least for delphi.

Thread t1 is simply incrementing the counter 10M times. At the same time, thread t2 is decrementing the counter 10M times. So expected counter value at the end is 0 but I read different values each time.

What is the proper way of sharing a native variable between threads in Delphi without locking?

procedure TForm1.Button1Click(Sender: TObject);
var
  t1, t2: TThread;
  Counter: NativeInt;
begin
  Counter := 0;

  // first thread to increment shared counter
  t1 := TThread.CreateAnonymousThread(
    procedure ()
    var
      i: Integer;
    begin
      for i := 1 to 10000000 do
        Inc(Counter);
    end
  );

  // second thread to decrement shared counter
  t2 := TThread.CreateAnonymousThread(
    procedure ()
    var
      i: Integer;
    begin
      for i := 1 to 10000000 do
        Dec(Counter);
    end
  );

  t1.FreeOnTerminate := false;
  t2.FreeOnTerminate := false;

  // start threads
  t1.Start;
  t2.Start;

  // wait for them to finish
  t1.WaitFor;
  t2.WaitFor;

  t1.Free;
  t2.Free;

  // print the counter, expected counter is 0
  Caption := IntToStr(Counter);
end;
Mike Torrettinni
  • 1,816
  • 2
  • 17
  • 47
Mehmet Fide
  • 1,643
  • 1
  • 20
  • 35

1 Answers1

3

Reading and writing of aligned variables is atomic. But the problem is that when you use inc and dec you are both reading and writing. By performing two memory accesses then the compound operation is no longer atomic.

Use atomic increment functions instead. The TInterlocked class methods, or AtomicIncrement.

As far as what is native about NativeInt, that refers to its size. It is an integral type the same size as a pointer. So 32 bits in a 32 bit process, 64 bits in a 64 bit process. These types are seldom used for pure Delphi code, usually for interop with third party libraries which might declare handle types using pointer sized integers.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • I have checked how AtomicIncrement is implemented in Delphi. It uses "lock xadd [eax],edx". Instead of using Inc() and Dec() functions, TInterlocked.Increment(Counter) and TInterlocked.Decrement(Counter) solved the problem. But NativeInt is not accepted as input type, I had to change it to Integer. Thanks. – Mehmet Fide Jan 22 '18 at 23:00
  • 1
    I doubt you have any reason to use NativeInt for a variable where you perform arithmetic – David Heffernan Jan 22 '18 at 23:03
  • 1
    @MehmetFide: `NativeInt` maps to either `Integer` or `Int64` based on platform architecture. `TInterlocked.Increment()` and `TInterlocked.Decrement()` have overloads for both types, however their parameters are declared as `var`, so you can't pass a `NativeInt` variable unless you type-cast it. But yeah, it doesn't usually make sense to use `NativeInt` for non-pointer arithmetic operations. – Remy Lebeau Jan 23 '18 at 00:40