Sync golang: Разбираемся с новым sync.Map в Go 1.9 / Хабр
Разбираемся с новым sync.Map в Go 1.9 / Хабр
Одним из нововведений в Go 1.9 было добавление в стандартную библиотеку нового типа sync.Map, и если вы ещё не разобрались что это и для чего он нужен, то эта статья для вас.
Для тех, кому интересен только вывод, TL;DR:
если у вас высоконагруженная (и 100нс решают) система с большим количеством ядер процессора (32+), вы можете захотеть использовать sync.Map вместо стандартного map+sync.RWMutex. В остальных случаях, sync.Map особо не нужен.
Если же интересны подробности, то давайте начнем с основ.
Если вы работаете с данными в формате «ключ»-«значение», то всё что вам нужно это встроенный тип map
(карта). Хорошее введение, как пользоваться map есть в Effective Go и блог-посте «Go Maps in Action».
map
— это generic структура данных, в которой ключом может быть любой тип, кроме слайсов и функций, а значением — вообще любой тип. По сути это хорошо оптимизированная хеш-таблица. Если вам интересно внутреннее устройство map — на прошлом GopherCon был очень хороший доклад на эту тему.
Вспомним как пользоваться map:
// инициализация
m := make(map[string]int)
// запись
m["habr"] = 42
// чтение
val := m["habr"]
// чтение с comma,ok
val, ok := m["habr"] // ok равен true, если ключ найден
// итерация
for k, v := range m { ... }
// удаление
delete(m, "habr")
Во время итерации значения в map могут изменяться.
Go, как известно, является языком созданным для написания concurrent программ — программ, который эффективно работают на мультипроцессорных системах. Но тип map не безопасен для параллельного доступа. Тоесть для чтения, конечно, безопасен — 1000 горутин могут читать из map без опасений, но вот параллельно в неё ещё и писать — уже нет. До Go 1.8 конкурентный доступ (чтение и запись из разных горутин) могли привести к неопределенности, а после Go 1.8 эта ситуация стала явно выбрасывать панику с сообщением «concurrent map writes».
Решение делать или нет map потокобезопасным было непростым, но было принято не делать — эта безопасность не даётся бесплатно. Там где она не нужна, дополнительные средства синхронизации вроде мьютексов будут излишне замедлять программу, а там где она нужна — не составляет труда реализовать эту безопасность с помощью sync.Mutex.
В текущей реализации map очень быстр:
Такой компромисс между скоростью и потокобезопасностью, при этом оставляя возможность иметь и первый и второй вариант. Либо у вас сверхбыстрый map безо всяких мьютексов, либо чуть медленнее, но безопасен для параллельного доступа. Важно тут понимать, что в Go использование переменной параллельно несколькими горутинами — это не далеко единственный способ писать concurrent-программы, поэтому кейс этот не такой частый, как может показаться вначале.
Давайте, посмотрим, как это делается.
Реализация потокобезопасной map очень проста — создаём новую структуру данных и встраиваем в неё мьютекс. Структуру можно назвать как угодно — хоть MyMap, но есть смысл дать ей осмысленное имя — скорее всего вы решаете какую-то конкретную задачу.
type Counters struct {
sync.Mutex
m map[string]int
}
Мьютекс никак инициализировать не нужно, его «нулевое значение» это разлоченный мьютекс, готовый к использованию, а map таки нужно, поэтому будет удобно (но не обязательно) создать функцию-конструктор:
func NewCounters() *Counters {
return &Counters{
m: make(map[string]int),
}
}
Теперь у переменной типа Counters будет метод Lock()
и Unlock()
, но если мы хотим упростить себе жизнь и использовать этот тип из других пакетов, то будет также удобно сделать функции обёртки вроде Load()
и Store()
. В таком случае мьютекс можно не встраивать, а просто сделать «приватным» полем:
type Counters struct {
mx sync.Mutex
m map[string]int
}
func (c *Counters) Load(key string) (int, bool) {
c.mx.Lock()
defer c.mx.Unlock()
val, ok := c.m[key]
return val, ok
}
func (c *Counters) Store(key string, value int) {
c.mx.Lock()
defer c.mx.Unlock()
c.m[key] = value
}
Тут нужно обратить внимание на два момента:
defer
имеет небольшой оверхед (порядка 50-100 наносекунд), поэтому если у вас код для высоконагруженной системы и 100 наносекунд имеют значение, то вам может быть выгодней не использоватьdefer
- методы
Get()
иStore()
должны быть определены для указателя наCounters
, а не наCounters
(тоесть неfunc (c Counters) Load(key string) int { ... }
, потому что в таком случае значение ресивера (c
) копируется, вместе с чем скопируется и мьютекс в нашей структуре, что лишает всю затею смысла и приводит к проблемам.
Вы также можете, если нужно, определить методы Delete()
и Range()
, чтобы защищать мьютексом map во время удаления и итерации по ней.
Кстати, обратите внимание, я намеренно пишу «если нужно», потому что вы всегда решаете конкретную задачу и в каждом конкретном случае у вас могут разные профили использования. Если вам не нужен Range()
— не тратьте время на его реализацию. Когда нужно будет — всегда сможете добавить. Keep it simple.
Теперь мы можем легко использовать нашу безопасную структуру данных:
counters := NewCounters()
counters.Store("habr", 42)
v, ok := counters.Load("habr")
В зависимости, опять же, от конкретной задачи, можно делать любые удобные вам методы. Например, для счётчиков удобно делать увеличение значения. С обычной map мы бы делали что-то вроде:
counters["habr"]++
а для нашей структуры можем сделать отдельный метод:
func (c *Counters) Inc(key string) {
c.mx.Lock()
defer c.mx.Unlock()
c.m[key]++
}
...
counters.Inc("habr")
Но часто у работы с данными в формате «ключ»-«значение», паттерн доступа неравномерный — либо частая запись, и редкое чтение, либо наоборот. Типичный случай — обновление происходит редко, а итерация (range) по всем значениям — часто. Чтение, как мы помним, из map — безопасно, но сейчас мы будем лочиться на каждой операции чтения, теряя без особой выгоды время на ожидание разблокировки.
В стандартной библиотеке для решения этой ситуации есть тип sync.RWMutex. Помимо Lock()/Unlock()
, у RWMutex есть отдельные аналогичные методы только для чтения — RLock()/RUnlock()
. Если метод нуждается только в чтении — он использует RLock()
, который не заблокирует другие операции чтения, но заблокирует операцию записи и наоборот. Давай обновим наш код:
type Counters struct {
mx sync.RWMutex
m map[string]int
}
...
func (c *Counters) Load(key string) (int, bool) {
c.mx.RLock()
defer c.mx.RUnlock()
val, ok := c.m[key]
return val, ok
}
Решения map+sync.RWMutex
являются почти стандартом для map, которые должны использоваться из разных горутин. Они очень быстрые.
До тех пор, пока у вас не появляется 64 ядра и большое количество одновременных чтений.
Если посмотреть на код sync.RWMutex, то можно увидеть, что при блокировке на чтение, каждая горутина должна обновить поле readerCount
— простой счётчик. Это делается атомарно с помощью функции из пакета sync/atomic atomic.AddInt32(). Эти функции оптимизированы под архитектуру конкретного процессора и реализованы на ассемблере.
Когда каждое ядро процессора обновляет счётчик, оно сбрасывает кеш для этого адреса в памяти для всех остальных ядер и объявляет, что владеет актуальным значением для этого адреса. Следующее ядро, прежде чем обновить счётчик, должно сначала вычитать это значение из кеша другого ядра.
На современном железе передача между L2 кешем занимает что-то около 40 наносекунд. Это немного, но когда много ядер одновременно пытаются обновить счётчик, каждое из них становится в очередь и ждёт эту инвалидацию и вычитывание из кеша. Операция, которая должна укладываться в константное время внезапно становится O(N) по количеству ядер. Эта проблема называется cache contention.
В прошлом году в issue-трекере Go была создана issue #17973 на эту проблему RWMutex. Бенчмарк ниже показывает почти 8-кратное увеличение времени на RLock()/RUnlock() на 64-ядерной машине по мере увеличения количества горутин активно «читающих» (использующих RLock/RUnlock):
А это бенчмарк на одном и том же количестве горутин (256) по мере увеличения количества ядер:
Как видим, очевидная линейная зависимость от количества задействованных ядер процессора.
В стандартной библиотеке map-ы используются довольно много где, в том числе в таких пакетах как encoding/json
, reflect
или expvars
и описанная проблема может приводить к не очень очевидным замедлениям в более высокоуровневом коде, который и не использует напрямую map+RWMutex, а, например, использует reflect.
Собственно, для решения этой проблемы — cache contention в стандартной библиотеке и был добавлен sync.Map.
Итак, ещё раз сделаю акцент — sync.Map решает совершенно конкретную проблему cache contention в стандартной библиотеке для таких случаев, когда ключи в map стабильны (не обновляются часто) и происходит намного больше чтений, чем записей.
Если вы совершенно чётко не идентифицировали в своей программе узкое место из-за cache contention в map+RWMutex, то, вероятнее всего, никакой выгоды от sync.Map
вы не получите, и возможно даже слегка потеряете в скорости.
Ну а если все таки это ваш случай, то давайте посмотрим, как использовать API sync.Map. И использовать его на удивление просто — практически 1-в-1 наш код раньше:
// counters := NewCounters() <--
var counters sync.Map
Запись:
counters.Store("habr", 42)
Чтение:
v, ok := counters.Load("habr")
Удаление:
counters.Delete("habr")
При чтении из sync.Map вам, вероятно также потребуется приведение к нужному типу:
v, ok := counters.Load("habr")
if ok {
val = v.(int)
}
Кроме того, есть ещё метод LoadAndStore(), который возвращает существующее значение, и если его нет, то сохраняет новое, и Range(), которое принимает аргументом функцию для каждого шага итерации:
v2, ok := counters.LoadOrStore("habr2", 13)
counters.Range(func(k, v interface{}) bool {
fmt.Println("key:", k, ", val:", v)
return true // if false, Range stops
})
API обусловлен исключительно паттернами использования в стандартной библиотеке. Сейчас sync.Map
используется в пакетах encoding/{gob/xml/json}, mime, archive/zip, reflect, expvars, net/rpc.
По производительности sync.Map гарантирует константное время доступа к map вне зависимости от количества одновременных чтений и количества ядер. До 4 ядер, sync.Map
при большом количестве параллельных чтений, может быть существенно медленнее, но после начинает выигрывать у map+RWMutex:
Резюмируя — sync.Map
это не универсальная реализация неблокирующей map-структуры на все случаи жизни. Это реализация для конкретного паттерна использования для, преимущественно, стандартной библиотеки. Если ваш паттерн с этим совпадает и вы совершенно чётко знаете, что узкое место в вашей программе это cache contention на map+sync.RWMutex
— смело используйте sync.Map. В противном случае, sync.Map
вам вряд ли поможет.
Если же вам просто лень писать map+RWMutex обертку и высокая производительность совершенно не критична, но нужна потокобезопасная map, то sync.Map
также может быть неплохим вариантом. Но не ожидайте от sync.Map слишком многого для всех случаев.
Так же для вашего случая могут больше подходить други реализации hash-таблиц, например на lock-free алгоритмах. Подобные пакеты были давно, и единственная причина, почему sync.Map находится в стандартной библиотеке — этого его активное использование другими пакетами из стандратной библиотеки.
Go sync.Pool / Хабр
Вольный пересказ документации к sync.Pool
Сборщик мусора (далее GC) не постоянно собирает мусор, а через определённые промежутки времени. В случае если ваш код выделяет память под некоторые структуры данных, а потом освобождает их — и так по кругу — это вызывает определённое давление на GC, в том числе заставляет runtime
обратиться к ОС для выделения новой памяти. Представьте: выделяем кусок (например []byte
), работаем с ним, освобождаем. Пройдёт определённое время, прежде GC «очнётся ото сна» и соберёт этот кусок. Если в это время мы выделим ещё один такой же кусок и уже выделенной у ОС памяти на это не хватит, то приложение будет вынуждено запросить у ОС ещё памяти. По времени приложения запрос памяти у ОС длится вечность. А в это самое время где-то пылится, ждёт своего часа тот прежний «отработанный» кусок.
Что же делать?
- создать пул
- сбрасывать состояние куска
- складывать в пул отработанные куски
- брать новые куски из пула
Создать пул
import(
"sync"
)
var bytesPool = sync.Pool{
New: func() interface{} { return []byte{} },
}
/*
В данном примере функция `New` не нужна. Если пул пуст,
и `New` не `nil` - то она будет использована для создания нового
объекта. Его нужно будет преобразовать из `interace{}` - привести
к нужному типу. Смотри ниже - про это есть децл.
*/
Сбросить состояние
// пусть ary у нас []byte определённой длины и ёмкости
ary = ary[:0]
// усекаем len, сохраняем cap
Положить в пул
/*
так или иначе у нас могут оказаться слишком большие куски,
которые в принципе нам не понадобятся (во всяком случае
не часто) - выбросим их; иначе: кусок размером 2048 байт
будет использоваться там где нужно всего 500-800 байт,
при большом количестве это негативно отразится на памяти
- а ведь мы с этим и боремся
*/
const maxCap = 1024
if cap(ary) <= maxCap {
// кладём в пул куски ограниченного размера
bytesPool.Put(ary)
}
Взять из пула
nextAry := bytesPool.Get().([]byte)
Пояснение про New
Функция New
создаёт пустой []byte{}
, да ещё и эти преобразования в interface{}
и обратно. В случае с []byte
мы скорее всего будем наращивать его с помощью append
, что в принципе делает такой подход не выгодным:
- создание
[]byte
нулевой ёмкости - двойное преобразование в
interface{}
и обратно append
всё равно создаст новый кусокappend
можно скормитьnil
, только типа []byte (а не interface{})
Гораздо удобней сделать две функции, которые бы занимались всей вознёй с пулом
// получить
func getBytes() (b []byte) {
ifc := bytesPool.Get()
if ifc != nil {
b = ifc.([]byte)
}
return
}
// положить
func putBytes(b []byte) {
if cap(b) <= maxCap {
b = b[:0] // сброс
bytesPool.Put(b)
}
}
Помните
sync.Pool
не панацея- пул горутино-безопасен
- пул не обязательно освободит данные при первом пробуждении GC, но он может освободить их в любой момент
- нет возможности определить и установить размер пула
- нет необходимости заботится о переполнении пула
- вовсе незачем городить пул везде где ни попади, он создавался как амортизатор при множественном совместном использовании некоторых общих объектов, даже не просто внутри пакета, а даже больше — другими пакетами
- вероятно у Вас есть или будут ситуации, когда необходимость/возможность помочь GC будет очевидной
- пул ограниченного размера делается с помощью канала с буфером
Хороший пример использования пула: пакет fmt. Со 109-ой по 150-ую строку.
Танцы с мьютексами в Go / Хабр
Перевод обучающей статьи разработчика из SendGrid о том, когда и зачем можно и нужно использовать «традиционные» методы синхронизации данных в Go.
Уровень чтения: средний (intermediate) — эта статья подразумевает, что вы знакомы с основами Go и моделью concurrency, и, как минимум, знакомы с подходами к синхронизации данных методами блокировок и каналов.
Заметка читателю: На этот пост меня вдохновил хороший друг. Когда я помог ему разобраться с некоторыми гонками в его коде и постарался научить его искусству синхронизации данных так хорошо, насколько только был способен, я понял, что эти советы могут быть полезны и другим. Так что, будь это унаследованная кодовая база, в которой определенные решения по дизайну уже были приняты до вас, или вы просто хотите лучше понимать традиционные примитивы синхронизации в Go — эта статья может быть для вас.
Когда я впервые начал работать с языком программирования Go, я моментально влюбился в слоган «Не общайтесь разделением памяти. Разделяйте память через общение.» (Don’t communicate by sharing memory; share memory by communicating.) Для меня это означало писать весь конкурентный (concurrent) код «правильным» путем, используя каналы всегда-всегда. Я считал, что используя потенциал каналов, я гарантированно избегаю проблем с конкурентностью, блокировками, дедлоками и т.д.
По мере того, как я прогрессировал в Go, учился писать код на Go более идиоматически и изучал лучшие практики, я регулярно натыкался на большие кодовые базы, где люди регулярно использовали примитивы sync/mutex, а также sync/atomic, и несколько других «низкоуровневых» примитивов синхронизации «старой школы». Мои первые мысли были — ну, они явно делают это неверно, и, очевидно, они не смотрели ни одного выступления Роба Пайка о плюсах реализации конкуретного кода с помощью каналов, в которых он часто рассказывает о дизайне, основанном на труде Тони Хоара Communicating Sequential Processes.
Но реальность была сурова. Go-сообщество цитировало этот слоган там и тут, но заглядывая во многие open source проекты, я видел, что мьютексы повсюду и их много. Я боролся с этой загадкой некоторое время, но, в итоге, я увидел свет в конце тоннеля, и настало время засучить рукава и отложить каналы в сторонку. Теперь, быстро перемотаем на 2015 год, в котором я пишу на Go уже около 2.5 лет, в течение которых у меня было прозрение или даже два касательно более традиционных примитивов синхронизации вроде блокировок мьютексами. Давайте, спросите меня сейчас, в 2015? Ей, @deckarep, ты всё еще пишешь конкурентные программы используя только лишь каналы? Я отвечу — нет, и вот почему.
Во-первых, давайте не забывать о важности прагматичности. Когда речь заходит о защите состояния объекта методом блокировок или каналов, давайте зададимся вопросом — «Какой же метод я должен использовать?». И, как оказалось, есть очень хороший пост, который замечательно отвечает на этот вопрос:
Используйте тот метод, который наиболее выразителен и/или прост в вашем случае.
Частая ошибка новичков в Go это переиспользовать каналы и горутины просто потому что это возможно, и/или потому что это весело. Не бойтесь использовать sync.Mutex, если он решает вашу проблему лучше всего. Go прагматичен в том, чтобы давать вам те инструменты решения задачи, которые подходят лучше, и не навязывает вам лишь один подход.
Обратите внимание на ключевые слова в этой цитате: выразителен, прост, переиспользовать, не бойтесь, прагматичен. Я могу честно признать некоторые озвученные тут вещи: я боялся, когда я впервые пробовал Go. Я был совсем новичок в языке, и мне необходимо было время, чтобы быстро делать выводы. Вы, наверняка, вынесете собственные выводы из упомянутой выше статьи, и из этой статьи, по мере того, как мы углубимся в принятые практики использования мьютексов и различных нюансов. Статья выше также неплохо описывает соображения касательно выбора между мьютексами и каналами.
Когда использовать Каналы: передача владения данными, распределение вычислений и передача асинхронных результатов.
Когда использовать Мьютексы: кэши, состояния.
В конце концов, каждое приложение разное, и может потребоваться немного экспериментов и ложных стартов. Указания выше лично мне помогают, но позвольте мне объяснить их чуть более подробно. Если вам нужно защитить доступ к простой структуре данных, такой как слайс, или map, или что-нибудь своё, и если интерфейс доступа к этой структуре данных прост и прямолинеен — начинайте с мьютекса. Это также помогает спрятать «грязные» подробности кода блокировки в вашем API. Конечные пользователи вашей структуры не должны заботиться о том, как она делает внутреннюю синхронизацию.
Если же ваша синхронизация на мьютексах начинает становиться громоздкой и вы начинаете плясать танцы с мьютексами, это самое время переключиться на другой подход. Ещё раз, примите, как данное, что мьютексы удобны для простых сценариев, чтобы защитить минимально разделяемые данные. Используйте их для того, для чего они и нужны, но уважайте их и не давайте им выйти из под контроля. Оглянитесь назад, посмотрите внимательно на логику вашей программы, и если вы сражаетесь с мьютексами, значит это повод переосмыслить ваш дизайн. Возможно переход на каналы гораздо лучше впишется в логику вашего приложения, или, ещё лучше, возможно вам и не нужно разделять состояние вообще.
Многопоточность не сложна — сложны блокировки.
Поймите, я не утверждаю, что мьютексы лучше каналов. Я всего лишь говорю, что вы должны быть знакомы с обеими методами синхронизации, и если видите, что ваше решение на каналах выглядит переусложнённым, знать, что у вас есть другие варианты. Примеры в этой статье служат цели помочь вам писать лучший, более поддерживаемый и надёжный код. Мы, как инженеры, должны быть сознательны в том, как мы подходим к работе с разделяемыми данными и состояниями гонок в мультипоточных приложениях. Go позволяет невероятно легко писать высокопроизводительный конкурентные и/или параллельные приложения, но подвохи есть, и мы должны уметь аккуратно их обходить, создавая правильный код. Давайте посмотрим на них подробнее:
Номер 1: Определяя структуру, в которой мьютекс должен защищать одно или больше значений, помещайте мьютекс выше тех полей, доступ к которым, он будет защищать. Вот пример этой идиомы в исходном коде Go. Имейте ввиду, что это всего лишь договорённость, и никак не влияет на логику кода.
var sum struct {
sync.Mutex // <-- этот мьютекс защищает
i int // <-- это поле под ним
}
Номер 2: держите блокировку не дольше, чем она на самом деле требуется. Пример — если возможно, не держите мьютекс во время IO-вызова. Наоборот, постарайтесь защищать ваши данные только минимально необходимое время. Если вы сделаете как-нибудь вот так в веб-обработчике, вы просто потеряете преимущества конкурентности, сериализовав доступ к обработчику:
// В коде ниже подразумевается, что `mu` существует только
// для защиты переменной cache
// NOTE: Простите за игнор ошибок, это для краткости примера
// Не делайте так, если это возможно
func doSomething(){
mu.Lock()
item := cache["myKey"]
http.Get() // какой-нибудь дорогой IO-вызов
mu.Unlock()
}
// Вместо этого, делайте как-нибудь так
func doSomething(){
mu.Lock()
item := cache["myKey"]
mu.Unlock()
http.Get() // Это может занять время, но нам ок
}
Номер 3: Используйте defer, чтобы разблокировать мьютекс там где у функции есть несколько точек выхода. Для вас это означает меньше ручного кода и может помочь избежать дедлоков, когда кто-то меняет код через 3 месяца и добавляет новую точку выхода, упустив из виду блокировку.
func doSomething() {
mu.Lock()
defer mu.Unlock()
err := ...
if err != nil {
//log error
return // <-- разблокировка произойдет здесь
}
err = ...
if err != nil {
//log error
return // <-- или тут
}
return // <-- и, конечно, тут тоже
}
При этом, постарайтесь не зависеть вслепую от defer во всех случаях подряд. К примеру, следующий код — это ловушка, в которую вы можете попасться, если вы думаете, что defer-ы выполняются не при выходе из функции, а при выходе из области видимости (scope):
func doSomething(){
for {
mu.Lock()
defer mu.Unlock()
// какой-нибудь интересный код
// <-- defer не будет выполнен тут, как кто-нибудь *может* подумать
}
// <-- он(и) будут исполнены тут, при выходе из функции
}
// И поэтому в коде выше будет дедлок!
Наконец, не забывайте, что defer можно вообще не использовать в простых случаях без множественных точек выхода. Отложенные выполнения (defer) имеют небольшие накладные расходы, хотя зачастую ими можно пренебречь. И рассматривайте это, как очень преждевременную и, зачастую, лишнюю оптимизацию.
Номер 4: точная (fine-grained) блокировка может давать лучшую производительность ценой более сложного кода для управления ею, в то время, как более грубая блокировка может быть менее производительна, но делать код проще. Но опять же, будьте прагматичны в оценках дизайна. Если вы видите, что «танцуете с мьютексами», то, скорее всего, это подходящий момент для рефакторинга и перехода на синхронизацию посредством каналов.
Номер 5: Как упоминалось выше, хорошей практикой является инкапсулировать используемый метод синхронизации. Пользователи вашего пакета не должны заботится, каким именно образом вы защищаете данные в вашем коде.
В примере ниже, представьте, что мы представляем метод get(), который будет выбирать код из кэша только если в нём есть хотя бы одно значение. И поскольку мы должны блокировать как обращение к содержимому, так и подсчет значений, этот код приведет к дедлоку:
package main
import (
"fmt"
"sync"
)
type DataStore struct {
sync.Mutex // ← этот мьютекс охраняет кэш ниже
cache map[string]string
}
func New() *DataStore {
return &DataStore{
cache: make(map[string]string),
}
}
func (ds *DataStore) set(key string, value string) {
ds.Lock()
defer ds.Unlock()
ds.cache[key] = value
}
func (ds *DataStore) get(key string) string {
ds.Lock()
defer ds.Unlock()
if ds.count() > 0 { // <-- count() тоже блокируется!
item := ds.cache[key]
return item
}
return ""
}
func (ds *DataStore) count() int {
ds.Lock()
defer ds.Unlock()
return len(ds.cache)
}
func main() {
/* Выполнение кода ниже приведет к дедлоку, так как метод get() заблокируется и метод count() также заблокируется перед тем как get() разблокирует мьютекс
*/
store := New()
store.set("Go", "Lang")
result := store.get("Go")
fmt.Println(result)
}
Поскольку мьютексы в Go нерекурсивны, предложенное решение может выглядеть так:
package main
import (
"fmt"
"sync"
)
type DataStore struct {
sync.Mutex // ← этот мьютекс защищает кэш ниже
cache map[string]string
}
func New() *DataStore {
return &DataStore{
cache: make(map[string]string),
}
}
func (ds *DataStore) set(key string, value string) {
ds.cache[key] = value
}
func (ds *DataStore) get(key string) string {
if ds.count() > 0 {
item := ds.cache[key]
return item
}
return ""
}
func (ds *DataStore) count() int {
return len(ds.cache)
}
func (ds *DataStore) Set(key string, value string) {
ds.Lock()
defer ds.Unlock()
ds.set(key, value)
}
func (ds *DataStore) Get(key string) string {
ds.Lock()
defer ds.Unlock()
return ds.get(key)
}
func (ds *DataStore) Count() int {
ds.Lock()
defer ds.Unlock()
return ds.count()
}
func main() {
store := New()
store.Set("Go", "Lang")
result := store.Get("Go")
fmt.Println(result)
}
Обратите внимание в этом коде, что для каждого не-экспортированного метода есть аналогичный экспортированный. Эти методы работают как публичный API, и заботятся о блокировках на этом уровне. Далее они вызывают неэкспортированные методы, которые вообще не заботятся о блокировках. Это гарантирует, что все вызовы ваших методов извне будут блокироваться лишь раз и лишены проблемы рекурсивной блокировки.
Номер 6: В примерах выше мы использовали простой sync.Mutex, который может только блокировать и разблокировать. sync.Mutex предоставляет одинаковые гарантии, вне зависимости от того читает ли горутина данные или пишет. Но существует также sync.RWMutex, который даёт более точную семантику блокировок для кода, который только обращается к данным. Когда же использовать RWMutex вместо стандартного Mutex?
Ответ: используйте RWMutex, когда вы абсолютно уверены, что код в вашей критической секции не изменяет охраняемые данные.
// я могу смело использовать RLock() для счетчика, так как он не меняет данные
func count() {
rw.RLock() // <-- заметьте букву R в RLock (read-lock)
defer rw.RUnlock() // <-- заметьте букву R в RUnlock()
return len(sharedState)
}
// Но я должен использовать Lock() для set(), который меняет данные
func set(key string, value string) {
rw.Lock() // <-- заметьте, тут мы берем "обычный" Lock (write-lock)
defer rw.Unlock() // <-- а тут Unlock(), без R
sharedState[key] = value // <-- изменяет состояние(данные)
}
В коде выше мы подразумеваем, что переменная `sharedState` — это некий объект, возможно map, в котором мы можем считать его длинну. Поскольку функция count() гарантирует, что наш объект не изменяется, то мы можем смело вызывать её параллельно из любого количества ридеров (горутин). В некоторых сценариях это может уменьшить количество горутин в состоянии блокировки и потенциально дать прирост производительности в сценарии, где происходит много read-only обращений к данным. Но помните, если у вас есть код, меняющий данные, как в set(), вы обязаны использовать rw.Lock() вместо rw.RLock().
Номер 7: познакомьтесь с адски крутым и встроенным race-детектором в Go. Этот детектор заработал себе репутацию, найдя состояния гонки даже в стандартной библиотеке Go в своё время. Именно поэтому он встроен в инструментарий Go и есть немало выступлений и статей о нём, которые расскажут про него лучше, чем я.
- если вы ещё не запускаете свои unit/integration тесты с включенным рейс-детектором в вашем CI — настройте это прямо сейчас
- если ваши тесты не тестируют параллельный доступ к вашему API/коду — детектор вам сильно не поможет
- не запускайте программу с race-детектором в продакшене, там есть накладные расходы, которые уменьшают производительность
- если race-детектор нашел состояние гонки — это реальная гонка
- состояния гонки могут быть и при синхронизации через каналы, если вы неосторожны
- никакие блокировки в мире вас не спасут, если горутины читают или пишут разделяемые данные вне пределов критической секции
- если авторы Go могут иногда писать код, в котором есть гонки, то вы тоже можете
Я надеюсь, эта статья даёт достаточно ёмкое представление о том, как и когда использовать мьютексы в Go. Пожалуйста, экспериментируйте с низкоуровневыми примитивами синхронизации в Go, делайте ошибки, научитесь на них, цените и понимайте инструментарий. И прежде всего, будьте прагматичны в вашем коде, используйте правильные инструменты для каждого конкретного случая. Не бойтесь, как боялся я вначале. Если бы я всегда слушал все негативные вещи, которые говорят про блокировки, я бы сейчас не был в этом бизнесе, создавая крутейшие распределённые системы используя такие крутые технологии, как Go.
Примечание: я люблю обратную связь, так что если вы находите этот материал полезным, пинганите меня, твитните или дайте мне конструктивный отзыв.
Спасибо и хорошего кодинга!
Go: понять дизайн Sync.Pool
Пакет sync
предоставляет мощный пул экземпляров, которые могут быть повторно использованы для того, чтобы уменьшить давление на сборщик мусора. Перед использованием пакета очень важно провести тестирование приложения до и после использования пула, поскольку это может снизить производительность, если вы плохо понимаете, как оно работает внутри.
Эта статья основана на Go 1.12 и 1.13 и объясняет эволюцию sync / pool.go между этими двумя версиями.
Ограничение pool
Давайте возьмем простой пример, чтобы увидеть, как это работает в довольно простом контексте с выделениями 1k:
type Small struct { a int } var pool = sync.Pool{ New: func() interface{} { return new(Small) }, } //go:noinline func inc(s *Small) { s.a++ } func BenchmarkWithoutPool(b *testing.B) { var s *Small for i := 0; iВот два теста, один из которых не использует
sync.Pool
а другой его использует:name time/op alloc/op allocs/op WithoutPool-8 3.02ms ± 1% 160kB ± 0% 1.05kB ± 1% WithPool-8 1.36ms ± 6% 1.05kB ± 0% 3.00 ± 0%Поскольку цикл имеет 10k итераций, бенчмарк, который не использует пул, выделил 10k в куче против только 3 для бенчмарка с пулом. 3 распределения выполняются пулом, но выделен только один экземпляр структуры. Пока все хорошо; с помощью
sync.Pool
работает намного быстрее и потребляет меньше памяти.Но в реальном приложении ваш экземпляр, вероятно, будет использоваться для тяжелых вещей и делать много новых распределений головы. В этом случае, когда память увеличится, будет запущен сборщик мусора. Мы также можем принудить сборщик мусора в наших тестах с помощью команды
runtime.GC()
для имитации этого поведения:name time/op alloc/op allocs/op WithoutPool-8 993ms ± 1% 249kB ± 2% 10.9k ± 0% WithPool-8 1.03s ± 4% 10.6MB ± 0% 31.0k ± 0%Теперь мы видим, что производительность пула ниже, а количество выделенных ресурсов и используемой памяти намного выше. Давайте посмотрим глубже в пакете, чтобы понять, почему.
Внутренний рабочий процесс
Покопавшись в sync/pool.go я нашел инициализацию пакета, который мог бы ответить на нашу вопрос:
func init() { runtime_registerPoolCleanup(poolCleanup) }Он регистрируется во время выполнения как метод очистки пулов. И этот же метод будет вызван сборщиком мусора в своем выделенном файле
runtime/mgc.go
:func gcStart(trigger gcTrigger) { [...] // clearpools before we start the GC clearpools()Это объясняет, почему при вызове сборщика мусора производительность была ниже. Пулы очищаются каждый раз, когда запускается сборщик мусора. Документация также предупреждает нас об этом:
Любой элемент, хранящийся в пуле, может быть удален автоматически в любое время без уведомления
Теперь давайте создадим рабочий процесс, чтобы понять, как управляются элементы:
sync.Pool рабочий процесс в Go 1.12
Для каждой
sync.Pool
мы создаем, go генерирует внутренний пул, прикрепленныйpoolLocal
к каждому процессору. Этот внутренний пул состоит из двух атрибутов:private
иshared
. Первый доступен только его владельцу (push и pop - и, следовательно, не нуждается в блокировке), а атрибутshared
может быть прочитан любым другим процессором и должен быть безопасным для параллелизма. Действительно, пул - это не простой локальный кеш, он может использоваться любым потоком / программами в нашем приложении.Версия 1.13 Go улучшит доступ к общим элементам, а также предоставит новый кеш, который должен решить проблему, связанную с сборщиком мусора и очисткой пулов.
Новый пул без блокировки и кэш victim
Go версии 1.13 содержит новый двусвязный список в виде общего пула, который снимает блокировку и улучшает общий доступ. Это основа для улучшения кеша. Вот новый рабочий процесс общего доступа:
новые общие пулы в Go 1.13
С этим новым цепочечным пулом каждый процессор с push и pop во главе своей очереди, в то время как общий доступ выскочит из хвоста. Глава очереди может расти, выделяя новую структуру в два раза больше, которая будет связана с предыдущей благодаря атрибутам
next
/prev
. Размер по умолчанию для исходной структуры составляет 8 элементов. Это означает, что вторая структура будет содержать 16 элементов, третья - 32 и так далее.
Кроме того, блокировка теперь не нужна, и код может полагаться на атомарные операции.Что касается нового кэша, новая стратегия довольно проста. Теперь есть 2 набора пулов: активные и архивные. Когда сборщик мусора запускается, он сохраняет ссылку каждого пула на новый атрибут в этом пуле, а затем копирует набор пулов в архивированные, прежде чем очищать текущие пулы:
// Удалите кэши victim из всех пулов. for _, p := range oldPools { p.victim = nil p.victimSize = 0 } // Переместить основной кэш в кэш-память victim. for _, p := range allPools { p.victim = p.local p.victimSize = p.localSize p.local = nil p.localSize = 0 } // Кэши victim и без пулов имеют первичные кэши. oldPools, allPools = allPools, nilС этой стратегией у приложения теперь будет еще один цикл сбора мусора для создания / сбора новых элементов с резервной копией благодаря кэшу victim. В рабочем процессе кэш-память victim будет запрошена в конце процесса, после общих пулов.
Go, практика асинхронного взаимодействия / Хабр
Немножко про каналы, про выполнение в основном процессе, про то как вынести блокирующие операции в отдельную горутину.
- Каналы и пустое значение
- Односторонние каналы
- Выполнение в основном треде ОС
- Вынос блокирующих операций
Каналы и пустое значение
Каналы — это инструмент для асинхронной разработки. Но зачастую не важно что переслать по каналу — важен лишь факт пересылки. Порой встречается
done := make(chan bool)
/// […]
done <- true
Размер bool зависит от платформы, да, обычно, это не тот случай, когда следует беспокоиться о размере. Но всё же существует способ ничего не отправлять, а если точнее — то отправлять ничего (если быть ещё точнее, то речь о пустой структуре).
done := make(chan struct{})
// […]
done <- struct{}{}
Вот собственно и всё.
Односторонние каналы
Есть ещё один момент, который хотелось бы явно осветить. Пример:
func main() {
done := make(chan struct{})
go func() {
// stuff
done <- struct{}{} // перед завершением сообщаем об этом
}()
<- done // ожидание завершения горутины
}
Всё просто — done в горутине нужен только для записи. В принципе, в горутине его можно и прочитать (получить значение из канала done). Во избежании неприятностей, если код путаный, выручают параметры. Параметры функции, что передаётся горутине. Теперь так
func main() {
done := make(chan struct{})
go func(done chan<- struct{}) {
// stuff
done <- struct{}{} // перед завершением сообщаем об этом
} (done)
<- done // ожидание завершения горутины
}
Теперь, при передаче канала так, он будет преобразован в канал только для записи. Но вот внизу, канал по прежнему останется двунаправленным. В принципе, канал можно преобразовать в односторонний и не передавая его аргументом:
done := make(chan struct{})
writingChan := (chan<- struct{})(done) // первые скобки не важны
readingChan := (<-chan struct{})(done) // первые скобки обязательны
При частой необходимости, можно сделать функцию, которая будет всем этим заниматься. Вот пример на play.golang.org. Всё это позволяет отловить некоторые ошибки на этапе компиляции.
Выполнение в основном треде ОС
Например такие библиотеки как — OpenGL, libSDL, Cocoa — используют локальные для процесса структуры данных (thread local storage). Это значит, что они должны выполняться в основном треде ОС (main OS thread), иначе — ошибка. Функция runtime.LockOSThread()
позволяет приморозить текущую горутину к текущему треду (thread) ОС. Если вызвать её при инициализации (в функции init
), то это и будет основной тред ОС (main OS thread). При этом другие горутины спокойно могут выполняться в параллельных тредах ОС.
Для того, чтобы вынести вычисления в отдельный тред (в данном случае речь о горутине, не факт что она будет в отдельном треде ОС) достаточно просто пересылать функции в основной. Вот и всё.
ПростыняНа play.golang.org
package main
import (
«fmt»
«runtime»
)
func init() {
runtime.LockOSThread() // примораживаем текущую горутину к текущему треду
}
func main() {
/*
коммуникации
*/
done := make(chan struct{}) // <- остановка и выход
stuff := make(chan func()) // <- отправка функций в основной тред
/*
создадим второй тред (в данном случае — вторую горутину, но это не важно)
и начнём отправлять «работу» в первый
*/
go func(done chan<- struct{}, stuff chan<- func()) { // параллельная работа
stuff <- func() { // первый пошёл
fmt.Println(«1»)
}
stuff <- func() { // второй пошёл
fmt.Println(«2»)
}
stuff <- func() { // третий пошёл
fmt.Println(«3»)
}
done <- struct{}{}
}(done, stuff)
Loop:
for {
select {
case do := <-stuff: // получение «работы»
do() // и выполнение
case <-done:
break Loop
}
}
}
Вынос блокирующих операций
Куда чаще встречаются блокирующие IO-операции, но они побеждаются аналогично.
ПростыняНа play.golang.org
package main
import «os»
func main() {
/*
коммуникации
*/
stop := make(chan struct{}) // нужен для остановки «пишущей» горутины
done := make(chan struct{}) // ожидание её завершения
write := make(chan []byte) // данные для записи
/*
параллельный поток для IO-операций
*/
go func(write <-chan []byte, stop <-chan struct{}, done chan<- struct{}) {
Loop:
for {
select {
case msg := <-write: // получения сообщения для записи
os.Stdout.Write(msg) // асинхронная запись
case <-stop:
break Loop
}
}
done <- struct{}{}
}(write, stop, done)
write <- []byte(«Hello «) // отправка сообщений
write <- []byte(«World!\n») // на запись
stop <- struct{}{} // остановка
<-done // ожидание завершения
}
Если несколько горутин будут отправлять свои сообщения к одной «пишущей», то они всё равно будут блокироваться. В этом случае выручит канал с буфером. Учитывая, что slice — это референсный тип, по каналу будет пересылаться только указатель.
Референс
- Разъяснение LockOSThread (англ.)
- Пустые структуры на blog.golang.org (англ.)
- Ещё про пустые структуры (англ.)
Синглтон паттерн на Go | 4gophers.ru
Синглтон паттерн на Go
Это перевод статьи «Singleton Pattern in Go»
В последние несколько лет популярность Go растет с невероятной скоростью, каким бы удивительным это не было. Go привлекает разработчиков из разных областей ИТ и с различным опытом разработки. Появляется много статей о том, как компании переходят с Ruby на Go, окунаются в мир параллельного/конкурентного подхода для решения задач.
Последние 10 лет Ruby on Rails позволял разработчикам и стартаперам писать довольно мощные системы, как правило, без необходимости вникать в то, как все устроено внутри или беспокоиться о конкурентности и потоко-безопсности кода. Для RoR приложения создание потоков и параллельная работа — вещи довольно редкие. На хостингах и внутри фреймворков используется другой подход, через запуск параллельных процессов. Параллельные серверы, такие как Puma, стали набирать популярность только последние несколько лет, но они тянут за собой много различных проблем, связанных третьесторонними гемами и кодом, не предназначенным для многопоточности.
Теперь, когда много новых разработчиков запрыгнули в лодку языка Go, нам приходится более внимательно и пристально относится к коду, продумывать его поведение и разрабатывать с учетом потоко-безопасности.
Распространенная ошибка
Недавно я начал все чаще и чаще замечать эту ошибку в различных репозиториях c Go проектами. Синглтон, реализация которого абсолютна небезопасна для использования в конкурентных приложениях. Приведу пример, о чем я говорю:
package singleton
type singleton struct {
}
var instance *singleton
func GetInstance() *singleton {
if instance == nil {
instance = &singleton{} // Это НЕ потоко-безопасно
}
return instance
}
При таком сценарии несколько go-рутин могут пройти проверку в if
и они все могут создать свои экземпляры singleton
, которые затрут один другого. Нет гарантий, какой из экземпляров вернет этот метод, а выполнение операций над этим экземпляром может привести к неожиданным последствиям.
Это очень плохо, потому что синглтон подразумевает использование одного экземпляра в разных участках кода, а в нашем случае это будут разные объекты в разных состояниях и с разным поведением. Это может стать настоящим адом во время дебага — ошибку заметить очень трудно, так как паузы рантайма не такие большие, как в продакшене и код ведет себя почти как нужно, что скрывает ошибку от разработчика.
Агрессивная блокировка
Я часто замечал как неправильно пытаются решить эту проблему. Конечно, подобный способ решает проблему потоко-безопасности, но тянет за собой другую не менее опасную ошибку. Он заключается в использовании агрессивной блокировки ресурса при вызове метода.
var mu Sync.Mutex
func GetInstance() *singleton {
mu.Lock() // Неоправданная блокировка, в случае
// когда экземпляр уже создан
defer mu.Unlock()
if instance == nil {
instance = &singleton{}
}
return instance
}
Код, приведенный выше, использует sync.Mutex
для решения проблемы вызова метода в разных потока и приобретает блокировку до создания инстанса. Проблема в том, что мы будем приобретать лишнюю блокировку когда экземпляр уже создан и нужно просто вернуть его. Если будет много конкурентного выполнения кода, то в этом месте у нас будет «бутылочное горлышко», так как в один момент времени только одна go-рутина сможет получить доступ к экземпляру объекта.
Таким образом, это не самое лучшее решение. Нам стоит использовать другие подходы.
Блокировка с двойной проверкой
В C++ (и многих других языках) один из самых лучших способов обеспечения минимальной потоко-безопасной блокировки, это использования паттерна «блокировка с двойной проверкой»(он же «double checked locking» и «check-lock-check»). Ниже приведена реализация этого паттерна на псевдокоде:
if check() {
lock() {
if check() {
// perform your lock-safe code here
}
}
}
Основная идея в том, что мы избегаем агрессивной блокировки благодаря проверки перед этим. Естественно что операция if
дешевле блокировки. К тому же, мы можем подождать и получить индивидуальную блокировку. Таким образом, внутри блока код будет исполнятся единоразово в один момент времени. Но между первой проверкой и взятием блокировки может успеть отработать другой поток, который получил блокировку, поэтому нам нужно выполнить вторую проверку уже после блокировки, чтобы избежать перетирания экземпляров объекта.
На протяжении многих лет, люди работающие со мной знают, что я очень строг, по поводу использования этого паттерна, с моими командами во время ревью.
Если мы применим этот паттерн в нашем методе GetInstance()
, в результате будет что то такое:
func GetInstance() *singleton {
if instance == nil { // Все еще не идеально.
// Тут нет полной атомарности
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &singleton{}
}
}
return instance
}
Так уже лучше, но все еще не идеально. Из-за оптимизации при компиляции нет никакой уверенности, что проверка экземпляра бут выполнятся атомарно. Тем не менее, мы двигаемся в нужном направлении.
Мы можем улучшить этот способ, используя пакет sync/atomic
, с помощью которого можно будет устанавливать и проверять флаги, которые будут указывать инициализирован наш экземпляр или нет.
import "sync"
import "sync/atomic"
var initialized uint32
// ...
func GetInstance() *singleton {
if atomic.LoadUInt32(&initialized) == 1 {
return instance
}
mu.Lock()
defer mu.Unlock()
if initialized == 0 {
instance = &singleton{}
atomic.StoreUint32(&initialized, 1)
}
return instance
}
Но… Мне кажется, нам стоит присмотреться к реализации синхронизации go-рутин в стандартной библиотеке.
Идиоматически правильный синглтон в Go
Мы хотим реализовать синглтон паттерн идиоматическим для Go способом. Для этого у нас есть прекрасный пакет sync
. В этом пакете есть тип Once
. Этот тип позволяет выполнять действие только один раз. Ниже приведен код из стандартной библиотеки Go.
// Once это объект, которые позволяет выполнять некоторое
// действие только один раз
type Once struct {
m Mutex
done uint32
}
// Do вызывает функцию f только в том случае, если это первый вызов Do для
// этого экземпляра Once. Другими словами, если у нас есть var once Once и
// once.Do(f) будет вызываться несколько раз, f выполнится только в
// момент первого вызова, даже если f будет иметь каждый раз другое значение.
// Для вызова нескольких функций таким способом нужно несколько
// экземпляров Once.
//
// Do предназначен для инициализации, которая должна выполняться единожды
// Так как f ничего не возвращает, может быть необходимым использовать
// замыкание для передачи параметров в функцию, выполняемую Do:
// config.once.Do(func() { config.init(filename) })
//
// Поскольку ни один вызов к Do не завершится пока не произойдет
// первый вызов f, то f может заблокировать последующие вызовы
// Do и получится дедлок.
//
// Если f паникует, то Do считает это обычным вызовом и, при последующих
// вызовах, Do не будет вызывать f.
//
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 { // Check
return
}
// Медленный путь.
o.m.Lock() // Lock
defer o.m.Unlock()
if o.done == 0 { // Check
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
А это означает, что мы можем, со спокойной душой, использовать пакет sync
для вызова метода единоразово. Использовать метод Do
можем вот так:
once.Do(func() {
// в этом месте можно безопасно инициализировать экземпляр
})
Ниже вы можете видеть полный код реализации паттерна синглтон, в которой используется тип sync.Once
для синхронизации доступа в GetInstance()
и обеспечивается единоразовое создание нужного экземпляра.
package singleton
import (
"sync"
)
type singleton struct {
}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
Как видите, использование sync.Once
это самый элегантный путь для реализации потоко-безопасности. Аналогично в Objective-C и Swift (Cocoa) реализован метод dispatch_once
для выполнения подобных инициализаций.
Заключение
Когда приходится работать с распределенностью и параллельностью, то необходимо совсем по другому смотреть на код. Старайтесь всегда проводить ревью кода чтобы быть в курсе всей кодовой базы.
Все новоприбывшие Go разработчики должны, прежде всего, разобраться как нужно писать потоко-безопасный код. Несмотря на то, что Go позволяет проектировать многопоточный код без значительный усилий, все же остается достаточно много моментов, в которых нужно подумать и найти лучшее решение.
sync — язык программирования Go
Обзор ▹
Обзор ▾
Пакетная синхронизация предоставляет базовые примитивы синхронизации, такие как взаимная
исключения блокировки. За исключением типов Once и WaitGroup, большинство из них предназначены
для использования низкоуровневыми библиотечными подпрограммами. Синхронизация более высокого уровня
лучше делать через каналы и коммуникации.
Значения, содержащие типы, определенные в этом пакете, не должны копироваться.
Cond реализует переменную условия, точку рандеву
для горутин, ожидающих или объявляющих о возникновении
события.
Каждому Cond соответствует Locker L (часто * Mutex или * RWMutex),
которые необходимо соблюдать при изменении состояния и
при вызове метода Wait.
Cond нельзя копировать после первого использования.
type Cond struct { L шкафчик }
func NewCond
¶
func NewCond (l Locker) * Cond
NewCond возвращает новый Cond с Locker l.
func (* Cond) Broadcast
¶
func (c * Cond) Broadcast ()
Broadcast будит все горутины, ожидающие c.
Позволяет, но не обязательно, чтобы вызывающий абонент удерживал c.L
во время звонка.
func (* Cond) Сигнал
¶
func (c * Cond) Сигнал ()
Сигнал будит одну горутину, ожидающую на c, если таковая имеется.
Позволяет, но не обязательно, чтобы вызывающий абонент удерживал c.L
во время звонка.
func (* Cond) Подождите
¶
func (c * Cond) Wait ()
Ожидание атомарно разблокирует c.L и приостанавливает выполнение
вызывающей горутины. После возобновления выполнения позже,
Подождите, замки c.L, прежде чем вернуться. В отличие от других систем,
Ожидание не может вернуться, если его не разбудит трансляция или сигнал.
Поскольку c.L не заблокирован при первом возобновлении ожидания, вызывающий
обычно не может предполагать, что условие истинно, когда
Подождите, вернется.Вместо этого вызывающий должен ждать в цикле:
c.L. Замок () for! condition () { c.Wait () } ... использовать условие ... c.L.Unlock ()
Шкафчик представляет собой объект, который можно заблокировать и разблокировать.
Интерфейс шкафчика типа
{ Замок() Разблокировать () }
Карта похожа на карту Go [интерфейс {}] интерфейс {}, но безопасна для одновременного использования.
несколькими горутинами без дополнительной блокировки или координации.Загрузка, сохранение и удаление выполняются в амортизированном постоянном времени.
Тип карты специализированный. В большинстве случаев вместо этого следует использовать простую карту Go,
с отдельной блокировкой или согласованием, для большей безопасности типа и чтобы
легче поддерживать другие инварианты вместе с содержимым карты.
Тип карты оптимизирован для двух распространенных случаев использования: (1) когда запись для данного
ключ всегда записывается только один раз, но читается много раз, как в кешах, которые только растут,
или (2) когда несколько горутин читают, записывают и перезаписывают записи для непересекающихся
наборы ключей.В этих двух случаях использование карты может значительно уменьшить блокировку.
конкуренция по сравнению с картой Go в паре с отдельным Mutex или RWMutex.
Нулевая карта пуста и готова к использованию. Карту нельзя копировать после первого использования.
type Map struct { }
func (* Карта) Удалить
¶1.9
func (m * Map) Удалить (клавиша интерфейса {})
Удалить удаляет значение ключа.
func (* Карта) Загрузить
¶1.9
func (m * Map) Load (key interface {}) (value interface {}, ok bool)
Load возвращает значение, хранящееся в карте для ключа, или ноль, если нет
значение присутствует.
Результат ok указывает, было ли найдено значение на карте.
func (* Карта) LoadAndDelete
¶1,15
func (m * Map) LoadAndDelete (key interface {}) (value interface {}, loaded bool)
LoadAndDelete удаляет значение ключа, возвращая предыдущее значение, если оно есть.Загруженный результат сообщает, присутствовал ли ключ.
func (* Карта) LoadOrStore
¶1.9
func (m * Map) LoadOrStore (ключ, интерфейс значения {}) (фактический интерфейс {}, загруженный bool)
LoadOrStore возвращает существующее значение для ключа, если он присутствует.
В противном случае он сохраняет и возвращает заданное значение.
Загруженный результат истинен, если значение было загружено, и ложно, если сохранено.
func (* Карта) Диапазон
¶1.9
func (m * Map) Диапазон (f func (key, valu
.
sync — язык программирования Go
Обзор ▹
Обзор ▾
Пакетная синхронизация предоставляет базовые примитивы синхронизации, такие как взаимная
исключения блокировки. За исключением типов Once и WaitGroup, большинство из них предназначены
для использования низкоуровневыми библиотечными подпрограммами. Синхронизация более высокого уровня
лучше делать через каналы и коммуникации.
Значения, содержащие типы, определенные в этом пакете, не должны копироваться.
Cond реализует переменную условия, точку рандеву
для горутин, ожидающих или объявляющих о возникновении
события.
Каждому Cond соответствует Locker L (часто * Mutex или * RWMutex),
которые необходимо соблюдать при изменении состояния и
при вызове метода Wait.
Cond нельзя копировать после первого использования.
type Cond struct { L шкафчик }
func NewCond
¶
func NewCond (l Locker) * Cond
NewCond возвращает новый Cond с Locker l.
func (* Cond) Broadcast
¶
func (c * Cond) Broadcast ()
Broadcast будит все горутины, ожидающие c.
Позволяет, но не обязательно, чтобы вызывающий абонент удерживал c.L
во время звонка.
func (* Cond) Сигнал
¶
func (c * Cond) Сигнал ()
Сигнал будит одну горутину, ожидающую на c, если таковая имеется.
Позволяет, но не обязательно, чтобы вызывающий абонент удерживал c.L
во время звонка.
func (* Cond) Подождите
¶
func (c * Cond) Wait ()
Ожидание атомарно разблокирует c.L и приостанавливает выполнение
вызывающей горутины. После возобновления выполнения позже,
Подождите, замки c.L, прежде чем вернуться. В отличие от других систем,
Ожидание не может вернуться, если его не разбудит трансляция или сигнал.
Поскольку c.L не заблокирован при первом возобновлении ожидания, вызывающий
обычно не может предполагать, что условие истинно, когда
Подождите, вернется.Вместо этого вызывающий должен ждать в цикле:
c.L. Замок () for! condition () { c.Wait () } ... использовать условие ... c.L.Unlock ()
Шкафчик представляет собой объект, который можно заблокировать и разблокировать.
Интерфейс шкафчика типа
{ Замок() Разблокировать () }
Карта похожа на карту Go [интерфейс {}] интерфейс {}, но безопасна для одновременного использования.
несколькими горутинами без дополнительной блокировки или координации.Загрузка, сохранение и удаление выполняются в амортизированном постоянном времени.
Тип карты специализированный. В большинстве случаев вместо этого следует использовать простую карту Go,
с отдельной блокировкой или согласованием, для большей безопасности типа и чтобы
легче поддерживать другие инварианты вместе с содержимым карты.
Тип карты оптимизирован для двух распространенных случаев использования: (1) когда запись для данного
ключ всегда записывается только один раз, но читается много раз, как в кешах, которые только растут,
или (2) когда несколько горутин читают, записывают и перезаписывают записи для непересекающихся
наборы ключей.В этих двух случаях использование карты может значительно уменьшить блокировку.
конкуренция по сравнению с картой Go в паре с отдельным Mutex или RWMutex.
Нулевая карта пуста и готова к использованию. Карту нельзя копировать после первого использования.
type Map struct { }
func (* Карта) Удалить
¶1.9
func (m * Map) Удалить (клавиша интерфейса {})
Удалить удаляет значение ключа.
func (* Карта) Загрузить
¶1.9
func (m * Map) Load (key interface {}) (value interface {}, ok bool)
Load возвращает значение, хранящееся в карте для ключа, или ноль, если нет
значение присутствует.
Результат ok указывает, было ли найдено значение на карте.
func (* Карта) LoadAndDelete
¶1,15
func (m * Map) LoadAndDelete (key interface {}) (value interface {}, loaded bool)
LoadAndDelete удаляет значение ключа, возвращая предыдущее значение, если оно есть.Загруженный результат сообщает, присутствовал ли ключ.
func (* Карта) LoadOrStore
¶1.9
func (m * Map) LoadOrStore (ключ, интерфейс значения {}) (фактический интерфейс {}, загруженный bool)
LoadOrStore возвращает существующее значение для ключа, если он присутствует.
В противном случае он сохраняет и возвращает заданное значение.
Загруженный результат истинен, если значение было загружено, и ложно, если сохранено.
func (* Карта) Диапазон
¶1.9
func (m * Map) Диапазон (f func (key, value interface {}) bool)
Диапазон вызывает f последовательно для каждого ключа и значения, присутствующего на карте.
Если f возвращает false, диапазон останавливает итерацию.
Диапазон не обязательно соответствует какому-либо согласованному снимку карты
содержимое: ни один ключ не будет посещен более одного раза, но если значение для любого ключа
сохраняется или удаляется одновременно, Ran
.
go — Как правильно использовать sync.Cond?
Переполнение стека
- Около
Продукты
- Для команд
Переполнение стека
Общественные вопросы и ответыПереполнение стека для команд
Где разработчики и технологи делятся частными знаниями с коллегамиВакансии
Программирование и связанные с ним технические возможности карьерного ростаТалант
Нанимайте технических специалистов и создавайте свой бренд работодателяРеклама
Обратитесь к разработчикам и технологам со всего мира- О компании
.
Файл | байтов | Модифицированный | ||
---|---|---|---|---|
.. | ||||
атомная / | 4096 | 2020-09-09 20:19:05 +0000 UTC | ||
услов. Го | 2604 | 2020-09-09 20:19:05 +0000 UTC | ||
cond_test.go | 5190 | 2020-09-09 20:19:05 +0000 UTC | ||
example_pool_test.идти | 1025 | 2020-09-09 20:19:05 +0000 UTC | ||
example_test.go | 1165 | 2020-09-09 20:19:05 +0000 UTC | ||
export_test.go | 1297 | 2020-09-09 20:19:05 +0000 UTC | ||
map.go | 11773 | 2020-09-09 20:19:05 +0000 UTC | ||
map_bench_test.идти | 6456 | 2020-09-09 20:19:05 +0000 UTC | ||
map_reference_test.go | 3995 | 2020-09-09 20:19:05 +0000 UTC | ||
map_test.go | 4246 | 2020-09-09 20:19:05 +0000 UTC | ||
mutex.go | 7496 | 2020-09-09 20:19:05 +0000 UTC | ||
mutex_test.идти | 5833 | 2020-09-09 20:19:05 +0000 UTC | ||
однократно | 2301 | 2020-09-09 20:19:05 +0000 UTC | ||
once_test.go | 1125 | 2020-09-09 20:19:05 +0000 UTC | ||
pool.go | 8346 | 2020-09-09 20:19:05 +0000 UTC | ||
pool_test.go | 7429 | 2020-09-09 20:19:05 +0000 UTC | ||
poolqueue.идти | 9085 | 2020-09-09 20:19:05 +0000 UTC | ||
runtime.go | 2018 | 2020-09-09 20:19:05 +0000 UTC | ||
runtime2.go | 464 | 2020-09-09 20:19:05 +0000 UTC | ||
время выполнения2_lockrank.go | 545 | 2020-09-09 20:19:05 +0000 UTC | ||
runtime_sema_test.идти | 1375 | 2020-09-09 20:19:05 +0000 UTC | ||
rwmutex.go | 4522 | 2020-09-09 20:19:05 +0000 UTC | ||
rwmutex_test.go | 4501 | 2020-09-09 20:19:05 +0000 UTC | ||
waitgroup.go | 4520 | 2020-09-09 20:19:05 +0000 UTC | ||
waitgroup_test.идти | 5915 | 2020-09-09 20:19:05 +0000 UTC |
.