Прошу прощения за мой слегка юмористический заголовок. Я использую в нем два разных определения слова «безопасный» (очевидно).
Я новичок в многопоточности (ну, я использовал многопоточность в течение многих лет, но только в очень простых ее формах). Теперь я столкнулся с проблемой написания параллельных реализаций некоторых алгоритмов, и потоки должны работать на тех же данных. Рассмотрим следующую ошибку новичка:
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
. Действительно, первое значение
равно 0
, а затем мы inc
это 20000000
раз. Однако, поскольку процедура inc
не является «атомарной», два потока будут конфликтовать (я предполагаю что inc
делает три вещи: читает, увеличивает и сохраняет), поэтому многие из inc
будут фактически «потеряны». A типичное значение, которое я получаю из приведенного выше кода, составляет 10030423
.
Простейший обходной путь - использовать InterlockedIncrement
вместо Inc
(что будет намного медленнее в этом глупом примере, но дело не в этом). Другой обходной путь - поместить inc
в критическую секцию (да, в этом глупом примере это тоже будет очень медленным).
В большинстве реальных алгоритмов конфликты встречаются не так часто. На самом деле они могут быть очень необычными. Один из моих алгоритмов создает фракталов DLA , и одна из переменных, которые я inc
то и дело, - это количество адсорбированных частиц. Конфликты здесь очень редки, и, что более важно, меня действительно не волнует, дает ли переменная сумма до 20000000, 20000008, 20000319 или 19999496. Таким образом, возникает соблазн , а не использовать InterlockedIncrement
или критические секции, так как они просто раздувают код и делают его (незначительно) медленнее, а не (насколько я понимаю) преимуществом.
Однако у меня вопрос: могут ли быть более серьезные последствия конфликтов, чем слегка «неправильное» значение увеличивающейся переменной? Может ли, например, произойти сбой программы?
По общему признанию, этот вопрос может показаться глупым, поскольку, в конце концов, стоимость использования InterlockedIncrement
вместо inc
довольно низка (во многих случаи, но не все!), и поэтому (возможно) глупо не перестраховаться. Но я также чувствую, что было бы хорошо узнать, как это действительно работает на теоретическом уровне, поэтому я все еще думаю, что этот вопрос очень интересен.