Это многостраничный печатный вид этого раздела. Нажмите что бы печатать.

Вернуться к обычному просмотру страницы.

Пакет Sync для Go

Пакет sync предоставляет базовые примитивы синхронизации, такие как взаимоисключающие блокировки.

За исключением типов 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 нужен для того, чтобы:

  1. Одни горутины могли “уснуть” и ждать какого-то условия
  2. Другие горутины могли их “разбудить”, когда это условие выполнится
Основные методы:
  • 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 полезен, когда:

  1. У вас сложное условие ожидания (не просто “есть данные”)
  2. Нужно уведомлять сразу несколько горутин
  3. Состояние может меняться часто и нужно минимизировать накладные расходы

Каналы лучше подходят для более простых случаев передачи данных между горутинами.

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

type Locker

type Locker интерфейс {
    Lock()
    Unlock()
}

Locker - это интерфейс из пакета sync, который определяет базовые методы для блокировки:

Подробное объяснение типа Locker

Этот интерфейс реализуют:

  • sync.Mutex - обычная мьютекс-блокировка
  • sync.RWMutex - блокировка с возможностью множественного чтения
  • Любые другие типы, которые реализуют эти два метода

Зачем нужен Locker?

  1. Унификация работы с разными типами блокировок - вы можете писать функции, которые работают с любым типом блокировки
  2. Абстракция - позволяет не зависеть от конкретной реализации блокировки
  3. Тестирование - можно создавать 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?

  1. Когда ваша функция/метод должен работать с разными типами блокировок
  2. Когда вы хотите сделать код более гибким для тестирования
  3. Когда вы разрабатываете библиотеку и хотите оставить выбор блокировки пользователю

Locker - это простой интерфейс, который позволяет абстрагироваться от конкретного типа блокировки. Он делает ваш код более гибким и переиспользуемым, особенно когда речь идет о конкурентных операциях.

type Map

type Map struct {
    // содержит отфильтрованные или неэкспортируемые поля
}

Map похож на Go map[any]any, но безопасен для одновременного использования несколькими goroutines без дополнительной блокировки или координации. Загрузка, хранение и удаление выполняются за амортизированное постоянное время.

Тип Map является специализированным. В большинстве случаев следует использовать обычный Go map с отдельной блокировкой или координацией, чтобы обеспечить лучшую типовую безопасность и упростить поддержание других инвариантов наряду с содержимым карты.

Тип Map оптимизирован для двух распространенных случаев использования:

  1. когда запись для данного ключа записывается только один раз, но читается много раз, как в кэшах, которые только растут, или
  2. когда несколько 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 выполняет атомарную операцию:

  1. Проверяет, соответствует ли текущее значение для указанного ключа old значению
  2. Если значения совпадают - удаляет запись из мапы
  3. Возвращает 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
	})
}
Особенности работы
  1. Безопасность для concurrent-использования: метод можно вызывать из нескольких горутин без дополнительной синхронизации
  2. Сравнение значений: сравнение происходит через ==, поэтому для сложных типов нужно быть внимательным
  3. 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
  1. Атомарность: Операция выполняется атомарно, что делает её безопасной для использования из нескольких горутин
  2. Эффективность: Избегает “гонки” при инициализации значений
  3. Удобство: Заменяет распространённый паттерн “проверить-затем-сохранить”
Когда использовать?

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)
  • Тогда следующий может войти

Основные методы:

  1. Lock() - захватывает мьютекс (блокирует, если он уже захвачен)
  2. Unlock() - освобождает мьютекс
  3. 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?

  1. Когда несколько горутин работают с общими данными
  2. Когда нужно гарантировать целостность данных
  3. Для защиты операций, которые должны выполняться атомарно

Важные правила:

  1. Всегда освобождайте мьютекс (лучше через defer)
  2. Не копируйте мьютекс после использования
  3. Избегайте блокировок на долгое время
  4. Соблюдайте порядок блокировки нескольких мьютексов

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 - структуры, которая гарантирует, что определённая функция будет выполнена ровно один раз, даже если её вызов происходит из нескольких горутин.

Разберём код по частям:

  1. Инициализация:
var once sync.Once
onceBody := func() {
    fmt.Println("Only once")
}
  • Создаётся объект sync.Once
  • Определяется функция onceBody, которая будет выполнена один раз
  1. Канал для синхронизации:
done := make(chan bool)
  • Создаётся буферизированный канал для ожидания завершения всех горутин
  1. Запуск горутин:
