在 Delphi 线程之间共享本地变量

4

我一直认为如果线程之间有共享变量且这个变量是原生类型,那么原子性应该可以解决问题。

但是根据下面代码的输出结果,在Delphi中并非如此。

线程t1简单地将计数器递增10M次。 同时,线程t2将计数器递减10M次。 因此,预期的计数器值在最后应该是0,但是我每次读取到的值都不同。

在Delphi中,没有锁定的情况下分享原生变量的正确方法是什么?

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;

1
我猜这就是 System.SyncObjs 中的 TInterlocked 设计的初衷。 - Uwe Raabe
1个回答

3

对齐变量的读写是原子的。但问题在于,当您使用 incdec 时,您既要读取又要写入。通过执行两个内存访问,复合操作不再是原子操作。

请改用原子递增函数。可以使用 TInterlocked 类方法或 AtomicIncrement

至于 NativeInt 的本机特性,这指的是其大小。它是一个与指针大小相同的整数类型。因此,在32位进程中为32位,在64位进程中为64位。这些类型很少用于纯Delphi代码,通常用于与第三方库进行交互,这些库可能使用指针大小的整数声明句柄类型。


我已经检查了Delphi中如何实现AtomicIncrement。它使用“lock xadd [eax],edx”。与使用Inc()和Dec()函数不同,TInterlocked.Increment(Counter)和TInterlocked.Decrement(Counter)解决了这个问题。但是NativeInt不能作为输入类型接受,我不得不将其更改为Integer。谢谢。 - Mehmet Fide
1
我怀疑你在进行算术运算的变量中使用NativeInt没有任何理由。 - David Heffernan
1
@MehmetFide:NativeInt 根据平台架构映射到 IntegerInt64TInterlocked.Increment()TInterlocked.Decrement() 都有两种类型的重载,但是它们的参数被声明为 var,因此除非进行类型转换,否则无法传递 NativeInt 变量。但是,对于非指针算术运算,使用 NativeInt 通常没有意义。 - Remy Lebeau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接