Путаница в определении гонки данных - PullRequest
0 голосов
/ 14 октября 2018

Гонка данных происходит, когда в программе есть два обращения к памяти, когда оба:

  • нацелены на одно и то же местоположение
  • одновременно выполняются двумя потоками
  • не читаются
  • не являются операциями синхронизации

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

Теперь рассмотрим этот пример:

import java.util.concurrent.*;

class DataRace{
   static boolean flag = false;
   static void raiseFlag() {
      flag = true;
   }
   public static void main(String[] args) {
      ForkJoinPool.commonPool().execute(DataRace::raiseFlag);
      System.out.println(flag);
  }
}

Насколько я понимаю, это удовлетворяет определению гонки данных.У нас есть две инструкции для доступа к одному и тому же местоположению (флаг), обе не считываются, обе являются одновременными и не являются операциями синхронизации.И поэтому выходные данные зависят от того, как потоки чередуются, и могут иметь значение «True» или «False».

Если мы предположим, что это гонка данных, тогда я могу просто добавить блокировки перед доступом и решить это.Но даже если я добавлю блокировки в оба потока, мы знаем, что в замках также есть условие гонки.Таким образом, любой поток может получить блокировку, и выходные данные все еще могут быть «True» или «False».

Так что это мое замешательство, и вот два вопроса, которые я хотел бы задать:

  1. Это гонка данных?Если нет, то почему?

  2. Если это гонка данных, почему предлагаемое решение не работает?

1 Ответ

0 голосов
/ 15 октября 2018

Прежде всего, произвольный порядок выполнения потока не является самой гонкой данных, даже если это может вызвать ее.Если вам нужно синхронизировать 2 или более потоков для выполнения их кода в определенном порядке, вы должны использовать механизм ожидания, такой как мониторы .Мониторы - это конструкции, которые могут выполнять как взаимное исключение (блокировка) , так и ожидание.Мониторы также известны как условные переменные, и Java поддерживает их.

Теперь вопрос в том, что такое гонка данных.Гонка данных происходит, когда 2 или более потоков обращаются к одной и той же ячейке памяти в одно и то же время, и некоторые из обращений являются записями.Эта ситуация может привести к непредсказуемым значениям, которые может содержать ячейка памяти.

Классический пример.Позволяет иметь 32-битную ОС и переменную длиной 64 бита, как у типов long или double.Давайте иметь переменную long.

long SharedVariable;

И поток 1, который выполняет следующий код.

SharedVariable=0;

И поток 2, который выполняет следующий код.

SharedVariable=0x7FFF_FFFF_FFFF_FFFFL;

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

SharedVariable==0
SharedVariable==0x7FFF_FFFF_FFFF_FFFFL
**SharedVariable==0x0000_0000_FFFF_FFFFL**
**SharedVariable==0x7FFF_FFFF_0000_0000L**

Последние 2 значения являются неожиданными -вызванный гонкой данных.

Проблема здесь в том, что в 32-битных ОС существует гарантия того, что доступ к 32-битным переменным является атомарным - поэтому платформа гарантирует, что даже если 2 или более потоков обращаются кта же 32-битная ячейка памяти, в то же время доступ к этой ячейке памяти является атомарным - только одна нить может получить доступ к такой переменной.Но поскольку у нас есть 64-битная переменная, на уровне ЦП переменная записи в 64-битную длину преобразуется в 2 инструкции ЦП.Таким образом, код SharedVariable=0; переводится примерно так:

mov SharedVariableHigh32bits,0
mov SharedVariableLow32bits,0

А код SharedVariable=0x7FFF_FFFF_FFFF_FFFFL; переводится примерно так:

mov SharedVariableHigh32bits,0x7FFFFFFF
mov SharedVariableLow32bits,0xFFFFFFFF

Без блокировки процессор можетвыполните эти 4 инструкции в следующих приказах.

Заказ 1.

mov SharedVariableHigh32bits,0 // T1
mov SharedVariableLow32bits,0 // T1
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2

Результат: 0x7FFF_FFFF_FFFF_FFFFL.

Заказ 2.

mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2
mov SharedVariableHigh32bits,0  // T1
mov SharedVariableLow32bits,0  // T1

Результат: 0.

Заказ 3.

mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableHigh32bits,0 // T1
mov SharedVariableLow32bits,0 // T1
mov SharedVariableLow32bits,0xFFFFFFFF // T2

Результат: 0x0000_0000_FFFF_FFFFL.

Заказ 4.

mov SharedVariableHigh32bits,0 // T1
mov SharedVariableHigh32bits,0x7FFFFFFF // T2
mov SharedVariableLow32bits,0xFFFFFFFF // T2
mov SharedVariableLow32bits,0 // T1

Результат: 0x7FFF_FFFF_0000_0000L.

Итак, условие гонки вызвало серьезную проблему, поскольку вы можете получить значение, которое является совершенно неожиданным и недействительным.Используя блокировки, вы можете предотвратить это, но простое использование блокировки не гарантирует порядок выполнения - какой поток выполняет свой код первым.Поэтому, если вы используете блокировку, вы получите только 2 порядка исполнения - порядок 1 и порядок 2, а не неожиданные значения 0x0000_0000_FFFF_FFFFL и 0x7FFF_FFFF_0000_0000L.Но, тем не менее, если вам нужно синхронизировать, какой поток выполняется первым, а какой - вторым, вам нужно использовать не только блокировку, но и механизм ожидания, который предлагает мониторинг (условные переменные).

BTW в соответствии с этим статья , Java гарантирует атомарный доступ ко всем переменным примитивного типа, кроме long и double.На 64-битных платформах даже доступ к long и double должен быть атомарным, но похоже, что стандарт не гарантирует этого.

И даже если стандарт гарантирует атомарный доступ, он всегда будетлучше использовать замки.Блокировки определяют барьеры памяти , которые препятствуют некоторым оптимизациям компилятора, которые могут переупорядочить ваш код на уровне команд ЦП и вызывают проблемы при использовании переменных для управления порядком выполнения.

Итак, простоесовет здесь заключается в том, что если вы не являетесь экспертом по параллельному программированию (а я тоже нет) и не пишете SW, который должен получить абсолютную максимальную производительность с помощью методов без блокировок, всегда используйте блокировки - даже если обращаетесь к переменнымкоторые гарантированно имеют атомарный доступ.

...