for i := 0; i < 10; i++ {
    go func() {
        once.Do(onceBody)
        done <- true
    }()
}
  • Запускается 10 горутин
  • Каждая вызывает once.Do(onceBody)
  • После выполнения отправляет сигнал в канал done
  1. Ожидание завершения:
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) // Фиксированное время для демонстрации
}
Как это работает:
  1. При первом вызове Log:

    • Пул пуст, поэтому вызывается New и создается новый bytes.Buffer
    • Буфер используется и возвращается в пул
  2. При последующих вызовах:

    • Буфер берется из пула (без аллокации)
    • Сбрасывается (Reset())
    • Используется повторно
Преимущества подхода:
  1. Снижение нагрузки на GC:

    • Буферы переиспользуются, а не создаются заново
    • Меньше работы для сборщика мусора
  2. Экономия памяти:

    • Не нужно постоянно выделять/освобождать память
    • Размер пула автоматически регулируется
  3. Потокобезопасность:

    • sync.Pool безопасен для использования из нескольких горутин
Важные нюансы:
  1. Обязательно сбрасывайте состояние:

    • Перед использованием вызовите Reset(), чтобы очистить предыдущие данные
  2. Не сохраняйте объекты из пула:

    • После Put() считайте объект более недоступным
  3. Подходит для часто создаваемых объектов:

    • Идеально для объектов, которые:
      • Дорого создавать
      • Используются кратковременно
      • Имеют примерно одинаковый размер
Реальный вывод программы:
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:
    1. wg.Add(1) - увеличиваем счетчик ожидаемых горутин
    2. Запускаем анонимную функцию как горутину
    3. defer wg.Done() - гарантируем уменьшение счетчика при любом выходе из функции
    4. Выполняем “запрос” с помощью http.Get

4. Ожидание завершения

	wg.Wait()  // Блокируемся, пока счетчик не станет 0
}
  • Основная горутина блокируется, пока все запущенные горутины не вызовут Done()

Как это работает:

  1. Изначально счетчик WaitGroup = 0
  2. Для каждого URL:
    • Add(1) увеличивает счетчик (становится 1, 2, 3…)
    • Горутина запускается
  3. Каждая горутина при завершении вызывает Done() (уменьшает счетчик)
  4. Wait() блокируется, пока счетчик не вернется к 0

Важные моменты:

  1. Порядок операций:

    • Всегда вызывайте Add() ДО запуска горутины
    • Используйте defer wg.Done() для надежности
  2. Передача параметров:

    • URL передается как аргумент в горутину (go func(url string))
    • Это избегает проблем с общим доступом к переменной цикла
  3. Реальные применения:

    • Параллельные HTTP-запросы
    • Обработка файлов/данных в нескольких горутинах
    • Любые задачи, которые можно распараллелить

Что произойдет при выполнении:

  1. Запустятся 3 горутины (по одной на каждый URL)
  2. Основная горутина заблокируется на wg.Wait()
  3. Когда все 3 горутины завершатся (вызовут Done())
  4. Основная горутина продолжит выполнение (и завершит программу)

Этот паттерн - основа для многих конкурентных операций в 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 не станет равным нулю.

1 - Описание пакета sync/atomic

Пакет atomic предоставляет низкоуровневые примитивы атомарной памяти, полезные для реализации алгоритмов синхронизации.

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

Операция обмена, реализуемая функциями SwapT, является атомарным эквивалентом:

old = *addr
*addr = new
return old

Операция сравнения и замены, реализуемая функциями CompareAndSwapT, является атомарным эквивалентом:

if *addr == old {
	*addr = new
	return true
}
return false

Операция сложения, реализуемая функциями AddT, является атомарным эквивалентом:

*addr += delta
return *addr

Операции load и store, реализуемые функциями LoadT и StoreT, являются атомарными эквивалентами “return *addr” и “*addr = val”.

В терминологии модели памяти Go, если эффект атомарной операции A наблюдается атомарной операцией B, то A “синхронизируется раньше” B. Кроме того, все атомарные операции, выполняемые в программе, ведут себя так, как будто выполняются в некотором последовательно согласованном порядке. Это определение обеспечивает ту же семантику, что и последовательные атомики в C++ и волатильные переменные в Java.

Функции

func AddInt32

func AddInt32(addr *int32, delta int32) (new int32)

AddInt32 атомарно добавляет delta к *addr и возвращает новое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Add.

func AddInt64

