автор:дядя мяу
оригинал:blog.beta cat.IO/post/go wave…
В стандартной библиотеке языка Gosync/atomic
Пакеты инкапсулируют атомарные операции, предоставляемые базовым оборудованием, в функции Go. Но эти операции поддерживают только несколько основных типов данных, поэтому для расширения сферы применения атомарных операций язык Go в версии 1.4 доsync/atomic
В пакет добавлен новый типValue
. Значение этого типа действует как контейнер и может использоваться для хранения и загрузки «атомарных»любой типценность .
историческое происхождение
я здесьgolang-dev
14 лет в списке рассылкиэто обсуждение, сообщил пользовательencoding/gob
проблемы с производительностью на многоядерной машине (80-ядерной), думаюencoding/gob
Причина, по которой возможность многоядерности не может быть использована в полной мере, заключается в том, что в ней используется большое количество мьютексов, если заменить эти мьютексы наatomic.LoadPointer/StorePointer
Чтобы сделать управление параллелизмом, производительность будет улучшена в 20 раз.
В ответ на эту проблему было предложено, чтобы существующиеatomic
пакет на основе пакетаatomic.Value
типы, чтобы пользователи могли использовать Go, не полагаясь на внутренние типыunsafe.Pointer
используется в случаеatomic
Предусмотрены атомарные операции. Итак, что мы видим сейчасatomic
В дополнение к пакетуatomic.Value
Кроме того, остальные были написаны на ассемблере в первые дни, иatomic.Value
Базовая реализация типа также основывается на существующейatomic
пакетная основа.
Так почему же в описанном выше сценарииatomic
было бы лучше, чемmutex
Производительность намного лучше? авторDmitry VyukovСуммирует одно различие между ними:
Mutexes do no scale. Atomic loads do.
Mutex
Зависит отоперационная системапонял, иatomic
Атомарные операции в пакете задаются выражениембазовое оборудованиеОказывать непосредственную поддержку. В наборе инструкций, реализуемом ЦП, некоторые инструкции инкапсулированы вatomic
package, эти инструкции не могут прерываться (прерывать) во время выполнения, поэтому атомарные операции могут бытьlock-free
Он гарантирует безопасность параллелизма в случае ЦП, а его производительность также может линейно увеличиваться с увеличением количества ЦП.
Что ж, сказав так много атомарных операций, давайте сначала посмотрим, какие операции можно назватьатомарная операция.
атомарность
Свойство того, что одна или несколько операций не прерываются во время выполнения ЦП, называетсяатомарность. Эти операции представляются внешнему миру как неразрывное целое, они либо выполняются, либо не выполняются, внешний мир не увидит, что они выполнены лишь наполовину. В реальном мире центральный процессор не может выполнять серию операций без перерыва, но если мы выполняем несколько операций, мы можем сделать ихПромежуточное состояние невидимо для внешнего мира, то мы можем утверждать, что они обладают «неделимой» атомарностью.
Некоторые друзья могут не знать, что в Go (и даже в большинстве языков) обычный оператор присваивания не является атомарной операцией. Например, на 32-битной машине записьint64
Переменная типа будет иметь промежуточное состояние, потому что она будет разделена на две записи (MOV
) — запишите младшие 32 бита и старшие 32 бита, как показано на следующем рисунке:
Если поток только что закончил записывать младшие 32 бита и не успел записать старшие 32 бита, другой поток читает эту переменную, то он получает нелогичную промежуточную переменную, которая, скорее всего, заставит нашу программу выглядеть странной ошибкой. .
Это всего лишь базовый тип.Если мы присваиваем значение структуре, у нее выше вероятность проблем с параллелизмом. Очень вероятно, что записывающий поток только что закончил запись небольшой половины поля, а читающий поток будет читать переменную, поэтому можно будет прочитать только часть измененного значения. Это, очевидно, разрушает целостность переменной, и считанное значение совершенно неверно.
Столкнувшись с проблемой чтения и записи переменных в условиях многопоточности, наш главный герой——atomic.Value
появляется на сцене, это позволяет нам не полагаться наunsafe.Pointer
типа и может инкапсулировать операции чтения и записи любого типа данных в атомарные операции (что делает промежуточные состояния невидимыми для внешнего мира).
использовать позу
atomic.Value
Есть два способа показать типы внешнему миру:
-
v.Store(c)
- операции записи, исходная переменнаяc
хранится вatomic.Value
Типv
внутри. -
c = v.Load()
- читать операции из потокобезопасногоv
Прочтите содержимое, сохраненное на предыдущем шаге.
Краткий интерфейс делает его использование очень простым, просто используйте операции чтения и назначения переменных, которые должны быть защищены от параллелизма.Load()
а такжеStore()
Просто замените его.
Ниже приведен распространенный сценарий использования: приложение периодически получает последнюю информацию о конфигурации из внешнего мира, а затем изменяет переменные конфигурации, хранящиеся в собственной памяти. Рабочие потоки обрабатывают запросы в соответствии с последней конфигурацией.
package main
import (
"sync/atomic"
"time"
)
func loadConfig() map[string]string {
// 从数据库或者文件系统中读取配置信息,然后以map的形式存放在内存里
return make(map[string]string)
}
func requests() chan int {
// 将从外界中接受到的请求放入到channel里
return make(chan int)
}
func main() {
// config变量用来存放该服务的配置信息
var config atomic.Value
// 初始化时从别的地方加载配置文件,并存到config变量里
config.Store(loadConfig())
go func() {
// 每10秒钟定时的拉取最新的配置信息,并且更新到config变量里
for {
time.Sleep(10 * time.Second)
// 对应于赋值操作 config = loadConfig()
config.Store(loadConfig())
}
}()
// 创建工作线程,每个工作线程都会根据它所读取到的最新的配置信息来处理请求
for i := 0; i < 10; i++ {
go func() {
for r := range requests() {
// 对应于取值操作 c := config
// 由于Load()返回的是一个interface{}类型,所以我们要先强制转换一下
c := config.Load().(map[string]string)
// 这里是根据配置信息处理请求的逻辑...
_, _ = r, c
}
}()
}
}
Внутренняя реализация
Луо Юн ХаоСказал:
Simplicity is the hidden complexity
Давайте посмотрим, какая скрытая сложность скрывается за его простым внешним видом.
структура данных
atomic.Value
предназначен для хранения любых типов данных, поэтому его внутреннее поле представляет собойinterface{}
Типа, очень простой и грубый.
type Value struct {
v interface{}
}
КромеValue
Кроме того, этот файл также определяетifaceWords
тип, который на самом деле является пустым интерфейсом (interface{}
) внутренний формат представления (см. определение eface в runtime/runtime2.go). Его эффект заключается вinterface{}
Введите разложение, чтобы получить два поля.
type ifaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}
Операция записи (сохранения)
Прежде чем приступить к написанию, давайте взглянем на внутренности языка Go.unsafe.Pointer
Типы.
unsafe.Pointer
Из соображений безопасности язык Go не поддерживает прямое манипулирование памятью, но его стандартная библиотека предоставляетНебезопасно (обратная совместимость не гарантируется)тип указателяunsafe.Pointer
, чтобы программа могла гибко манипулировать памятью.
unsafe.Pointer
Особенность заключается в том, что он может обходить системную проверку типов языка Go и преобразовывать в любой тип указателя и из него. То есть, если оба типа имеют одинаковую структуру памяти (макет), мы можем положитьunsafe.Pointer
В качестве моста пусть эти два типа указателей преобразуют друг друга, чтобы одна и та же память имела два разныхинтерпретироватьСпособ.
Например,[]byte
а такжеstring
По сути, внутренняя структура хранения одинакова, но система типов языка Go запрещает их взаимозаменяемость. если с помощьюunsafe.Pointer
, мы можем понять, что в случае нулевой копии[]byte
массив преобразуется непосредственно вstring
Типы.
bytes := []byte{104, 101, 108, 108, 111}
p := unsafe.Pointer(&bytes) //强制转换成unsafe.Pointer,编译器不会报错
str := *(*string)(p) //然后强制转换成string类型的指针,再将这个指针的值当做string类型取出来
fmt.Println(str) //输出 "hello"
понялunsafe.Pointer
Роль мы можем посмотреть прямо в коде:
func (v *Value) Store(x interface{}) {
if x == nil {
panic("sync/atomic: store of nil value into Value")
}
vp := (*ifaceWords)(unsafe.Pointer(v)) // Old value
xp := (*ifaceWords)(unsafe.Pointer(&x)) // New value
for {
typ := LoadPointer(&vp.typ)
if typ == nil {
// Attempt to start first store.
// Disable preemption so that other goroutines can use
// active spin wait to wait for completion; and so that
// GC does not see the fake type accidentally.
runtime_procPin()
if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
runtime_procUnpin()
continue
}
// Complete first store.
StorePointer(&vp.data, xp.data)
StorePointer(&vp.typ, xp.typ)
runtime_procUnpin()
return
}
if uintptr(typ) == ^uintptr(0) {
// First store in progress. Wait.
// Since we disable preemption around the first store,
// we can wait with active spinning.
continue
}
// First store completed. Check type and overwrite data.
if typ != xp.typ {
panic("sync/atomic: store of inconsistently typed value into Value")
}
StorePointer(&vp.data, xp.data)
return
}
}
Приблизительная логика:
- Строки 5~6 - пройти
unsafe.Pointer
Будутока такженаписатьзначения преобразуются вifaceWords
введите, чтобы мы могли получить эти два следующихinterface{}
Тип примитива (typ) и реальное значение (data). - Начиная со строки 7 — бесконечный цикл for. Сотрудничать
CompareAndSwap
Съедобный, можно добиться эффекта оптимистичного замка. - Строка 8, мы можем пройти
LoadPointer
Эта атомарная операция получает текущийValue
тип хранится в . В соответствии с различием этого типа рассматриваются следующие три случая.
- Сначала напишите (строки 9~24) - a
Value
После инициализации экземпляра егоtyp
В поле будет установлено нулевое значение nil указателя, поэтому строка 9 сначала определяет,typ
равен нулю, то доказывает этоValue
Данные еще не записаны. После этого выполняется начальная операция записи:-
runtime_procPin()
Это функция в рантайме, я не особо разбираюсь в конкретной функции и соответствующей документации не нашел. Угадайте здесь, с одной стороны, он запрещает планировщику вытеснять текущую горутину, чтобы она не прерывалась при выполнении текущей логики, чтобы работа могла быть завершена как можно быстрее, потому что ее ждали другие. С другой стороны, потоки GC не могут быть включены, пока вытеснение отключено, что не позволяет потокам GC видеть необъяснимый указатель.^uintptr(0)
Тип (это промежуточное состояние в процессе присвоения). - использовать
CAS
операции, сначала попробуйтеtyp
Установить как^uintptr(0)
это промежуточное состояние. В случае неудачи он доказывает, что другой поток упреждающе завершил операцию присваивания, снимает упреждающую блокировку и возвращается к первому шагу цикла for. - Если установка выполнена успешно, это доказывает, что текущий поток схватил «оптимистическую блокировку» и может безопасно
v
Установите новое переданное значение (строки 19~23). Обратите внимание, что здесь первая записьdata
поле, а затем напишитеtyp
поле. потому что мыtyp
Значение поля используется в качестве основы для оценки того, завершена запись или нет.
-
- Первая запись не завершена (строки 25~30) - если вы видите
typ
поле или^uintptr(0)
Этот промежуточный тип доказывает, что первая запись не была завершена, поэтому он будет продолжать цикл «ожидание занятости», пока первая запись не будет завершена. - Выполняется первая запись (строка 31 и позже) — сначала проверьте, соответствует ли тип, записанный последним, типу, который будет записан на этот раз, и выдайте исключение, если нет. В противном случае напрямую запишите значение, которое будет записано на этот раз, в
data
поле.
Основная идея этой логики заключается в том, что для завершения атомарной записи нескольких полей мы можем захватить одно из полей и использовать его состояние для отметки состояния всей атомарной записи. у меня есть эта идеяТранзакции в TiDBЧто-то похожее я видел в реализации, там это назвалиPercolator
Модель, основная идея - выбрать сначала однуprimaryRow
, то все операции также начинаются сprimaryRow
успех или неудача как знак. Что ж, верно, нет ничего нового под солнцем.
Если у вас нет терпения посмотреть на код, ничего страшного, вот упрощенная версия блок-схемы:
Операция чтения (загрузки)
Сначала код:
func (v *Value) Load() (x interface{}) {
vp := (*ifaceWords)(unsafe.Pointer(v))
typ := LoadPointer(&vp.typ)
if typ == nil || uintptr(typ) == ^uintptr(0) {
// First store not yet completed.
return nil
}
data := LoadPointer(&vp.data)
xp := (*ifaceWords)(unsafe.Pointer(&x))
xp.typ = typ
xp.data = data
return
}
Чтение относительно простое, оно имеет две ветви:
- Если текущий
typ
равен нулю или^uintptr(0)
, это доказывает, что первая запись не началась или не была завершена, а затем возвращает nil напрямую (без раскрытия промежуточного состояния внешнему миру). - В противном случае, согласно текущему увиденному
typ
а такжеdata
построить новыйinterface{}
Вернитесь назад.
Суммировать
Эта статья начинается с обсуждения в списке рассылки, представляяatomic.Value
Историческая причина предложенного. Затем он представляет свою позицию использования и внутреннюю реализацию от мелкой до глубокой. Пусть все знают не только что это такое, но и почему это так.
Кроме того, опять же, атомарные операции задаютсябазовое оборудованиеподдерживается, в то время как блокировкаоперационная системаОбеспечена реализация API. Если реализована одна и та же функция, первая обычно более эффективна и использует преимущества нескольких ядер компьютера. Поэтому, когда мы хотим обновить некоторые переменные одновременно и безопасно в будущем, мы должны предпочесть использоватьatomic.Value
реализовать.