Является ли синхронизация для замены переменных дешевле, чем для чего-то еще? - PullRequest
0 голосов
/ 31 января 2019

В многопоточной среде, не правда ли, что каждая операция в ОЗУ должна быть synchronized?

Скажем, у меня есть переменная, которая является указателем на другой адрес памяти:

foo     12345678

Теперь, если один поток устанавливает эту переменную в другой адрес памяти (скажем, 89ABCDEF), в то время как первый поток читает переменную, не может ли быть так, что первый поток полностью читает мусор изпеременная, если доступ не будет synchronized (на некотором системном уровне)?

foo     12345678  (before)
        89ABCDEF  (new data)
        •••••     (writing thread progress)
        89ABC678  (memory content)

Поскольку я никогда не видел, чтобы такие вещи происходили, я предполагаю, что это некоторая синхронизация на системном уровне при записипеременные.Я предполагаю, что именно поэтому она называется «атомарной» операцией.Как я обнаружил здесь , эта проблема на самом деле является темой и не является полностью выдуманной для меня.

С другой стороны, я везде читал, что синхронизация оказывает существенное влияние на производительность.(Помимо потоков, которые должны ждать до нашей эры. Они не могут войти в блокировку; я имею в виду только действие блокировки и разблокировки.) Например, здесь :

synchronized добавляет значительноенакладные расходы на методы […].Эти операции довольно дороги [...], что крайне влияет на производительность программы.[…] Дорогостоящие синхронизированные операции, которые вызывают ужасную медлительность кода.

Как это сочетается?Почему блокировка для изменения переменной незаметна быстро, а блокировка для чего-то еще такого дорогого?Или это одинаково дорого, и должен быть большой предупреждающий знак при использовании - скажем, long и double, потому что они всегда неявно требуют синхронизации?

Ответы [ 3 ]

0 голосов
/ 31 января 2019

Что касается вашей первой точки зрения, когда процессор записывает некоторые данные в память, эти данные всегда записываются правильно и не могут быть "перехвачены" другими записями процессами потоков, ОС и т. Д. Это не вопрос синхронизации, просто требуетсядля обеспечения правильного поведения оборудования.

Синхронизация - это программная концепция, требующая аппаратной поддержки.Предположим, что вы просто хотите получить замок.Предполагается, что он свободен, когда в 0 и заблокирован, когда в 1.

Основной способ сделать это -

got_the_lock=0
while(!got_the_lock)
  fetch lock value from memory  
  set lock value in memory to 1
  got_the_lock = (fetched value from memory ==  0)
done
print  "I got the lock!!"  

Проблема в том, что если другие потоки делают то же самое наВ то же время и считывание значения блокировки до того, как оно будет установлено в 1, несколько потоков могут подумать, что получили блокировку.

Чтобы избежать этого, нужен атомарный доступ к памяти.Атомарный доступ, как правило, представляет собой цикл чтения-изменения-записи данных в памяти, который не может быть прерван и который запрещает доступ к этой информации до завершения.Таким образом, не все обращения являются атомарными, только специальная операция чтения-изменения-записи, и она реализуется благодаря специальной поддержке процессора (см. Инструкции test-and-set или fetch-and-add , например).Большинству обращений это не нужно и может быть обычным.Атомарный доступ в основном используется для синхронизации потоков, чтобы гарантировать, что только один поток находится в критической секции.

Так почему атомный доступ дорог?Есть несколько причин.

  1. Во-первых, необходимо обеспечить правильный порядок инструкций.Вы, вероятно, знаете, что порядок команд может отличаться от порядка программ инструкций при условии соблюдения семантики программы.Это в значительной степени используется для повышения производительности: команды переупорядочения компилятора, процессор выполняют их не по порядку, кэши обратной записи записывают данные в память в любом порядке, а буфер записи в память делает то же самое.Это переупорядочение может привести к неправильному поведению.
1 while (x--) ;         // random and silly loop
2 f(y);
3 while(test_and_set(important_lock)) ; //spinlock to get a lock
4 g(z);

Очевидно, что инструкция 1 не является ограничивающей, а 2 может быть выполнена раньше (и, вероятно, 1 будет удалено оптимизирующим компилятором).Но если 4 выполняется до 3, поведение будет не таким, как ожидалось.

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

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

Атомный доступ требует по крайней мере 100-200 циклов на современных процессорах и соответственно очень дорого.

Как это сочетается?Почему блокировка для изменения переменной незаметна быстро, а блокировка для чего-то еще такого дорогого?Или это одинаково дорого, и должен быть большой предупреждающий знак при использовании - скажем, long и double, потому что они всегда неявно требуют синхронизации?

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

0 голосов
/ 31 января 2019

не правда ли, что все операции в ОЗУ должны быть синхронизированы?

Нет.Большинство «операций с оперативной памятью» будут нацелены на области памяти, которые используются только одним потоком.Например, в большинстве языков программирования ни один из аргументов функции или локальных переменных потока не будет предоставлен другим потокам;и часто поток будет использовать объекты кучи, которые он не разделяет ни с одним другим потоком.

Вам необходима синхронизация, когда два или более потоков взаимодействуют друг с другом через общие переменные.Он состоит из двух частей:

взаимное исключение

Возможно, вам потребуется предотвратить "условия гонки".Если какой-то поток T обновляет структуру данных, ему может потребоваться перевести структуру во временное недопустимое состояние до завершения обновления.Вы можете использовать взаимное исключение (т. Е. Мьютексы / семафоры / блокировки / критические секции), чтобы гарантировать, что никакой другой поток U не сможет увидеть структуру данных, когда она находится в этом временном недопустимом состоянии.

согласованность кэша

На компьютере с более чем одним процессором каждый процессор обычно имеет свой кэш-память.Таким образом, когда два разных потока, работающих на двух разных процессорах, обращаются к одним и тем же данным, каждый из них может просматривать свою собственную, отдельно кэшированную копию.Таким образом, когда поток T обновляет эту общую структуру данных, важно обеспечить, чтобы все обновленных им переменных попали в кэш потока U, прежде чем поток U сможет увидеть любую из них.

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

0 голосов
/ 31 января 2019

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

Однако такой конфликт может быть сведен к минимумуиспользуя синхронизацию на более детальном уровне, как в операции CAS (сравнение и замена), выполняемой ЦП, или барьер памяти для чтения энергозависимой переменной.Гораздо лучший вариант - вообще избежать синхронизации без ущерба для безопасности.

Рассмотрим следующий код:

synchronized(this) {
    // a DB call
}

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

Это причина неблокирующих алгоритмов, таких как Treiber Stack Michael Scott существовать.Они выполняют свои задачи (в противном случае мы бы использовали гораздо больший синхронизированный блок) с минимальным объемом синхронизации.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...