func AddInt64(addr *int64, delta int64) (new int64)

AddInt64 атомарно добавляет delta к *addr и возвращает новое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Add (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func AddUint32

func AddUint32(addr *uint32, delta uint32) (new uint32)

AddUint32 атомарно добавляет дельту к *addr и возвращает новое значение. Чтобы вычесть из x знаковое положительное константное значение c, выполните AddUint32(&x, ^uint32(c-1)). В частности, чтобы уменьшить x, выполните AddUint32(&x, ^uint32(0)). Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Add.

func AddUint64

func AddUint64(addr *uint64, delta uint64) (new uint64)

AddUint64 атомарно добавляет delta к *addr и возвращает новое значение. Чтобы вычесть из x знаковое положительное константное значение c, выполните AddUint64(&x, ^uint64(c-1)). В частности, чтобы уменьшить x, выполните AddUint64(&x, ^uint64(0)). Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Add (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func AddUintptr

func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

AddUintptr атомарно добавляет delta к *addr и возвращает новое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Add.

func AndInt32

func AndInt32(addr *int32, mask int32) (old int32)

AndInt32 атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.And.

func AndInt64

func AndInt64(addr *int64, mask int64) (old int64)

AndInt64 атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.And вместо этого.

func AndUint32

func AndUint32(addr *uint32, mask uint32) (old uint32)

AndUint32 атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.And вместо него.

func AndUint64

func AndUint64(addr *uint64, mask uint64) (old uint64)

AndUint64 атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.And вместо этого.

func AndUintptr

func AndUintptr(addr *uintptr, mask uintptr) (old uintptr)

AndUintptr атомарно выполняет побитовую операцию AND над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.And вместо этого.

func CompareAndSwapInt32

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

CompareAndSwapInt32 выполняет операцию сравнения и замены для значения int32. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.CompareAndSwap.

func CompareAndSwapInt64

func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)

CompareAndSwapInt64 выполняет операцию сравнения и замены для значения int64. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.CompareAndSwap (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func CompareAndSwapPointer

func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

CompareAndSwapPointer выполняет операцию сравнения и обмена для значения unsafe.Pointer. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Pointer.CompareAndSwap.

func CompareAndSwapUint32

func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)

CompareAndSwapUint32 выполняет операцию сравнения и замены для значения uint32. Рассмотрите возможность использования более удобной и менее подверженной ошибкам функции Uint32.CompareAndSwap.

func CompareAndSwapUint64

func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)

CompareAndSwapUint64 выполняет операцию сравнения и замены для значения uint64. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.CompareAndSwap (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func CompareAndSwapUintptr

func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)

CompareAndSwapUintptr выполняет операцию сравнения и замены для значения uintptr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.CompareAndSwap.

func LoadInt32

func LoadInt32(addr *int32) (val int32)

LoadInt32 атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Load.

func LoadInt64

func LoadInt64(addr *int64) (val int64)

