За исключением типов Once и WaitGroup, большинство из них предназначены для использования низкоуровневыми библиотечными процедурами. Синхронизацию более высокого уровня лучше выполнять через каналы и коммуникации.
Значения, содержащие типы, определенные в этом пакете, не должны копироваться.
Функции
func OnceFunc
func OnceFunc(f func()) func()
OnceFunc возвращает функцию, которая вызывает f только один раз. Возвращаемая функция может вызываться одновременно.
Если f вызывает панику, возвращаемая функция будет вызывать панику с тем же значением при каждом вызове.
func OnceValue
func OnceValue[T any](f func() T) func() T
OnceValue возвращает функцию, которая вызывает f только один раз и возвращает значение, возвращаемое f. Возвращаемая функция может вызываться одновременно.
Если f вызывает панику, возвращаемая функция будет вызывать панику с тем же значением при каждом вызове.
Пример
В этом примере OnceValue используется для выполнения «дорогостоящего» вычисления только один раз, даже при одновременном использовании.
package main
import (
"fmt"
"sync"
)
func main() {
once := sync.OnceValue(func() int {
sum := 0
for i := 0; i < 1000; i++ {
sum += i
}
fmt.Println("Computed once:", sum)
return sum
})
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
const want = 499500
got := once()
if got != want {
fmt.Println("want", want, "got", got)
}
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Output:
Computed once: 499500
func OnceValues
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
OnceValues возвращает функцию, которая вызывает f только один раз и возвращает значения, возвращаемые f. Возвращаемая функция может вызываться одновременно.
Если f вызывает панику, возвращаемая функция будет вызывать панику с тем же значением при каждом вызове.
Пример
В этом примере используется OnceValues для однократного чтения файла.
package main
import (
"fmt"
"os"
"sync"
)
func main() {
once := sync.OnceValues(func() ([]byte, error) {
fmt.Println("Reading file once")
return os.ReadFile("example_test.go")
})
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
data, err := once()
if err != nil {
fmt.Println("error:", err)
}
_ = data // Ignore the data for this example
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Типы
type Cond
type Cond struct {
// L удерживается во время наблюдения или изменения условия
L Locker
// содержит отфильтрованные или неэкспортируемые поля
}
Cond реализует переменную условия, точку встречи для goroutines, ожидающих или объявляющих о наступлении события.
Каждый Cond имеет связанный с ним Locker L (часто *Mutex или *RWMutex), который должен удерживаться при изменении условия и при вызове метода Cond.Wait.
Cond не должен копироваться после первого использования.
В терминологии модели памяти Go, Cond организует так, что вызов Cond.Broadcast или Cond.Signal «синхронизируется перед» любым вызовом Wait, который он разблокирует.
Для многих простых случаев использования пользователям будет удобнее использовать каналы, чем Cond (Broadcast соответствует закрытию канала, а Signal — отправке по каналу).
Для получения дополнительной информации о заменах sync.Cond см. серию статей Роберто Клаписа о расширенных моделях параллелизма, а также доклад Брайана Миллса о моделях параллелизма.
func NewCond
func NewCond(l Locker) *Cond
NewCond возвращает новый Cond с Locker l.
func (*Cond) Broadcast
func (c *Cond) Broadcast()
Broadcast пробуждает все goroutines, ожидающие c.
Вызывающему разрешается, но не требуется, удерживать c.L во время вызова.
func (*Cond) Signal
func (c *Cond) Signal()
Signal пробуждает одну goroutine, ожидающую c, если таковая имеется.
Вызывающему разрешается, но не требуется, удерживать c.L во время вызова.
Signal() не влияет на приоритет планирования goroutine; если другие goroutines пытаются заблокировать c.L, они могут быть пробуждены раньше «ожидающей» goroutine.
func (*Cond) Wait
func (c *Cond) Wait()
Wait атомарно разблокирует c.L и приостанавливает выполнение вызывающего goroutine. После возобновления выполнения Wait блокирует c.L перед возвратом. В отличие от других систем, Wait не может вернуться, если его не разбудит Cond.Broadcast или Cond.Signal.
Поскольку c.L не блокируется во время ожидания Wait, вызывающий обычно не может предполагать, что условие будет выполнено, когда Wait вернется. Вместо этого вызывающий должен ждать в цикле:
c.L.Lock()
for !condition() {
c.Wait()
}...
использовать условие ...
c.L.Unlock()
Подробное объяснение типа Cond
Что такое Cond
?
Cond
(от слова “condition” - условие) - это примитив синхронизации из пакета sync
, который позволяет горутинам ожидать или объявлять о наступлении некоторого события.
Проще говоря, Cond
нужен для того, чтобы:
- Одни горутины могли “уснуть” и ждать какого-то условия
- Другие горутины могли их “разбудить”, когда это условие выполнится
Основные методы:
Wait()
- блокирует горутину до получения уведомленияSignal()
- пробуждает одну случайную горутину из ожидающихBroadcast()
- пробуждает все ожидающие горутины
Зачем это нужно?
Cond
особенно полезен в ситуациях, где:
- Несколько горутин ожидают какого-то общего условия
- Состояние может измениться в любой момент
- Вы не хотите постоянно опрашивать условие в цикле (busy waiting)
Реальный пример: Ограниченная очередь
Допустим, у нас есть очередь с ограниченным размером, и мы хотим:
- Блокировать писателей, когда очередь полна
- Блокировать читателей, когда очередь пуста
package main
import (
"fmt"
"sync"
"time"
)
type BoundedQueue struct {
mu sync.Mutex
cond *sync.Cond
queue []int
size int
cap int
}
func NewBoundedQueue(capacity int) *BoundedQueue {
q := &BoundedQueue{
queue: make([]int, 0, capacity),
cap: capacity,
}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *BoundedQueue) Put(item int) {
q.mu.Lock()
defer q.mu.Unlock()
// Ждем, пока освободится место
for q.size == q.cap {
q.cond.Wait()
}
q.queue = append(q.queue, item)
q.size++
fmt.Printf("Добавлен элемент %d. Размер очереди: %d\n", item, q.size)
// Уведомляем ожидающих читателей
q.cond.Broadcast()
}
func (q *BoundedQueue) Get() int {
q.mu.Lock()
defer q.mu.Unlock()
// Ждем, пока появится элемент
for q.size == 0 {
q.cond.Wait()
}
item := q.queue[0]
q.queue = q.queue[1:]
q.size--
fmt.Printf("Извлечен элемент %d. Размер очереди: %d\n", item, q.size)
// Уведомляем ожидающих писателей
q.cond.Broadcast()
return item
}
func main() {
queue := NewBoundedQueue(3)
// Писатели
for i := 0; i < 5; i++ {
go func(val int) {
queue.Put(val)
}(i)
}
// Читатели
for i := 0; i < 5; i++ {
go func() {
time.Sleep(1 * time.Second)
queue.Get()
}()
}
time.Sleep(5 * time.Second)
}
Когда использовать Cond
вместо каналов?
Cond
полезен, когда:
- У вас сложное условие ожидания (не просто “есть данные”)
- Нужно уведомлять сразу несколько горутин
- Состояние может меняться часто и нужно минимизировать накладные расходы
Каналы лучше подходят для более простых случаев передачи данных между горутинами.
sync.Cond
- это инструмент для сложных сценариев синхронизации, где горутинам нужно ждать выполнения определенных условий. Он особенно полезен при реализации структур данных с ограничениями (как в нашем примере с очередью), пулов ресурсов или других сценариев, где состояние может меняться и нужно эффективно уведомлять ожидающие горутины.
type Locker
type Locker интерфейс {
Lock()
Unlock()
}
Locker
- это интерфейс из пакета sync
, который определяет базовые методы для блокировки:
Подробное объяснение типа Locker
Этот интерфейс реализуют:
sync.Mutex
- обычная мьютекс-блокировкаsync.RWMutex
- блокировка с возможностью множественного чтения- Любые другие типы, которые реализуют эти два метода
Зачем нужен Locker
?
- Унификация работы с разными типами блокировок - вы можете писать функции, которые работают с любым типом блокировки
- Абстракция - позволяет не зависеть от конкретной реализации блокировки
- Тестирование - можно создавать mock-объекты для тестирования
Пример 1: Использование с sync.Mutex
package main
import (
"fmt"
"sync"
"time"
)
func increment(counter *int, locker sync.Locker) {
locker.Lock()
defer locker.Unlock()
*counter++
fmt.Println(*counter)
}
func main() {
var counter int
var mu sync.Mutex
for i := 0; i < 5; i++ {
go increment(&counter, &mu)
}
time.Sleep(1 * time.Second)
}
Пример 2: Собственная реализация Locker
type DebugLocker struct {
mu sync.Mutex
}
func (d *DebugLocker) Lock() {
fmt.Println("Lock acquired")
d.mu.Lock()
}
func (d *DebugLocker) Unlock() {
fmt.Println("Lock released")
d.mu.Unlock()
}
func main() {
var counter int
locker := &DebugLocker{}
for i := 0; i < 5; i++ {
go increment(&counter, locker)
}
time.Sleep(1 * time.Second)
}
Реальный пример использования
Допустим, у нас есть кэш, который может использовать разные виды блокировок:
type Cache struct {
locker sync.Locker
data map[string]string
}
func NewCache(locker sync.Locker) *Cache {
return &Cache{
locker: locker,
data: make(map[string]string),
}
}
func (c *Cache) Set(key, value string) {
c.locker.Lock()
defer c.locker.Unlock()
c.data[key] = value
}
func (c *Cache) Get(key string) (string, bool) {
c.locker.Lock()
defer c.locker.Unlock()
val, ok := c.data[key]
return val, ok
}
func main() {
// Можно использовать обычный Mutex
cache1 := NewCache(&sync.Mutex{})
// Или RWMutex для оптимизации чтения
cache2 := NewCache(&sync.RWMutex{})
// Или даже нашу DebugLocker
cache3 := NewCache(&DebugLocker{})
}Locker представляет объект, который можно заблокировать и разблокировать.
Когда использовать Locker
?
- Когда ваша функция/метод должен работать с разными типами блокировок
- Когда вы хотите сделать код более гибким для тестирования
- Когда вы разрабатываете библиотеку и хотите оставить выбор блокировки пользователю
Locker
- это простой интерфейс, который позволяет абстрагироваться от конкретного типа блокировки. Он делает ваш код более гибким и переиспользуемым, особенно когда речь идет о конкурентных операциях.
type Map
type Map struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Map похож на Go map[any]any, но безопасен для одновременного использования несколькими goroutines без дополнительной блокировки или координации. Загрузка, хранение и удаление выполняются за амортизированное постоянное время.
Тип Map является специализированным. В большинстве случаев следует использовать обычный Go map с отдельной блокировкой или координацией, чтобы обеспечить лучшую типовую безопасность и упростить поддержание других инвариантов наряду с содержимым карты.
Тип Map оптимизирован для двух распространенных случаев использования:
- когда запись для данного ключа записывается только один раз, но читается много раз, как в кэшах, которые только растут, или
- когда несколько goroutines читают, записывают и перезаписывают записи для непересекающихся наборов ключей.
В этих двух случаях использование Map может значительно уменьшить конфликты блокировок по сравнению с Go map в паре с отдельным Mutex или RWMutex.
Карта с нулевым значением пуста и готова к использованию. Карту нельзя копировать после первого использования.
В терминологии модели памяти Go карта организует так, что операция записи «синхронизируется перед» любой операцией чтения, которая наблюдает эффект записи, где операции чтения и записи определяются следующим образом.
- Map.Load, Map.LoadAndDelete, Map.LoadOrStore, Map.Swap, Map.CompareAndSwap и Map.CompareAndDelete — операции чтения;
- Map.Delete, Map.LoadAndDelete, Map.Store и Map.Swap — операции записи;
- Map.LoadOrStore — операция записи, когда она возвращает загруженный набор, установленный в false;
- Map.CompareAndSwap — операция записи, когда она возвращает помененный набор, установленный в true; и
- Map.CompareAndDelete — операция записи, когда она возвращает удаленный набор, установленный в true.
func (*Map) Clear
func (m *Map) Clear()
Clear удаляет все записи, в результате чего карта становится пустой.
func (*Map) CompareAndDelete
func (m *Map) CompareAndDelete(key, old any) (deleted bool)
CompareAndDelete удаляет запись для ключа, если его значение равно old. Старое значение должно быть сопоставимого типа.
Если в карте нет текущего значения для ключа, CompareAndDelete возвращает false (даже если старое значение является значением интерфейса nil).
Подробное объяснение типа Map и функции CompareAndDelete
Что делает CompareAndDelete
?
Метод CompareAndDelete
выполняет атомарную операцию:
- Проверяет, соответствует ли текущее значение для указанного ключа
old
значению - Если значения совпадают - удаляет запись из мапы
- Возвращает
true
, если удаление произошло, иfalse
в противном случае
Это операция “compare-and-delete” (сравнить и удалить), аналогичная атомарным операциям CAS (Compare-And-Swap).
Как это работает?
deleted := m.CompareAndDelete(key, oldValue)
- Если в мапе
key
отсутствует → возвращаетfalse
- Если в мапе
key
есть, но значение ≠oldValue
→ возвращаетfalse
- Если в мапе
key
есть и значение ==oldValue
→ удаляет запись и возвращаетtrue
Пример использования
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Добавляем значения в мапу
m.Store("counter", 42)
m.Store("flag", true)
m.Store("name", "Alice")
// Попытка удалить с неправильным старым значением
deleted := m.CompareAndDelete("counter", 100)
fmt.Printf("Удаление counter=100: %v\n", deleted) // false
// Удаление с правильным значением
deleted = m.CompareAndDelete("counter", 42)
fmt.Printf("Удаление counter=42: %v\n", deleted) // true
// Проверяем, что counter удален
_, ok := m.Load("counter")
fmt.Printf("counter существует: %v\n", ok) // false
// Попытка удалить несуществующий ключ
deleted = m.CompareAndDelete("nonexistent", nil)
fmt.Printf("Удаление nonexistent: %v\n", deleted) // false
// Пример с nil значением
m.Store("nil-value", nil)
deleted = m.CompareAndDelete("nil-value", nil)
fmt.Printf("Удаление nil-value: %v\n", deleted) // true
}
Реальный кейс использования
Представим систему, где несколько горутин обновляют статус задач:
type TaskStatus string
const (
Pending TaskStatus = "pending"
Running TaskStatus = "running"
Completed TaskStatus = "completed"
)
func completeTask(m *sync.Map, taskID string) bool {
// Пытаемся перевести задачу из running в completed
return m.CompareAndDelete(taskID, Running)
}
func main() {
var tasks sync.Map
// Инициализируем задачи
tasks.Store("task1", Pending)
tasks.Store("task2", Running)
tasks.Store("task3", Running)
// Горутины пытаются завершить задачи
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if completeTask(&tasks, "task2") {
fmt.Println("Задача task2 завершена")
}
}()
}
wg.Wait()
// Проверяем оставшиеся задачи
tasks.Range(func(key, value interface{}) bool {
fmt.Printf("%s: %s\n", key, value)
return true
})
}
Особенности работы
- Безопасность для concurrent-использования: метод можно вызывать из нескольких горутин без дополнительной синхронизации
- Сравнение значений: сравнение происходит через
==
, поэтому для сложных типов нужно быть внимательным - nil значения: даже если
old
- nil, метод вернетfalse
если ключа нет в мапе
Когда использовать?
CompareAndDelete
полезен в сценариях:
- Удаление устаревших данных (когда значение соответствует ожидаемому)
- Реализация конечных автоматов (state machines)
- Оптимистичные блокировки (optimistic locking)
- Удаление элементов только при определенных условиях
Этот метод особенно полезен в конкурентных сценариях, где нужно атомарно проверить и удалить значение без явных блокировок.
func (*Map) CompareAndSwap
func (m *Map) CompareAndSwap(key, old, new any) (swapped bool)
CompareAndSwap меняет местами старое и новое значения для ключа, если значение, хранящееся в карте, равно old. Старое значение должно быть сравнимого типа.
func (*Map) Delete
func (m *Map) Delete(key any)
Delete удаляет значение для ключа.
func (*Map) Load
func (m *Map) Load(key any) (value any, ok bool)
Load возвращает значение, хранящееся в карте для ключа, или nil, если значение отсутствует. Результат ok указывает, было ли найдено значение в карте.
func (*Map) LoadAndDelete
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
LoadAndDelete удаляет значение для ключа, возвращая предыдущее значение, если оно есть. Результат loaded сообщает, присутствовал ли ключ.
func (*Map) LoadOrStore
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
Метод LoadOrStore
выполняет атомарную операцию “загрузить или сохранить” и особенно полезен в конкурентных сценариях, когда несколько горутин могут одновременно пытаться работать с одними и теми же ключами.
Объяснеие, что происходит на самом деле
Как это работает?
actual, loaded := m.LoadOrStore(key, value)
-
Если ключ
key
уже существует в мапе:- Возвращает существующее значение (в
actual
) loaded = true
(значение было загружено)
- Возвращает существующее значение (в
-
Если ключ
key
не существует в мапе:- Сохраняет переданное
value
по этому ключу - Возвращает это же значение (в
actual
) loaded = false
(значение было сохранено)
- Сохраняет переданное
Простой пример
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Пытаемся сохранить значение "apple" по ключу "fruit"
// Так как ключа нет - оно будет сохранено
actual, loaded := m.LoadOrStore("fruit", "apple")
fmt.Printf("Ключ 'fruit': actual=%v, loaded=%v\n", actual, loaded)
// Вывод: Ключ 'fruit': actual=apple, loaded=false
// Пытаемся сохранить "banana" по тому же ключу
// Но ключ уже существует - возвращается текущее значение
actual, loaded = m.LoadOrStore("fruit", "banana")
fmt.Printf("Ключ 'fruit': actual=%v, loaded=%v\n", actual, loaded)
// Вывод: Ключ 'fruit': actual=apple, loaded=true
// Проверяем текущее значение
value, _ := m.Load("fruit")
fmt.Println("Текущее значение для 'fruit':", value)
// Вывод: Текущее значение для 'fruit': apple
}
Реальный пример: кэширование результатов
Представим сервис, который кэширует результаты дорогих вычислений:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var cache sync.Map
func expensiveCalculation(id int) int {
// Имитация долгого вычисления
time.Sleep(time.Second)
return rand.Intn(1000)
}
func getCachedResult(id int) int {
// Пытаемся получить результат из кэша
result, loaded := cache.LoadOrStore(id, expensiveCalculation(id))
if loaded {
fmt.Printf("Результат для %d взят из кэша\n", id)
} else {
fmt.Printf("Результат для %d вычислен и сохранен\n", id)
}
return result.(int)
}
func main() {
rand.Seed(time.Now().UnixNano())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
getCachedResult(id % 3) // Используем только 3 разных ID
}(i)
}
wg.Wait()
}
Возможный вывод:
Результат для 0 вычислен и сохранен
Результат для 1 вычислен и сохранен
Результат для 2 вычислен и сохранен
Результат для 0 взят из кэша
Результат для 1 взят из кэша
Особенности LoadOrStore
- Атомарность: Операция выполняется атомарно, что делает её безопасной для использования из нескольких горутин
- Эффективность: Избегает “гонки” при инициализации значений
- Удобство: Заменяет распространённый паттерн “проверить-затем-сохранить”
Когда использовать?
LoadOrStore
идеально подходит для:
- Кэширования результатов
- Инициализации синглтонов
- Создания элементов по требованию
- Любых сценариев, где нужно “получить или создать” значение атомарно
Этот метод особенно полезен в высоконагруженных системах, где несколько горутин могут одновременно запрашивать одни и те же ресурсы.
func (*Map) Range
func (m *Map) Range(f func(key, value any) bool)
Range вызывает f последовательно для каждого ключа и значения, присутствующих в карте. Если f возвращает false, range останавливает итерацию.
Range не обязательно соответствует какому-либо последовательному снимку содержимого карты: ни один ключ не будет посещен более одного раза, но если значение для любого ключа хранится или удаляется одновременно (в том числе f), Range может отражать любое сопоставление для этого ключа из любой точки во время вызова Range. Range не блокирует другие методы на приемнике; даже f может вызывать любой метод на m.
Range может быть O(N) с количеством элементов в карте, даже если f возвращает false после постоянного числа вызовов.
func (*Map) Store
func (m *Map) Store(key, value any)
Store устанавливает значение для ключа.
func (*Map) Swap
func (m *Map) Swap(key, value any) (previous any, loaded bool)
Swap заменяет значение ключа и возвращает предыдущее значение, если оно есть. Результат loaded сообщает, присутствовал ли ключ.
type Mutex
type Mutex struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Mutex — это блокировка взаимного исключения. Нулевое значение для Mutex — это разблокированный мьютекс.
Mutex не должен копироваться после первого использования.
В терминологии модели памяти Go n-й вызов Mutex.Unlock «синхронизирует перед» m-й вызов Mutex.Lock для любого n < m. Успешный вызов Mutex.TryLock эквивалентен вызову Lock. Неудачный вызов TryLock не устанавливает никаких отношений «синхронизирует перед».
Объяснение Mutex
Mutex
(сокращение от “mutual exclusion” - взаимное исключение) - это примитив синхронизации, который позволяет только одной горутине за раз получать доступ к общему ресурсу.
Представьте его как дверь в туалет:
- Когда кто-то внутри, дверь заперта (Lock)
- Другие ждут снаружи (блокируются)
- Когда человек выходит, он открывает дверь (Unlock)
- Тогда следующий может войти
Основные методы:
Lock()
- захватывает мьютекс (блокирует, если он уже захвачен)Unlock()
- освобождает мьютексTryLock()
- пытается захватить мьютекс без блокировки (возвращает успех/неудачу)
Простой пример:
package main
import (
"fmt"
"sync"
"time"
)
var counter int
var mu sync.Mutex // Создаем мьютекс
func increment() {
mu.Lock() // Захватываем мьютекс
defer mu.Unlock() // Гарантируем освобождение
temp := counter
time.Sleep(1 * time.Millisecond) // Имитируем работу
counter = temp + 1
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Итоговое значение счетчика:", counter) // Всегда 100
}
Реальный пример: Банковский перевод
type BankAccount struct {
balance int
mu sync.Mutex
}
func (acc *BankAccount) Deposit(amount int) {
acc.mu.Lock()
defer acc.mu.Unlock()
acc.balance += amount
}
func (acc *BankAccount) Withdraw(amount int) bool {
acc.mu.Lock()
defer acc.mu.Unlock()
if acc.balance < amount {
return false
}
acc.balance -= amount
return true
}
func (acc *BankAccount) Transfer(to *BankAccount, amount int) bool {
// Важно: блокируем оба счета в одном порядке, чтобы избежать deadlock
acc.mu.Lock()
defer acc.mu.Unlock()
to.mu.Lock()
defer to.mu.Unlock()
if acc.balance < amount {
return false
}
acc.balance -= amount
to.balance += amount
return true
}
Когда использовать Mutex?
- Когда несколько горутин работают с общими данными
- Когда нужно гарантировать целостность данных
- Для защиты операций, которые должны выполняться атомарно
Важные правила:
- Всегда освобождайте мьютекс (лучше через
defer
) - Не копируйте мьютекс после использования
- Избегайте блокировок на долгое время
- Соблюдайте порядок блокировки нескольких мьютексов
TryLock пример:
func tryUpdate(data *string) {
var mu sync.Mutex
if mu.TryLock() {
defer mu.Unlock()
*data = "updated"
} else {
fmt.Println("Не удалось получить блокировку")
}
}
func (*Mutex) Lock
func (m *Mutex) Lock()
Lock блокирует m. Если блокировка уже используется, вызывающая goroutine блокируется до тех пор, пока мьютекс не станет доступным.
func (*Mutex) TryLock
func (m *Mutex) TryLock() bool
TryLock пытается заблокировать m и сообщает, удалось ли это.
Обратите внимание, что хотя правильные способы использования TryLock существуют, они редки, и использование TryLock часто является признаком более глубокой проблемы в конкретном использовании мьютексов.
func (*Mutex) Unlock
func (m *Mutex) Unlock()
Unlock разблокирует m. Если m не заблокирован при входе в Unlock, возникает ошибка выполнения.
Заблокированный мьютекс не связан с конкретной горутиной. Одна горутина может заблокировать мьютекс, а затем организовать его разблокировку другой горутиной.
type Once
type Once struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Once — это объект, который выполнит ровно одно действие.
Once нельзя копировать после первого использования.
В терминологии модели памяти Go возврат из f «синхронизируется перед» возвратом из любого вызова once.Do(f).
Пример
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Этот код демонстрирует использование sync.Once
в Go - структуры, которая гарантирует, что определённая функция будет выполнена ровно один раз, даже если её вызов происходит из нескольких горутин.
Разберём код по частям:
- Инициализация:
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
- Создаётся объект
sync.Once
- Определяется функция
onceBody
, которая будет выполнена один раз
- Канал для синхронизации:
done := make(chan bool)
- Создаётся буферизированный канал для ожидания завершения всех горутин
- Запуск горутин:
for i := 0; i < 10; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
- Запускается 10 горутин
- Каждая вызывает
once.Do(onceBody)
- После выполнения отправляет сигнал в канал
done
- Ожидание завершения:
for i := 0; i < 10; i++ {
<-done
}
- Основная горутина ждёт 10 сигналов (по одному от каждой горутины)
Что произойдёт при выполнении:
- Все 10 горутин попытаются выполнить
onceBody
черезonce.Do()
sync.Once
гарантирует, что:- Функция
onceBody
будет выполнена только один раз - Остальные горутин будут ждать завершения этого выполнения
- Все последующие вызовы будут проигнорированы
- Функция
- В результате в консоли мы увидим только одно сообщение “Only once”
Практическое применение:
- Инициализация глобальных ресурсов
- Ленивая инициализация
- Создание синглтонов
- Однократное выполнение настройки
Этот пример наглядно показывает мощь sync.Once
для решения задач, требующих однократного выполнения в конкурентной среде.
func (*Once) Do
func (o *Once) Do(f func())
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.
type Pool
type Pool struct {
// New опционально указывает функцию для генерации
// значения, когда Get в противном случае вернул бы nil.
// Он не может быть изменен одновременно с вызовами Get.
New func() any
// содержит отфильтрованные или неэкспортируемые поля
}
Pool — это набор временных объектов, которые могут быть индивидуально сохранены и извлечены.
Любой элемент, хранящийся в Pool, может быть удален автоматически в любое время без уведомления. Если Pool содержит единственную ссылку, когда это происходит, элемент может быть деаллоцирован.
Pool безопасен для одновременного использования несколькими goroutines.
Цель пула — кэшировать выделенные, но неиспользуемые элементы для повторного использования в будущем, снимая нагрузку с сборщика мусора. То есть он упрощает создание эффективных, потокобезопасных списков свободных элементов. Однако он подходит не для всех списков свободных элементов.
Пул целесообразно использовать для управления группой временных элементов, которые незаметно совместно используются и потенциально повторно используются одновременно независимыми клиентами пакета. Пул позволяет амортизировать накладные расходы на выделение памяти между многими клиентами.
Примером правильного использования пула является пакет fmt, который поддерживает хранилище временных буферов вывода динамического размера. Хранилище масштабируется под нагрузкой (когда многие goroutines активно печатают) и сокращается в состоянии покоя.
С другой стороны, свободный список, поддерживаемый как часть короткоживущего объекта, не подходит для использования в пуле, поскольку в этом сценарии накладные расходы не амортизируются должным образом. Более эффективно, чтобы такие объекты реализовывали свой собственный свободный список.
Пул не должен копироваться после первого использования.
В терминологии модели памяти Go вызов Put(x) «синхронизируется перед» вызовом Pool.Get, возвращающим то же значение x. Аналогично, вызов New, возвращающий x, «синхронизируется перед» вызовом Get, возвращающим то же значение x.
Пример
package main
import (
"bytes"
"io"
"os"
"sync"
"time"
)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func timeNow() time.Time {
return time.Unix(1136214245, 0)
}
func Log(w io.Writer, key, val string) {
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
b.WriteString(timeNow().UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(val)
w.Write(b.Bytes())
bufPool.Put(b)
}
func main() {
Log(os.Stdout, "path", "/search?q=flowers")
}
Этот пример демонстрирует эффективное использование sync.Pool
для оптимизации работы с временными объектами (в данном случае - bytes.Buffer
).
1. Пул буферов (sync.Pool
)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
- Создается пул объектов
bytes.Buffer
- Функция
New
создает новый буфер, когда пул пуст - Важно: возвращаются указатели (
new(bytes.Buffer)
), чтобы избежать лишних аллокаций
2. Функция логирования
func Log(w io.Writer, key, val string) {
// 1. Получаем буфер из пула (или создаем новый)
b := bufPool.Get().(*bytes.Buffer)
// 2. Сбрасываем состояние буфера перед использованием
b.Reset()
// 3. Формируем строку лога
b.WriteString(timeNow().UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(val)
// 4. Выводим лог
w.Write(b.Bytes())
// 5. Возвращаем буфер в пул для повторного использования
bufPool.Put(b)
}
3. Тестовая реализация времени
func timeNow() time.Time {
return time.Unix(1136214245, 0) // Фиксированное время для демонстрации
}
Как это работает:
-
При первом вызове
Log
:- Пул пуст, поэтому вызывается
New
и создается новыйbytes.Buffer
- Буфер используется и возвращается в пул
- Пул пуст, поэтому вызывается
-
При последующих вызовах:
- Буфер берется из пула (без аллокации)
- Сбрасывается (
Reset()
) - Используется повторно
Преимущества подхода:
-
Снижение нагрузки на GC:
- Буферы переиспользуются, а не создаются заново
- Меньше работы для сборщика мусора
-
Экономия памяти:
- Не нужно постоянно выделять/освобождать память
- Размер пула автоматически регулируется
-
Потокобезопасность:
sync.Pool
безопасен для использования из нескольких горутин
Важные нюансы:
-
Обязательно сбрасывайте состояние:
- Перед использованием вызовите
Reset()
, чтобы очистить предыдущие данные
- Перед использованием вызовите
-
Не сохраняйте объекты из пула:
- После
Put()
считайте объект более недоступным
- После
-
Подходит для часто создаваемых объектов:
- Идеально для объектов, которые:
- Дорого создавать
- Используются кратковременно
- Имеют примерно одинаковый размер
- Идеально для объектов, которые:
Реальный вывод программы:
2006-01-02T15:04:05Z path=/search?q=flowers
Этот паттерн особенно полезен в:
- Логгерах
- HTTP middleware
- Любом коде, где часто создаются временные буферы
func (*Pool) Get
func (p *Pool) Get() any
Get выбирает произвольный элемент из пула, удаляет его из пула и возвращает вызывающему. Get может игнорировать пул и рассматривать его как пустой. Вызывающие не должны предполагать какую-либо связь между значениями, переданными в Pool.Put, и значениями, возвращаемыми Get.
Если Get в противном случае вернул бы nil, а p.New не равен nil, Get возвращает результат вызова p.New.
func (*Pool) Put
func (p *Pool) Put(x any)
Put добавляет x в пул.
type RWMutex
type RWMutex struct {
// содержит отфильтрованные или неэкспортируемые поля
}
RWMutex — это блокировка взаимного исключения для чтения/записи. Блокировка может удерживаться произвольным количеством читателей или одним записывающим устройством. Нулевое значение для RWMutex — это разблокированный мьютекс.
RWMutex не должен копироваться после первого использования.
Если какой-либо goroutine вызывает RWMutex.Lock, когда блокировка уже удерживается одним или несколькими читателями, одновременные вызовы RWMutex.RLock будут блокироваться до тех пор, пока записывающий не получит (и не освободит) блокировку, чтобы обеспечить доступность блокировки для записывающего. Обратите внимание, что это запрещает рекурсивную блокировку чтения. RWMutex.RLock не может быть повышен до RWMutex.Lock, а RWMutex.Lock не может быть понижен до RWMutex.RLock.
В терминологии модели памяти Go n-й вызов RWMutex.Unlock «синхронизируется перед» m-м вызовом Lock для любого n < m, так же как и для Mutex. Для любого вызова RLock существует n, такое что n-й вызов Unlock «синхронизируется перед» этим вызовом RLock, а соответствующий вызов RWMutex.RUnlock «синхронизируется перед» n+1-м вызовом Lock.
func (*RWMutex) Lock
func (rw *RWMutex) Lock()
Lock блокирует rw для записи. Если блокировка уже заблокирована для чтения или записи, Lock блокируется до тех пор, пока блокировка не станет доступной.
func (*RWMutex) RLock
func (rw *RWMutex) RLock()
RLock блокирует rw для чтения.
Его не следует использовать для рекурсивной блокировки чтения; заблокированный вызов Lock исключает возможность получения блокировки новыми читателями. См. документацию по типу RWMutex.
func (*RWMutex) RLocker
func (rw *RWMutex) RLocker() Locker
RLocker возвращает интерфейс Locker, который реализует методы [Locker.Lock] и [Locker.Unlock] путем вызова rw.RLock и rw.RUnlock.
func (*RWMutex) RUnlock
func (rw *RWMutex) RUnlock()
RUnlock отменяет один вызов RWMutex.RLock; это не влияет на других одновременных читателей. Если rw не заблокирован для чтения при входе в RUnlock, возникает ошибка выполнения.
func (*RWMutex) TryLock
func (rw *RWMutex) TryLock() bool
TryLock пытается заблокировать rw для записи и сообщает, удалось ли это.
Обратите внимание, что хотя правильное использование TryLock существует, оно встречается редко, и использование TryLock часто является признаком более глубокой проблемы в конкретном использовании мьютексов.
func (*RWMutex) TryRLock
func (rw *RWMutex) TryRLock() bool
TryRLock пытается заблокировать rw для чтения и сообщает, удалось ли это.
Обратите внимание, что хотя правильное использование TryRLock существует, оно встречается редко, и использование TryRLock часто является признаком более глубокой проблемы в конкретном использовании мьютексов.
func (*RWMutex) Unlock
func (rw *RWMutex) Unlock()
Unlock разблокирует rw для записи. Если rw не заблокирован для записи при входе в Unlock, возникает ошибка выполнения.
Как и в случае с мьютексами, заблокированный RWMutex не связан с конкретной горутиной. Одна горутина может RWMutex.RLock (RWMutex.Lock) RWMutex, а затем организовать, чтобы другая горутина RWMutex.RUnlock (RWMutex.Unlock) его.
type WaitGroup
type WaitGroup struct {
// содержит отфильтрованные или неэкспортируемые поля
}
WaitGroup ожидает завершения работы набора goroutines. Основной goroutine вызывает WaitGroup.Add, чтобы установить количество goroutines, которые необходимо дождаться. Затем каждый из goroutines запускается и вызывает WaitGroup.Done по завершении работы. В то же время WaitGroup.Wait можно использовать для блокировки до тех пор, пока все goroutines не завершат работу.
WaitGroup нельзя копировать после первого использования.
В терминологии модели памяти Go вызов WaitGroup.Done «синхронизирует перед» возвратом любого вызова Wait, который он разблокирует.
Пример
package main
import (
"sync"
)
type httpPkg struct{}
func (httpPkg) Get(url string) {}
var http httpPkg
func main() {
var wg sync.WaitGroup
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.example.com/",
}
for _, url := range urls {
// Increment the WaitGroup counter.
wg.Add(1)
// Launch a goroutine to fetch the URL.
go func(url string) {
// Decrement the counter when the goroutine completes.
defer wg.Done()
// Fetch the URL.
http.Get(url)
}(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()
}
Этот пример демонстрирует классический паттерн использования sync.WaitGroup
для ожидания завершения группы горутин.
1. Структура и имитация HTTP-клиента
type httpPkg struct{}
func (httpPkg) Get(url string) {} // Пустой метод для демонстрации
var http httpPkg // Глобальная переменная "HTTP-клиента"
- Создается пустая структура
httpPkg
с методомGet
(имитация HTTP-запроса) - Создается глобальная переменная
http
этого типа
2. Основная функция
func main() {
var wg sync.WaitGroup // Создаем WaitGroup
urls := []string{ // Список URL для обработки
"http://www.golang.org/",
"http://www.google.com/",
"http://www.example.com/",
}
- Инициализируется
WaitGroup
для отслеживания горутин - Определяется список URL для обработки
3. Запуск горутин
for _, url := range urls {
wg.Add(1) // Увеличиваем счетчик WaitGroup перед запуском горутины
go func(url string) { // Запускаем горутину для каждого URL
defer wg.Done() // Уменьшаем счетчик при завершении (defer гарантирует выполнение)
http.Get(url) // "Выполняем" HTTP-запрос
}(url) // Важно: передаем url как параметр, чтобы избежать проблем с замыканием
}
- Для каждого URL:
wg.Add(1)
- увеличиваем счетчик ожидаемых горутин- Запускаем анонимную функцию как горутину
defer wg.Done()
- гарантируем уменьшение счетчика при любом выходе из функции- Выполняем “запрос” с помощью
http.Get
4. Ожидание завершения
wg.Wait() // Блокируемся, пока счетчик не станет 0
}
- Основная горутина блокируется, пока все запущенные горутины не вызовут
Done()
Как это работает:
- Изначально счетчик WaitGroup = 0
- Для каждого URL:
Add(1)
увеличивает счетчик (становится 1, 2, 3…)- Горутина запускается
- Каждая горутина при завершении вызывает
Done()
(уменьшает счетчик) Wait()
блокируется, пока счетчик не вернется к 0
Важные моменты:
-
Порядок операций:
- Всегда вызывайте
Add()
ДО запуска горутины - Используйте
defer wg.Done()
для надежности
- Всегда вызывайте
-
Передача параметров:
- URL передается как аргумент в горутину (
go func(url string)
) - Это избегает проблем с общим доступом к переменной цикла
- URL передается как аргумент в горутину (
-
Реальные применения:
- Параллельные HTTP-запросы
- Обработка файлов/данных в нескольких горутинах
- Любые задачи, которые можно распараллелить
Что произойдет при выполнении:
- Запустятся 3 горутины (по одной на каждый URL)
- Основная горутина заблокируется на
wg.Wait()
- Когда все 3 горутины завершатся (вызовут
Done()
) - Основная горутина продолжит выполнение (и завершит программу)
Этот паттерн - основа для многих конкурентных операций в Go, где нужно дождаться завершения группы горутин.
func (*WaitGroup) Add
func (wg *WaitGroup) Add(delta int)
Add добавляет delta, которая может быть отрицательной, к счетчику WaitGroup. Если счетчик становится равным нулю, все goroutines, заблокированные WaitGroup.Wait, освобождаются. Если счетчик становится отрицательным, Add вызывает панику.
Обратите внимание, что вызовы с положительной дельтой, которые происходят, когда счетчик равен нулю, должны происходить перед Wait. Вызовы с отрицательной дельтой или вызовы с положительной дельтой, которые начинаются, когда счетчик больше нуля, могут происходить в любое время. Обычно это означает, что вызовы Add должны выполняться до оператора, создающего goroutine или другое событие, которое необходимо ожидать. Если WaitGroup повторно используется для ожидания нескольких независимых наборов событий, новые вызовы Add должны происходить после того, как все предыдущие вызовы Wait вернулись. См. пример WaitGroup.
func (*WaitGroup) Done
func (wg *WaitGroup) Done()
Done уменьшает счетчик WaitGroup на единицу.
func (*WaitGroup) Wait
func (wg *WaitGroup) Wait()
Wait блокируется до тех пор, пока счетчик WaitGroup не станет равным нулю.