Безопасно ли использовать «небезопасные» функции потоков? - PullRequest
8 голосов
/ 07 января 2012

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

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

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

1 Ответ

10 голосов
/ 07 января 2012

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

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

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

.
...