LoadInt64 атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Load (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func LoadPointer

func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

LoadPointer атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Pointer.Load.

func LoadUint32

func LoadUint32(addr *uint32) (val uint32)

LoadUint32 атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Load.

func LoadUint64

func LoadUint64(addr *uint64) (val uint64)

LoadUint64 атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Load (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func LoadUintptr

func LoadUintptr(addr *uintptr) (val uintptr)

LoadUintptr атомарно загружает *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Load.

func OrInt32

func OrInt32(addr *int32, mask int32) (old int32)

OrInt32 атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, указанную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Or.

func OrInt64

func OrInt64(addr *int64, mask int64) (old int64)

OrInt64 атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Or вместо него.

func OrUint32

func OrUint32(addr *uint32, mask uint32) (старый uint32)

OrUint32 атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Or.

func OrUint64

func OrUint64(addr *uint64, mask uint64) (old uint64)

OrUint64 атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Or вместо него.

func OrUintptr

func OrUintptr(addr *uintptr, mask uintptr) (old uintptr)

OrUintptr атомарно выполняет побитовую операцию OR над *addr, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Or.

func StoreInt32

func StoreInt32(addr *int32, val int32)

StoreInt32 атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Store.

func StoreInt64

func StoreInt64(addr *int64, val int64)

StoreInt64 атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Store (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func StorePointer

func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

StorePointer атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Pointer.Store.

func StoreUint32

func StoreUint32(addr *uint32, val uint32)

StoreUint32 атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Store.

func StoreUint64

func StoreUint64(addr *uint64, val uint64)

StoreUint64 атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Store (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func StoreUintptr

func StoreUintptr(addr *uintptr, val uintptr)

StoreUintptr атомарно сохраняет val в *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Store.

func SwapInt32

func SwapInt32(addr *int32, new int32) (old int32)

SwapInt32 атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int32.Swap.

func SwapInt64

func SwapInt64(addr *int64, new int64) (old int64)

SwapInt64 атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Int64.Swap (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func SwapPointer

func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

SwapPointer атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Pointer.Swap.

func SwapUint32

func SwapUint32(addr *uint32, new uint32) (old uint32)

SwapUint32 атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint32.Swap.

func SwapUint64

func SwapUint64(addr *uint64, new uint64) (old uint64)

SwapUint64 атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uint64.Swap (особенно если вы ориентируетесь на 32-разрядные платформы; см. раздел «Ошибки»).

func SwapUintptr

func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)

SwapUintptr атомарно сохраняет new в *addr и возвращает предыдущее значение *addr. Рассмотрите возможность использования более эргономичного и менее подверженного ошибкам Uintptr.Swap.

Типы

type Bool

type Bool struct {
    // содержит отфильтрованные или неэкспортируемые поля
}

Bool — это атомарное булево значение. Нулевое значение — false.

func (*Bool) CompareAndSwap

func (x *Bool) CompareAndSwap(old, new bool) (swapped bool)

CompareAndSwap выполняет операцию сравнения и замены для булевого значения x.

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var flag atomic.Bool
	flag.Store(false)

	// Пытаемся изменить false → true (успешно)
	swapped := flag.CompareAndSwap(false, true)
	fmt.Println("Swapped (false→true):", swapped, "Flag:", flag.Load())

	// Пытаемся изменить false → true (неуспешно, текущее значение уже true)
	swapped = flag.CompareAndSwap(false, true)
	fmt.Println("Swapped (false→true):", swapped, "Flag:", flag.Load())
}

func (*Bool) Load

func (x *Bool) Load() bool

Load атомарно загружает и возвращает значение, хранящееся в x.

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
	var ready atomic.Bool
	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		time.Sleep(500 * time.Millisecond)
		ready.Store(true)
	}()

	for !ready.Load() {
		fmt.Println("Waiting...")
		time.Sleep(100 * time.Millisecond)
	}
	fmt.Println("Ready!")
	wg.Wait()
}

func (*Bool) Store

func (x *Bool) Store(val bool)

Store атомарно сохраняет val в x.

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var initialized atomic.Bool
	var wg sync.WaitGroup

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			if initialized.Load() {
				fmt.Printf("Worker %d: already initialized\n", id)
				return
			}

			initialized.Store(true)
			fmt.Printf("Worker %d: did initialization\n", id)
		}(i)
	}

	wg.Wait()
}

func (*Bool) Swap

func (x *Bool) Swap(new bool) (old bool)

Swap атомарно сохраняет new в x и возвращает предыдущее значение.

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var toggle atomic.Bool
	toggle.Store(false)

	// Переключаем значение и получаем старое
	old := toggle.Swap(true)
	fmt.Println("Old value:", old, "New value:", toggle.Load())

	// Переключаем ещё раз
	old = toggle.Swap(false)
	fmt.Println("Old value:", old, "New value:", toggle.Load())
}

type Int32/Int64/Uint32/Uint64/Uintptr

type Int32 struct {
    // содержит отфильтрованные или неэкспортируемые поля
}

Int32 — это атомарное целое число int32. Нулевое значение — ноль. Int64 — это атомарное целое число int64. Нулевое значение — ноль. Uint32 - это атомарный uint32. Нулевое значение равно нулю. Uint64 - это атомарный uint64. Нулевое значение равно нулю. Uintptr - это атомарный uintptr. Нулевое значение равно нулю.

func (*Int32) Add

func (x *Int32) Add(delta int32) (new int32)

Add атомарно добавляет delta к x и возвращает новое значение.

func (*Int32) And

func (x *Int32) And(mask int32) (old int32)

And атомарно выполняет побитовую операцию AND над x, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение.

func (*Int32) CompareAndSwap

func (x *Int32) CompareAndSwap(old, new int32) (swapped bool)

CompareAndSwap выполняет операцию сравнения и замены для x.

func (*Int32) Load

func (x *Int32) Load() int32

