请原谅我略带幽默的标题。我在其中使用了“安全”一词的两种不同定义(显然)。
我对线程相当陌生(好吧,我已经使用线程很多年了,但只是非常简单的形式)。现在我面临着编写一些算法的并行实现的挑战,并且线程需要处理相同的数据。考虑以下新手错误:
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
s 将被有效地“丢失”。我从上面的代码中得到的一个典型值是10030423
.
最简单的解决方法是使用InterlockedIncrement
代替Inc
(在这个愚蠢的例子中会慢得多,但这不是重点)。另一种解决方法是将inc
关键部分放在内部(是的,在这个愚蠢的例子中也会很慢)。
现在,在大多数实际算法中,冲突并不常见。事实上,它们可能非常罕见。我的一种算法创建了DLA 分形,而我不时产生的变量之一inc
是吸附粒子的数量。这里的冲突非常罕见,更重要的是,我真的不在乎变量的总和是否为 20000000、20000008、20000319 或 19999496。因此,很容易不使用InterlockedIncrement
或临界区,因为它们只会使代码膨胀并使其(稍微)慢到没有(据我所见)好处。
但是,我的问题是:冲突的后果是否比递增变量的稍微“不正确”的值更严重?例如,程序会崩溃吗?
诚然,这个问题可能看起来很愚蠢,因为毕竟使用InterlockedIncrement
而不是的成本inc
相当低(在许多情况下,但不是全部!),因此(也许)不安全是愚蠢的。但我也觉得在理论上知道它是如何工作的会很好,所以我仍然认为这个问题很有趣。