перейти на атомарную загрузку и сохранение

func resetElectionTimeoutMS(newMin, newMax int) (int, int) {
    oldMin := atomic.LoadInt32(&MinimumElectionTimeoutMS)
    oldMax := atomic.LoadInt32(&maximumElectionTimeoutMS)
    atomic.StoreInt32(&MinimumElectionTimeoutMS, int32(newMin))
    atomic.StoreInt32(&maximumElectionTimeoutMS, int32(newMax))
    return int(oldMin), int(oldMax)
}

У меня есть функция кода перехода, подобная этой. Меня смущает: зачем нам здесь atomic? Чему это мешает?

Спасибо.


person BufBills    schedule 27.08.2014    source источник
comment
Контекст: плот   -  person miku    schedule 27.08.2014


Ответы (2)


Атомарные функции выполняют задачу изолированным образом, когда кажется, что все части задачи выполняются мгновенно или не выполняются вообще.

В этом случае LoadInt32 и StoreInt32 гарантируют, что целое число сохраняется и извлекается таким образом, что при загрузке не будет получено частичное сохранение. Однако вам нужно, чтобы обе стороны использовали атомарные функции, чтобы это работало правильно. Пример raft неверен как минимум по двум причинам.

  1. Две атомарные функции не действуют как одна атомарная функция, поэтому чтение старой и установка новой в две строки — это состояние гонки. Вы можете читать, затем кто-то другой устанавливает, затем вы устанавливаете, и вы возвращаете ложную информацию для предыдущего значения до того, как вы его установили.

  2. Не все, обращающиеся к MinimumElectionTimeoutMS, используют атомарные операции. Это означает, что использование атомарных чисел в этой функции практически бесполезно.

Как это исправить?

func resetElectionTimeoutMS(newMin, newMax int) (int, int) {
    oldMin := atomic.SwapInt32(&MinimumElectionTimeoutMS, int32(newMin))
    oldMax := atomic.SwapInt32(&maximumElectionTimeoutMS, int32(newMax))
    return int(oldMin), int(oldMax)
}

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

Каждая функция также должна быть изменена для выполнения атомарной загрузки:

func minimumElectionTimeout() time.Duration {
    min := atomic.LoadInt32(&MinimumElectionTimeoutMS)
    return time.Duration(min) * time.Millisecond
}

Я рекомендую вам внимательно рассмотреть цитату VonC, упомянутую в документации golang atomic:

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

Если вы хотите понять атомарные операции, я рекомендую вам начать с http://preshing.com/20130618/atomic-vs-non-atomic-operations/. Это касается операций загрузки и сохранения, используемых в вашем примере. Однако есть и другие применения для атомов. В обзоре пакета atomic рассматриваются некоторые интересные вещи, такие как атомарная подкачка (пример, который я привел), сравните и своп (известный как CAS), и Добавление.

Забавная цитата из ссылки, которую я вам дал:

хорошо известно, что в x86 32-битная инструкция mov является атомарной, если операнд в памяти выровнен естественным образом, и неатомарной в противном случае. Другими словами, атомарность гарантируется только в том случае, если 32-битное целое число расположено по адресу, который точно кратен 4.

Другими словами, в обычных сегодняшних системах атомарные функции, используемые в вашем примере, фактически не работают. Они уже атомные! (Однако они не гарантируются, если вам нужно, чтобы они были атомарными, лучше указать это явно)

person Stephen Weinberg    schedule 27.08.2014

Учитывая, что пакет atomic предоставляет низкоуровневые примитивы атомарной памяти, полезные для реализации алгоритмов синхронизации, я предположим, что он предназначался для использования в качестве:

  • MinimumElectionTimeoutMS не изменяется при сохранении в oldMin
  • MinimumElectionTimeoutMS не изменяется при установке нового значения newMin.

Но пакет поставляется с предупреждением:

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

В этом случае (server.go из протокол распределенного консенсуса Raft), синхронизация непосредственно с переменной может считаться более быстрой, чем добавление Mutex к функции all.

За исключением того, что, как показано в ответе Стивен Вайнберг (проголосовал), это не то, как вы используете atomic. Это только гарантирует, что oldMin является точным при выполнении обмена.

См. другой пример в разделе "Нужен ли двухатомарный код стиля в sync/atomic.once.go?" в связи с "модель памяти".


OneOfOne упоминает в комментариях с использованием атомарного CAS в качестве спин-блокировки (очень быстрая блокировка):

BenchmarkSpinL-8            2000            708494 ns/op           32315 B/op       2001 allocs/op
BenchmarkMutex-8            1000           1225260 ns/op           78027 B/op       2259 allocs/op

Видеть:

person VonC    schedule 27.08.2014
comment
Спасибо. Так по существу. это похоже на использование блокировки для защиты var MinimumElectionTimeoutMS при присвоении ее значения oldMin. Верно? - person BufBills; 27.08.2014
comment
Нет, это не действует как замок. Фактически, предоставленный код плота неправильно использует атомарность. - person Stephen Weinberg; 27.08.2014
comment
@StephenWeinberg, это то, что я подозревал, читая предупреждение, которое идет с пакетом atomic ... - person VonC; 27.08.2014
comment
@NoName вы можете использовать атомарный CAS в качестве спин-блокировки, например github.com/OneOfOne/go-utils/blob/master/sync/spinlock.go - person OneOfOne; 27.08.2014
comment
@OneOfOne, потому что это быстрее, чем Mutex или Lock? (как в github.com/OneOfOne/go-utils/blob /master/sync/spinlock_test.go) - person VonC; 27.08.2014
comment
@VonC намного быстрее, 2000 708960 ns/op спин-блокировка против 1000 1220278 ns/op мьютекса. go test github.com/OneOfOne/go-utils/sync/ -bench=. -benchmem -cpu 8 -v на i7 - person OneOfOne; 27.08.2014
comment
@OneOfOne впечатляет! Я включил это использование в ответ для большей наглядности. - person VonC; 27.08.2014
comment
Спин-блокировки быстры только тогда, когда у вас мало или совсем нет состязаний. Тесты отличные, но реальный мир даст вам совсем другую производительность. Я рекомендую обычный sync.Mutex, если вы не обнаружите, что это ваше узкое место. Только тогда вы должны рассмотреть возможность использования спин-блокировки. - person Stephen Weinberg; 27.08.2014