Load атомарно загружает и возвращает значение, хранящееся в x.

func (*Int32) Or

func (x *Int32) Or(mask int32) (old int32)

Or атомарно выполняет операцию побитового ИЛИ над x, используя битовую маску, предоставленную в качестве mask, и возвращает старое значение.

func (*Int32) Store

func (x *Int32) Store(val int32)

Store атомарно сохраняет val в x.

func (*Int32) Swap

func (x *Int32) Swap(new int32) (old int32)

Swap атомарно сохраняет new в x и возвращает предыдущее значение.

type Pointer

type Pointer[T any] struct {
    // содержит отфильтрованные или неэкспортируемые поля
}

Pointer — это атомарный указатель типа *T. Нулевое значение — nil *T.

func (*Pointer[T]) CompareAndSwap

func (x *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool)

CompareAndSwap выполняет операцию сравнения и замены для x.

func (*Pointer[T]) Load

func (x *Pointer[T]) Load() *T

Load атомарно загружает и возвращает значение, хранящееся в x.

func (*Pointer[T]) Store

func (x *Pointer[T]) Store(val *T)

Store атомарно сохраняет val в x.

func (*Pointer[T]) Swap

func (x *Pointer[T]) Swap(new *T) (old *T)

Swap атомарно сохраняет new в x и возвращает предыдущее значение.

type Value

type Value struct {
    // содержит отфильтрованные или неэкспортируемые поля
}

Value обеспечивает атомарную загрузку и хранение значения с постоянным типом. Нулевое значение для Value возвращает nil из Value.Load. После вызова Value.Store Value не должен копироваться.

Value не должен копироваться после первого использования.

Пример config
package main

import (
	"fmt"
	"math/rand"
	"sync/atomic"
	"time"
)

// ServerConfig содержит конфигурацию сервера
type ServerConfig struct {
	Host    string
	Port    int
	Timeout time.Duration
	Debug   bool
}

var (
	config  atomic.Value // Текущая конфигурация
	counter atomic.Int32 // Счетчик запросов
)

func loadConfig() ServerConfig {
	// Имитация загрузки конфигурации (в реальности может быть из файла, БД и т.д.)
	rand.Seed(time.Now().UnixNano())
	return ServerConfig{
		Host:    fmt.Sprintf("server-%d.example.com", rand.Intn(3)+1),
		Port:    8000 + rand.Intn(100),
		Timeout: time.Duration(rand.Intn(5)+1) * time.Second,
		Debug:   rand.Intn(2) == 0,
	}
}

func processRequest(workerID int) {
	for {
		// Имитация обработки запроса
		time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)

		// Атомарно получаем текущую конфигурацию
		conf := config.Load().(ServerConfig)
		reqID := counter.Add(1)

		fmt.Printf("Worker %d processing request #%d with config: %+v\n",
			workerID, reqID, conf)
	}
}

func configUpdater() {
	for {
		time.Sleep(5 * time.Second)
		newConfig := loadConfig()
		config.Store(newConfig)
		fmt.Printf("\nConfig updated: %+v\n\n", newConfig)
	}
}

func main() {
	// Инициализация начальной конфигурации
	config.Store(loadConfig())
	fmt.Println("Initial config:", config.Load())

	// Запускаем обновление конфигурации в фоне
	go configUpdater()

	// Запускаем воркеров для обработки запросов
	for i := 1; i <= 5; i++ {
		go processRequest(i)
	}

	// Даем поработать 30 секунд
	time.Sleep(30 * time.Second)
	fmt.Println("Server shutting down...")
}

Вывод программы:

Initial config: {Host:server-3.example.com Port:8034 Timeout:4s Debug:false}

Worker 5 processing request #1 with config: {Host:server-3.example.com Port:8034 Timeout:4s Debug:false}
Worker 3 processing request #2 with config: {Host:server-3.example.com Port:8034 Timeout:4s Debug:false}

Config updated: {Host:server-1.example.com Port:8067 Timeout:2s Debug:true}

Worker 1 processing request #3 with config: {Host:server-1.example.com Port:8067 Timeout:2s Debug:true}
Worker 4 processing request #4 with config: {Host:server-1.example.com Port:8067 Timeout:2s Debug:true}
Пример ReadMostly
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

// ConcurrentMap реализует потокобезопасную map с помощью atomic.Value
type ConcurrentMap struct {
	data atomic.Value // хранит map[string]string
	mu   sync.Mutex   // только для записи
}

