Безопасно ли использовать "небезопасные" функции потоков?

Прошу прощения за мой слегка юмористический титул. Я использую два разных определения слова "безопасный" в нем (очевидно).

Я довольно плохо знаком с потоками (ну, я использовал потоки много лет, но только очень простые формы). Теперь я столкнулся с проблемой написания параллельных реализаций некоторых алгоритмов, и потоки должны работать с одними и теми же данными. Рассмотрим следующую ошибку новичка:

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 довольно низкий (во многих случаях, но не все!), и поэтому (возможно) глупо не играть безопасно. Но я также чувствую, что было бы хорошо узнать, как это действительно работает на теоретическом уровне, поэтому я все еще думаю, что этот вопрос очень интересен.

1 ответ

Решение

Ваша программа никогда не завершится сбоем из-за скачка с приращением целого числа, которое используется только как счетчик. Все, что может пойти не так, это то, что вы не получите правильный ответ. Очевидно, что если вы используете целое число в качестве индекса в массиве или, возможно, это был указатель, то у вас могут возникнуть проблемы.

Если вы не увеличиваете это значение невероятно часто, трудно представить, что блокируемый прирост будет достаточно дорогим, чтобы вы заметили разницу в производительности.

Более того, наиболее эффективный подход состоит в том, чтобы каждый поток поддерживал свой собственный счетчик. Затем суммируйте все отдельные потоки, когда вы присоединитесь к потокам в конце расчета. Таким образом, вы получаете лучшее из обоих миров. Нет разногласий по поводу увеличения и правильного ответа. Конечно, вам нужно принять меры, чтобы избежать ложного обмена.

Другие вопросы по тегам