使用'不安全'线程函数是否安全?

时间:2012-01-07 13:13:50

标签: multithreading delphi thread-safety critical-section interlocked-increment

请原谅我略带幽默的头衔。我在其中使用了两个不同的“安全”一词(显然)。

我对线程很陌生(好吧,我已经使用了多年的线程,但只是非常简单的形式)。现在我面临着编写某些算法的parallal实现的挑战,并且线程需要处理相同的数据。考虑以下新手错误:

const
  N = 2;

var
  value: integer = 0;    

function ThreadFunc(Parameter: Pointer): integer;
var
  i: Integer;
begin
  for i := 1 to 10000000 do
    inc(value);
  result := 0;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  threads: array[0..N - 1] of THandle;
  i: Integer;
  dummy: cardinal;
begin

  for i := 0 to N - 1 do
    threads[i] := BeginThread(nil, 0, @ThreadFunc, nil, 0, dummy);

  if WaitForMultipleObjects(N, @threads[0], true, INFINITE) = WAIT_FAILED then
    RaiseLastOSError;

  ShowMessage(IntToStr(value));

end;

初学者可能希望上面的代码显示消息20000000。实际上,首先value等于0,然后我们inc 20000000次。但是,由于inc过程不是'原子',两个线程会发生冲突(我猜inc做了三件事:它读取,递增,然后保存),所以很多inc将被有效“丢失”。我从上面的代码中得到的典型值是10030423

最简单的解决方法是使用InterlockedIncrement代替Inc(在这个愚蠢的例子中会慢得多,但这不是重点)。另一个解决方法是将inc置于一个关键部分(是的,在这个愚蠢的例子中也会非常慢)。

现在,在大多数实际算法中,冲突并不常见。事实上,它们可能非常罕见。我的一个算法创建DLA fractals,其中一个变量I inc时不时是吸附粒子的数量。这里的冲突是非常罕见的,更重要的是,我真的不在乎变量总和是20000000,2000000,20000319还是19999496。因此,使用{{1}很诱人 或者关键部分,因为它们只是膨胀代码并使它(略微)慢到没有(据我所知)的好处。

但是,我的问题是:冲突的后果是否会比增量变量的略微“不正确”值更严重?例如,程序会崩溃吗?

不可否认,这个问题可能看起来很愚蠢,因为毕竟使用InterlockedIncrement而不是InterlockedIncrement的成本相当低(在很多情况下,但不是全部!),所以它是(也许)愚蠢的安全地玩。但我也觉得知道这在理论水平上是如何运作会很好,所以我仍然认为这个问题非常有趣。

1 个答案:

答案 0 :(得分:10)

您的程序不会因为仅用作计数的整数增量的竞争而崩溃。所有可能出错的是你没有得到正确的答案。显然,如果你使用整数作为数组的索引,或者它可能是一个指针,那么你可能会遇到问题。

除非你非常频繁地增加这个值,否则很难想象互锁增量对于你注意到性能差异来说是昂贵的。

更有效的方法是让每个线程保持自己的私有计数。然后在计算结束时加入线程时对所有单个线程计数求和。这样你就可以获得两全其美。对增量和正确答案没有争论。当然,您需要采取措施确保不会被false sharing抓住。