请原谅我略带幽默的头衔。我在其中使用了两个不同的“安全”一词(显然)。
我对线程很陌生(好吧,我已经使用了多年的线程,但只是非常简单的形式)。现在我面临着编写某些算法的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
的成本相当低(在很多情况下,但不是全部!),所以它是(也许)愚蠢的不安全地玩。但我也觉得知道这在理论水平上是如何运作会很好,所以我仍然认为这个问题非常有趣。
答案 0 :(得分:10)
您的程序不会因为仅用作计数的整数增量的竞争而崩溃。所有可能出错的是你没有得到正确的答案。显然,如果你使用整数作为数组的索引,或者它可能是一个指针,那么你可能会遇到问题。
除非你非常频繁地增加这个值,否则很难想象互锁增量对于你注意到性能差异来说是昂贵的。
更有效的方法是让每个线程保持自己的私有计数。然后在计算结束时加入线程时对所有单个线程计数求和。这样你就可以获得两全其美。对增量和正确答案没有争论。当然,您需要采取措施确保不会被false sharing抓住。