8

请原谅我略带幽默的标题。我在其中使用了“安全”一词的两种不同定义(显然)。

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

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会做三件事:读取、递增和保存),因此很多incs 将被有效地“丢失”。我从上面的代码中得到的一个典型值是10030423.

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

现在,在大多数实际算法中,冲突并不常见。事实上,它们可能非常罕见。我的一种算法创建了DLA 分形,而我不时产生的变量之一inc是吸附粒子的数量。这里的冲突非常罕见,更重要的是,我真的不在乎变量的总和是否为 20000000、20000008、20000319 或 19999496。因此,很容易使用InterlockedIncrement或临界区,因为它们只会使代码膨胀并使其(稍微)慢到没有(据我所见)好处。

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

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

4

1 回答 1

10

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

除非你非常频繁地增加这个值,否则很难想象一个联锁的增量会足够昂贵,让你注意到性能差异。

更有效的方法是让每个线程保持自己的私有计数。然后在计算结束时加入线程时将所有单独的线程计数相加。这样你就可以两全其美了。没有关于递增和正确答案的争论。当然,您需要采取措施,确保您不被虚假分享中招。

于 2012-01-07T13:19:17.253 回答