sync: Mutex, Once, atomic
Гонка данных: когда каналов недостаточно
Каналы — идеальны для передачи данных. Но иногда нескольким горутинам нужен общий доступ к одному значению. Без синхронизации возникает гонка данных (data race):
var counter int
func increment() {
counter++ // НЕ атомарно: read → add → write
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println(counter) // не 1000! гонка данных
}Проверить наличие гонок: go run -race main.go. Детектор гонок найдёт проблему и покажет стек вызовов.
sync.Mutex: взаимное исключение
Mutex гарантирует, что в критической секции находится только одна горутина:
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // всегда через defer!
c.value++
}
func (c *SafeCounter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
c := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Increment()
}()
}
wg.Wait()
fmt.Println(c.Get()) // всегда 1000
}defer mu.Unlock() — обязательная идиома. Без defer легко забыть Unlock при панике или раннем return.
Правила Mutex:
- Не копировать после первого использования (передавай по указателю)
- Не блокировать рекурсивно — дедлок (нет reentrant mutex в Go)
- Держать блокировку как можно меньше времени
sync.RWMutex: много читателей, один писатель
Когда чтений намного больше чем записей — RWMutex эффективнее:
type Cache struct {
mu sync.RWMutex
store map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // разделяемая блокировка — несколько горутин могут читать
defer c.mu.RUnlock()
val, ok := c.store[key]
return val, ok
}
func (c *Cache) Set(key, val string) {
c.mu.Lock() // эксклюзивная блокировка — только одна горутина
defer c.mu.Unlock()
c.store[key] = val
}Когда использовать RWMutex вместо Mutex:
- Читают намного чаще, чем пишут
- Операции чтения занимают заметное время
Если записей столько же сколько чтений — RWMutex не даёт выигрыша (и чуть медленнее из-за overhead).
sync.Once: гарантированная однократная инициализация
Once.Do(f) выполняет f ровно один раз, даже при конкурентных вызовах:
type DB struct {
once sync.Once
conn *sql.DB
}
func (d *DB) getConn() *sql.DB {
d.once.Do(func() {
var err error
d.conn, err = sql.Open("postgres", dsn)
if err != nil {
panic(err)
}
})
return d.conn
}Классический singleton:
var (
instance *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
instance = loadConfig() // выполнится один раз
})
return instance
}sync.Once безопаснее init() — инициализация откладывается до первого использования (lazy).
sync.Map: конкурентный map
Встроенный map не потокобезопасен. Для конкурентного доступа есть sync.Map:
var m sync.Map
// Запись
m.Store("key", "value")
// Чтение
val, ok := m.Load("key")
// Атомарное «загрузить или сохранить»
actual, loaded := m.LoadOrStore("key", "default")
// Удаление
m.Delete("key")
// Итерация
m.Range(func(k, v any) bool {
fmt.Println(k, v)
return true // false — прервать итерацию
})sync.Map оптимизирован для двух сценариев:
- Ключ пишется один раз, читается много раз
- Разные горутины работают с разными ключами (без пересечений)
Для других случаев обычный map + sync.Mutex часто быстрее.
sync/atomic: без блокировок
sync/atomic — атомарные операции на уровне CPU-инструкций. Быстрее Mutex для одиночных переменных:
import "sync/atomic"
var counter int64
// Атомарный инкремент
atomic.AddInt64(&counter, 1)
// Атомарное чтение (безопасно без mutex)
val := atomic.LoadInt64(&counter)
// Атомарная запись
atomic.StoreInt64(&counter, 0)
// Compare-and-swap
old := int64(5)
new := int64(10)
swapped := atomic.CompareAndSwapInt64(&counter, old, new)Пример: быстрый счётчик запросов:
type Server struct {
requests atomic.Int64 // Go 1.19+: типизированный атомик
}
func (s *Server) Handle() {
s.requests.Add(1)
// обработка...
}
func (s *Server) Stats() int64 {
return s.requests.Load()
}atomic.Int64, atomic.Bool, atomic.Pointer[T] — новые типизированные атомики из Go 1.19.
sync.Pool: переиспользование объектов
sync.Pool — пул объектов для снижения нагрузки на GC:
var bufPool = sync.Pool{
New: func() any {
return &bytes.Buffer{}
},
}
func process(data string) string {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
buf.WriteString(data)
buf.WriteString(" processed")
return buf.String()
}Важно: объекты в Pool могут быть удалены GC в любой момент. Не храни в пуле объекты, которые нельзя пересоздать. Используй для буферов, временных структур — не для соединений с БД.
Выбор инструмента
| Задача | Инструмент |
|---|---|
| Передача данных между горутинами | chan |
| Защита сложного состояния | sync.Mutex |
| Много чтений, мало записей | sync.RWMutex |
| Однократная инициализация | sync.Once |
| Конкурентный map | sync.Map |
| Счётчик, флаг (одна переменная) | sync/atomic |
| Переиспользование объектов | sync.Pool |
| Ожидание группы горутин | sync.WaitGroup |
| Отмена и дедлайны | context |