// NewConcurrentMap создает новую потокобезопасную map
func NewConcurrentMap() *ConcurrentMap {
	cm := &ConcurrentMap{}
	cm.data.Store(make(map[string]string))
	return cm
}

// Get безопасно получает значение по ключу
func (cm *ConcurrentMap) Get(key string) (string, bool) {
	currentMap := cm.data.Load().(map[string]string)
	val, ok := currentMap[key]
	return val, ok
}

// Set безопасно устанавливает значение по ключу
func (cm *ConcurrentMap) Set(key, value string) {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	// Копируем текущую map
	oldMap := cm.data.Load().(map[string]string)
	newMap := make(map[string]string, len(oldMap)+1)

	// Копируем все элементы
	for k, v := range oldMap {
		newMap[k] = v
	}

	// Добавляем/обновляем элемент
	newMap[key] = value

	// Атомарно заменяем map
	cm.data.Store(newMap)
}

// Delete безопасно удаляет элемент по ключу
func (cm *ConcurrentMap) Delete(key string) {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	oldMap := cm.data.Load().(map[string]string)
	if _, exists := oldMap[key]; !exists {
		return
	}

	newMap := make(map[string]string, len(oldMap))
	for k, v := range oldMap {
		if k != key {
			newMap[k] = v
		}
	}

	cm.data.Store(newMap)
}

func main() {
	config := NewConcurrentMap()

	// Записываем начальные значения
	config.Set("host", "localhost")
	config.Set("port", "8080")
	config.Set("timeout", "30s")

	// Запускаем 5 читателей
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 3; j++ {
				time.Sleep(time.Millisecond * 100)
				if val, ok := config.Get("host"); ok {
					fmt.Printf("Reader %d: host=%s\n", id, val)
				}
			}
		}(i)
	}

	// Запускаем 2 писателя
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 2; j++ {
				time.Sleep(time.Millisecond * 200)
				newHost := fmt.Sprintf("server-%d-%d", id, j)
				config.Set("host", newHost)
				fmt.Printf("Writer %d updated host to %s\n", id, newHost)
			}
		}(i)
	}

	// Запускаем удаление через некоторое время
	wg.Add(1)
	go func() {
		defer wg.Done()
		time.Sleep(time.Millisecond * 300)
		config.Delete("timeout")
		fmt.Println("Deleted timeout key")
	}()

	wg.Wait()

	// Финальное состояние
	fmt.Println("\nFinal config:")
	if host, ok := config.Get("host"); ok {
		fmt.Println("host:", host)
	}
	if port, ok := config.Get("port"); ok {
		fmt.Println("port:", port)
	}
	if _, ok := config.Get("timeout"); !ok {
		fmt.Println("timeout: (deleted)")
	}
}

Вывод программы:

Reader 4: host=localhost
Reader 0: host=localhost
Reader 2: host=localhost
Reader 1: host=localhost
Writer 0 updated host to server-0-0
Reader 3: host=server-0-0
Reader 0: host=server-0-0
Reader 4: host=server-0-0
Writer 1 updated host to server-1-0
Reader 2: host=server-1-0
Deleted timeout key
Reader 1: host=server-1-0
Writer 0 updated host to server-0-1
Writer 1 updated host to server-1-1
Reader 3: host=server-1-1

Final config:
host: server-1-1
port: 8080
timeout: (deleted)

func (*Value) CompareAndSwap

func (v *Value) CompareAndSwap(old, new any) (swapped bool)

CompareAndSwap выполняет операцию сравнения и замены для Value.

Все вызовы CompareAndSwap для данного значения должны использовать значения одного и того же конкретного типа. CompareAndSwap несовместимого типа вызывает панику, как и CompareAndSwap(old, nil).

func (*Value) Load

func (v *Value) Load() (val any)

Load возвращает значение, установленное последним Store. Он возвращает nil, если для этого значения не было вызова Store.

func (*Value) Store

func (v *Value) Store(val any)

Store устанавливает значение Value v равным val. Все вызовы Store для данного Value должны использовать значения одного и того же конкретного типа. Store несовместимого типа вызывает панику, как и Store(nil).

func (*Value) Swap

func (v *Value) Swap(new any) (old any)

Swap сохраняет new в Value и возвращает предыдущее значение. Он возвращает nil, если Value пуст.

Все вызовы Swap для данного Value должны использовать значения одного и того же конкретного типа. Swap несовместимого типа вызывает панику, как и Swap(nil).