Пакет sync и atomic
Если канал — это коммуникация между горутинами, то
syncиsync/atomic— это синхронизация доступа к разделяемой памяти. Mutex, RWMutex, WaitGroup, Once, Pool, atomic — обязательные инструменты. Без них вы получите race conditions, которые проявятся в проде раз в неделю и будут отлаживаться месяц. На собеседовании спросят: про reentrant Mutex (нет!), про zero-value sync.Mutex (usable), про race detector, про разницу atomic/mutex, про sync.Pool.
Содержание
Заголовок раздела «Содержание»- Базовое API
- Под капотом
- Тонкие моменты / Gotchas
- Производительность
- Race detector
- Типичные вопросы на собесе
- Practice
- Источники
1. Базовое API
Заголовок раздела «1. Базовое API»1.1. sync.Mutex
Заголовок раздела «1.1. sync.Mutex»Mutex (Mutual Exclusion) — взаимное исключение. Только одна горутина одновременно может держать lock.
import "sync"
var ( mu sync.Mutex counter int)
func inc() { mu.Lock() defer mu.Unlock() counter++}Ключевые свойства:
- Zero value usable —
var mu sync.Mutexуже готов к работе, безnew()и инициализации. - Не recursive — повторный
Lock()той же горутиной = deadlock (не panic, просто зависание!). - Не привязан к горутине — формально можно
Unlock()из другой горутины (UB по Go Memory Model, но компилится; в runtime будет panic при попытке Unlock не залоченного mutex). - Не копируется — после первого использования копирование Mutex — бага (vet поймает: “passes lock by value”).
Best practice:
mu.Lock()defer mu.Unlock()// ... critical sectiondefer гарантирует Unlock даже при panic.
1.2. sync.RWMutex
Заголовок раздела «1.2. sync.RWMutex»Read-Write Mutex — допускает много читателей или одного писателя.
var ( rw sync.RWMutex cache = make(map[string]string))
func get(key string) string { rw.RLock() defer rw.RUnlock() return cache[key]}
func set(key, val string) { rw.Lock() defer rw.Unlock() cache[key] = val}RLock()/RUnlock()— для чтения (много одновременных).Lock()/Unlock()— для записи (эксклюзив).
Когда использовать?
- Чтений в 10+ раз больше, чем записей.
- Critical section короткий → накладные расходы RWMutex окупаются.
⚠️ Если writes часто блокируют reads (writer-starvation prevention) — может быть медленнее обычного Mutex. Замеряйте!
1.3. sync.WaitGroup
Заголовок раздела «1.3. sync.WaitGroup»WaitGroup — счётчик “сколько горутин ещё работает”. Wait() блокирует, пока счётчик не станет 0.
var wg sync.WaitGroupfor i := 0; i < 5; i++ { wg.Add(1) // ВАЖНО: до go! go func(i int) { defer wg.Done() // = Add(-1) time.Sleep(time.Duration(i) * time.Second) fmt.Println(i) }(i)}wg.Wait() // блок, пока все 5 не вызовут Donefmt.Println("all done")Правила:
Add(n)до запуска горутины (если в горутине — race сWait).Done()— обычно черезdeferдля гарантии.- Counter не должен уйти в минус (Done без Add → panic).
- Не переиспользуйте WaitGroup между раундами
Waitбез аккуратности (можно, но Add в момент Wait — race).
1.4. sync.Once
Заголовок раздела «1.4. sync.Once»Once — гарантирует один вызов функции, даже при concurrent invocations.
var ( once sync.Once instance *Singleton)
func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{...} }) return instance}Do(f)— выполнитfровно один раз за время жизниOnce. Все остальные вызовы (даже из других горутин) дождутся завершения первогоfи ничего больше не сделают.- Если
fпаникует —Onceсчитает себя “выполненной” (повторных вызовов не будет). - Идеально для lazy initialization singleton-ов.
С Go 1.21 добавлены sync.OnceFunc, sync.OnceValue, sync.OnceValues — удобные обёртки:
init := sync.OnceFunc(func() { fmt.Println("init once")})init() // печатаетinit() // ничего
getCfg := sync.OnceValue(func() *Config { return loadConfig() // вызовется один раз})cfg := getCfg() // loadcfg = getCfg() // тот же объект1.5. sync.Cond
Заголовок раздела «1.5. sync.Cond»Conditional variable — горутины могут “ждать” события и быть разбуженными.
var ( mu sync.Mutex cond = sync.NewCond(&mu) queue []Item)
func consumer() { mu.Lock() for len(queue) == 0 { cond.Wait() // отпускает mu, паркуется, пробудившись снова берёт mu } item := queue[0] queue = queue[1:] mu.Unlock() process(item)}
func producer(item Item) { mu.Lock() queue = append(queue, item) cond.Signal() // или cond.Broadcast() для всех mu.Unlock()}Wait()— отпускает mutex, паркует горутину. Пробуждение — снова берёт mutex.Signal()— будит одну ждущую горутину.Broadcast()— будит всех.
⚠️ В 99% случаев джуну Cond не нужен — каналы решают то же самое идиоматичнее. Знайте, что он существует, но избегайте.
1.6. sync.Pool
Заголовок раздела «1.6. sync.Pool»Pool — пул объектов для переиспользования. Снижает нагрузку на GC и аллокатор.
var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) },}
func handle() { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset() // ВАЖНО: очистить bufPool.Put(buf) }() // ... используем buf}Get()— берёт объект из пула. Если пул пуст — вызываетNew().Put(x)— возвращает объект в пул.New— фабрика для создания, если пул пуст.
Use cases: буферы (bytes.Buffer, []byte), reusable parsers, request-scoped objects.
⚠️ Подвохи:
- GC чистит пул: с Go 1.13 при сборе GC удаляет половину объектов из пула. Поэтому Pool — только для временных объектов, не для долгоживущих singleton-ов.
- Не для типов с состоянием, которое нельзя обнулить (Reset).
- Не для пула соединений (используйте
database/sqlили специализированные библиотеки).
1.7. sync.Map
Заголовок раздела «1.7. sync.Map»Map с поддержкой concurrent доступа. Альтернатива map + sync.RWMutex.
var m sync.Mapm.Store("k", "v")v, ok := m.Load("k")m.Delete("k")m.Range(func(key, value any) bool { fmt.Println(key, value) return true // продолжать итерацию})Когда использовать?
- Запись редкая, чтения частые на разных ключах.
- Append-only кеш.
⚠️ Когда не использовать?
- Любые сценарии с частой записью одного ключа → обычный
map + RWMutexбыстрее. - Если нужны
range,len, типобезопасность → обычный map лучше.
sync.Map использует нетипизированный интерфейс (any/interface{}). С Go 1.18+ можно сделать обёртку с генериками самому.
1.8. atomic пакет
Заголовок раздела «1.8. atomic пакет»Атомарные операции — без блокировок, на уровне CPU инструкций.
До Go 1.19 (функции):
Заголовок раздела «До Go 1.19 (функции):»import "sync/atomic"
var counter int64atomic.AddInt64(&counter, 1)v := atomic.LoadInt64(&counter)atomic.StoreInt64(&counter, 0)swapped := atomic.CompareAndSwapInt64(&counter, 0, 100)С Go 1.19 (типы):
Заголовок раздела «С Go 1.19 (типы):»var counter atomic.Int64counter.Add(1)v := counter.Load()counter.Store(0)swapped := counter.CompareAndSwap(0, 100)Доступные типы: atomic.Bool, atomic.Int32, atomic.Int64, atomic.Uint32, atomic.Uint64, atomic.Uintptr, atomic.Pointer[T] (Go 1.19+).
Когда atomic, а когда mutex?
Заголовок раздела «Когда atomic, а когда mutex?»| Задача | Лучше |
|---|---|
| Счётчик | atomic |
| Read-modify-write одного значения | atomic CAS |
| Защита нескольких полей одновременно | Mutex |
| Защита коллекции (map, slice) | Mutex / sync.Map |
| Защита invariant между полями | Mutex |
Правило: atomic — для одного значения. Если нужно атомарно изменить два числа — нужен Mutex (или CAS на одну структуру через unsafe.Pointer).
Memory ordering
Заголовок раздела «Memory ordering»Все atomic-операции в Go — sequentially consistent (SC). Это самая сильная гарантия: все горутины видят операции в одном глобальном порядке.
Это удобно (нет случайных race), но дороже более слабых моделей (acquire/release, relaxed) — например, в C++/Rust.
2. Под капотом
Заголовок раздела «2. Под капотом»2.1. sync.Mutex внутри
Заголовок раздела «2.1. sync.Mutex внутри»Mutex в Go (src/sync/mutex.go) — это структура:
type Mutex struct { state int32 // битовое поле: locked, woken, starving, waitersCount sema uint32 // семафор для парковки/пробуждения горутин}Fast path — uncontended Lock через одну CAS-операцию:
// Псевдокод:if atomic.CompareAndSwap(&m.state, 0, locked) { return // успех, lock взят}Slow path — если CAS не сработал, идём в medium-path (active spinning несколько раз), потом в slow-path (паркуемся на семафоре).
Starvation mode
Заголовок раздела «Starvation mode»С Go 1.9 у Mutex два режима:
- Normal — fast. Новые горутины могут “обогнать” уже ждущих в очереди (lock прыгает к “горячей” горутине). Это хорошо для throughput, но плохо для fairness.
- Starvation — если горутина ждёт lock больше 1 ms, Mutex переходит в режим, где lock гарантированно передаётся первой в очереди. Это снижает throughput, но обеспечивает fairness.
После того как goroutine получает lock и waitersCount = 0 (или прошло <1 ms), mutex возвращается в normal mode.
2.2. sync.RWMutex внутри
Заголовок раздела «2.2. sync.RWMutex внутри»type RWMutex struct { w Mutex // mutex для writers writerSem uint32 readerSem uint32 readerCount int32 // число активных reader-ов readerWait int32 // сколько readers ждут writer}Lock()— берётw, ставитreaderCountв negative число, ждёт пока активные reader-ы не закончат.RLock()— атомарно инкрементитreaderCount. Если оно negative (writer ждёт) — паркуется.Unlock()— возвращаетreaderCountназад в positive, пробуждает reader-ов.
⚠️ RWMutex предпочитает writer-ов (no writer starvation), что иногда удивляет.
2.3. sync.Once внутри
Заголовок раздела «2.3. sync.Once внутри»type Once struct { done uint32 // флаг "выполнено" m Mutex // для slow path}
func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) }}
func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() }}Fast path — атомарная проверка флага (без mutex), быстро. Slow path — mutex для гарантии единственного выполнения.
⚠️ Подвох: до Go 1.21 был баг — если f() паникует, done всё равно ставится в 1. Поведение интенциональное. С Go 1.21 это явно документировано.
2.4. sync.WaitGroup внутри
Заголовок раздела «2.4. sync.WaitGroup внутри»type WaitGroup struct { noCopy noCopy state1 uint64 // upper 32 bits: counter, lower 32 bits: waiters state2 uint32 // semaphore}Add(n)— атомарно прибавляет к counter-у.Done()=Add(-1).Wait()— атомарно проверяет counter; если 0 — сразу возвращает. Иначе инкрементит waiters, паркуется на семафоре.- Когда counter становится 0 — освобождается семафор, все waiters просыпаются.
2.5. sync.Pool внутри
Заголовок раздела «2.5. sync.Pool внутри»type Pool struct { noCopy noCopy local unsafe.Pointer // per-P структура localSize uintptr victim unsafe.Pointer // прошлый цикл GC victimSize uintptr New func() any}Per-P storage
Заголовок раздела «Per-P storage»У каждого P (см. GMP в файле 10) есть локальный пул объектов. Get/Put в основном идут без блокировки:
Get()— сначала пытается взять из своего P. Если пусто — пытается украсть у других P. Если ни у кого нет — вызываетNew.Put(x)— кладёт в свой P.
Локально структура — это shared queue (private + shared). Put идёт в private. Get — сначала private, потом shared head, потом ворует с shared tail других P.
GC и victim cache
Заголовок раздела «GC и victim cache»С Go 1.13 при GC:
- То, что было в
local, перемещается вvictim. - Что было в
victim— выкидывается. - На следующем GC шаге всё повторяется.
Это значит: объект, помещённый в пул, живёт минимум 1 GC цикл, максимум 2. После — будет собран.
Зачем victim? Чтобы избежать “холодного старта” сразу после GC — некоторые недавние объекты остаются доступными.
2.6. atomic внутри
Заголовок раздела «2.6. atomic внутри»Atomic-операции компилируются в специальные CPU инструкции:
| Операция в Go | x86 инструкция |
|---|---|
| atomic.LoadInt64 | MOVQ (aligned 64-bit) |
| atomic.StoreInt64 | XCHGQ или MOVQ + MFENCE |
| atomic.AddInt64 | LOCK XADDQ |
| atomic.CompareAndSwap | LOCK CMPXCHGQ |
LOCK prefix — заставляет CPU гарантировать атомарность операции (cache coherence через MESI протокол).
⚠️ Подвох с alignment: на 32-bit платформах (например, 32-bit ARM) atomic.LoadInt64 требует, чтобы поле было выровнено по 8 байтам. Если структура содержит другие поля до — поле может оказаться не выровнено → panic в runtime.
Best practice: int64/uint64 поле — всегда первым в struct, или используйте atomic.Int64 (с Go 1.19+, который сам обеспечивает alignment).
type Counter struct { n int64 // первым! иначе на 32-bit может крашить name string}С atomic.Int64:
type Counter struct { n atomic.Int64 // сам обеспечивает alignment name string}3. Тонкие моменты / Gotchas
Заголовок раздела «3. Тонкие моменты / Gotchas»3.1. Mutex не recursive
Заголовок раздела «3.1. Mutex не recursive»mu.Lock()mu.Lock() // deadlock! Та же горутина — то же mutex.⚠️ Это деадлок (горутина залипает навсегда), не паника. В отличие от Java’s ReentrantLock, Go-шный Mutex не reentrant.
Если кажется, что нужен recursive lock — это запах плохой архитектуры. Рефакторите код.
3.2. Передача Mutex по значению
Заголовок раздела «3.2. Передача Mutex по значению»type Counter struct { mu sync.Mutex n int}
func (c Counter) Inc() { // ⚠️ value receiver! c.mu.Lock() defer c.mu.Unlock() c.n++ // мутирует копию, не оригинал}⚠️ Передача Counter по значению копирует и mutex. Это race (две горутины с двумя копиями mutex-а — нет защиты).
Правило: value-receiver на типе с sync.Mutex — всегда баг. go vet это поймает: “passes lock by value”.
Фикс — pointer receiver:
func (c *Counter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.n++}3.3. WaitGroup.Add внутри горутины
Заголовок раздела «3.3. WaitGroup.Add внутри горутины»var wg sync.WaitGroupfor i := 0; i < 5; i++ { go func() { wg.Add(1) // ⚠️ Race с wg.Wait()! defer wg.Done() work() }()}wg.Wait() // может вернуться сразу, если ни одна горутина не успела AddФикс: Add до go.
3.4. WaitGroup переиспользование
Заголовок раздела «3.4. WaitGroup переиспользование»wg.Add(5)// ... горутиныwg.Wait()
wg.Add(5) // ⚠️ Race! Wait может ещё разблокировать предыдущих waiters⚠️ Технически можно переиспользовать WaitGroup, но в Go Memory Model Add после Wait — race. Best practice: на каждый “раунд” — новый WaitGroup.
3.5. Mutex после копирования через value
Заголовок раздела «3.5. Mutex после копирования через value»m := sync.Mutex{}m.Lock()m2 := m // ⚠️ копирование mutex в залоченном состоянииm.Unlock()m2.Unlock() // panic: unlock of unlocked mutexgo vet поймает.
3.6. defer mu.Unlock() vs explicit
Заголовок раздела «3.6. defer mu.Unlock() vs explicit»func foo() { mu.Lock() defer mu.Unlock() longComputation() // mu держится всё время}В hot path defer стоит ~50ns. Если critical section короткий — может быть существенно. Тогда — explicit:
func foo() { mu.Lock() longComputation() mu.Unlock()}⚠️ Но! Если longComputation может паниковать — explicit оставит mutex залоченным навсегда. Используйте defer, если есть риск panic.
3.7. Atomic alignment
Заголовок раздела «3.7. Atomic alignment»На 32-bit платформах (32-bit ARM, 32-bit x86):
type Bad struct { a int32 // 4 bytes b int64 // 8 bytes — может быть не выровнен!}
var x Badatomic.AddInt64(&x.b, 1) // panic на 32-bit ARMФикс — int64 первым:
type Good struct { b int64 // выровнен по 8 байт a int32}Или использовать atomic.Int64 (Go 1.19+) — компилятор сам обеспечит.
3.8. RWMutex.RLock не рекурсивен
Заголовок раздела «3.8. RWMutex.RLock не рекурсивен»rw.RLock()rw.RLock() // ⚠️ может вызвать deadlock!Если между двумя RLock-ами кто-то вызвал Lock, второй RLock залипнет навсегда (RWMutex даёт приоритет writer-у).
3.9. sync.Pool с указателями
Заголовок раздела «3.9. sync.Pool с указателями»type Buf struct { data []byte}
var pool = sync.Pool{ New: func() any { return &Buf{data: make([]byte, 4096)} },}
func use() { b := pool.Get().(*Buf) defer pool.Put(b) // ... используем b.data // ⚠️ Если data сохранили где-то ещё, после Put — race!}Правило: после Put — забудьте про объект, не используйте его ссылку.
3.10. atomic.Value для interface
Заголовок раздела «3.10. atomic.Value для interface»Если нужно атомарно подменять значение разных типов — atomic.Value:
var v atomic.Valuev.Store(&Config{Version: 1})// ...cfg := v.Load().(*Config)⚠️ Все Store должны быть одного типа. Иначе panic.
С Go 1.19+ для типобезопасности предпочитайте atomic.Pointer[Config].
4. Производительность
Заголовок раздела «4. Производительность»4.1. Бенчмарки (примерные, x86-64)
Заголовок раздела «4.1. Бенчмарки (примерные, x86-64)»| Операция | Время |
|---|---|
| Mutex Lock/Unlock (uncontended) | ~15 ns |
| Mutex Lock/Unlock (contended) | 200 ns - μs |
| RWMutex RLock/RUnlock | ~25 ns |
| atomic.AddInt64 | ~5 ns |
| atomic CAS | ~5-10 ns |
| channel send/recv | 30-70 ns |
4.2. Когда mutex быстрее канала?
Заголовок раздела «4.2. Когда mutex быстрее канала?»Почти всегда для simple counter / shared state. Канал — overhead на парковку, scheduler, hchan.
4.3. Lock granularity
Заголовок раздела «4.3. Lock granularity»- Coarse locking — один большой mutex на всю структуру. Просто, но scales плохо.
- Fine-grained locking — много маленьких mutex-ов (например, на bucket в map). Быстрее, но сложнее.
Compromise: sharded map — массив [N]struct{ mu sync.Mutex; data map[K]V }. Каждый ключ попадает в bucket по hash. Параллелизм до N.
4.4. False sharing
Заголовок раздела «4.4. False sharing»⚠️ Подвох на многоядерных: если два mutex/atomic находятся в одной cache line (64 bytes на x86), они мешают друг другу.
Фикс — padding:
type Counter struct { n int64 _ [56]byte // padding до 64 байт}
var counters [4]Counter // каждый в своей cache line4.5. sync.Pool — когда выгодно
Заголовок раздела «4.5. sync.Pool — когда выгодно»Замеряйте! Pool помогает, если:
- Объект большой (kB+).
- Создание объекта дорогое.
- Используется в hot path.
Иначе overhead pool-а > benefit.
5. Race detector
Заголовок раздела «5. Race detector»Go runtime имеет встроенный race detector, который ловит data races во время выполнения.
go build -race ./...go test -race ./...go run -race main.go⚠️ Замедляет программу в 5-10x, увеличивает память. Только для dev и CI, не для prod.
5.1. Что такое data race
Заголовок раздела «5.1. Что такое data race»Data race — два или более горутины обращаются к одной и той же памяти, минимум одна — на запись, без синхронизации.
var x intgo func() { x = 1 }()go func() { x = 2 }() // racefmt.Println(x) // UBRace detector это поймает. Без него — x может быть 1, 2, частично 1 и частично 2 (на платформах с не-atomic word-size write).
5.2. Использование в CI
Заголовок раздела «5.2. Использование в CI»Добавьте в pipeline:
go test -race -timeout 60s ./...Если find race — фейл билда. Обязательно для любого Go-проекта.
5.3. Чего race detector НЕ ловит
Заголовок раздела «5.3. Чего race detector НЕ ловит»- Race, который не случился в данном запуске (test coverage важен!).
- Logical races (race по бизнес-логике, без data race).
- Deadlocks (есть отдельный детектор только для тривиальных случаев — когда все горутины заблокированы).
6. Типичные вопросы на собесе
Заголовок раздела «6. Типичные вопросы на собесе»-
sync.Mutex recursive? Нет. Повторный Lock в той же горутине = deadlock.
-
Что произойдёт при Unlock без Lock? Panic: “unlock of unlocked mutex”.
-
Zero value sync.Mutex usable? Да.
var mu sync.Mutexготов к работе. -
Можно ли копировать sync.Mutex? Нет.
go vetпоймает. Передавайте по указателю. -
Что делает sync.RWMutex? Много reader-ов одновременно или один writer.
-
Когда RWMutex медленнее обычного Mutex? Когда писем много (writers блокируют reader-ов, есть накладные на trackirovku state).
-
Что не так с этим?
wg.Add(1)go func() {defer wg.Done()wg.Add(1) // ⚠️}()Add после старта горутины — race с Wait.
-
Что такое sync.Once? Гарантирует одно выполнение функции, даже из разных горутин.
-
Что произойдёт, если функция в Once.Do паникует? Once считается “выполненной”, повторных вызовов не будет.
-
Зачем sync.Pool? Переиспользование объектов для снижения нагрузки на GC.
-
Что делает GC с sync.Pool? Чистит примерно половину объектов каждый GC цикл (через victim cache).
-
Atomic vs Mutex — когда что? Atomic — для одного значения. Mutex — для нескольких полей / коллекций.
-
Что такое CAS? Compare-And-Swap. Атомарная операция: if value == old { value = new }. Используется для lock-free алгоритмов.
-
Memory ordering в Go atomic? Sequentially consistent. Все операции упорядочены глобально.
-
Зачем atomic.Int64 вместо atomic.AddInt64?
- Типобезопасность. 2) Автоматический alignment на 32-bit платформах.
-
Что такое starvation в Mutex? Когда горутина ждёт lock слишком долго. С Go 1.9 если ждёт >1ms, Mutex переходит в starvation mode и lock гарантированно передаётся следующей в очереди.
-
Race detector — что ловит? Data race: concurrent access to same memory, at least one write, without synchronization.
-
Какой overhead у race detector? 5-10x по CPU, 5-10x по памяти.
-
Можно ли deploy в prod с -race? Нет, slow и потребляет память.
-
sync.Map vs map+RWMutex? sync.Map — если запись на разные ключи (insert-mostly). map+RWMutex — если частые обновления одних и тех же ключей.
-
Когда defer mu.Unlock() плох? В hot path (50 ns overhead) и когда critical section короче, чем сам defer.
-
Как реализовать reentrant mutex? Никак идиоматично. Если очень нужно — отслеживать goroutine ID (но Go не даёт публичного API). Refactor код.
-
Что произойдёт при двойном wg.Done()? Counter уйдёт ниже нуля → panic.
-
Как корректно использовать sync.Pool с buffer-ами? Get → use → Reset → Put. После Put не трогать.
-
Что такое false sharing? Когда разные переменные на разных горутинах попадают в одну cache line → лишний bus traffic. Фикс — padding.
7. Practice
Заголовок раздела «7. Practice»Задача 1: Безопасный счётчик
Заголовок раздела «Задача 1: Безопасный счётчик»Реализуйте SafeCounter с методами Inc, Dec, Value. Три варианта: с Mutex, с RWMutex (Value через RLock), с atomic. Замерьте бенчмарками.
Задача 2: Sharded map
Заголовок раздела «Задача 2: Sharded map»Реализуйте ShardedMap[K comparable, V any] с 32 shard-ами. Каждый shard — map[K]V + sync.RWMutex. Метод Get(k), Set(k, v), Delete(k).
Задача 3: Worker pool с graceful shutdown
Заголовок раздела «Задача 3: Worker pool с graceful shutdown»Используя WaitGroup и канал done, реализуйте worker pool, который завершается, когда все задачи выполнены, и можно остановить через context.
Задача 4: Once + Once
Заголовок раздела «Задача 4: Once + Once»var once sync.Oncego once.Do(func() { time.Sleep(time.Second); fmt.Println("a") })go once.Do(func() { fmt.Println("b") })time.Sleep(2 * time.Second)Что напечатает? Когда напечатает “b”? (Подсказка: вторая горутина дождётся первой.)
Задача 5: Найти race
Заголовок раздела «Задача 5: Найти race»var ( cache = map[string]string{} mu sync.Mutex)
func Get(key string) string { if v, ok := cache[key]; ok { // ⚠️ read без lock return v } mu.Lock() defer mu.Unlock() if v, ok := cache[key]; ok { return v } v := compute(key) cache[key] = v return v}Запустите с -race. Поймайте race. Исправьте (через RWMutex.RLock — но тоже надо аккуратно с double-check).
Задача 6: sync.Pool benchmark
Заголовок раздела «Задача 6: sync.Pool benchmark»var pool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
func BenchmarkNoPool(b *testing.B) { for i := 0; i < b.N; i++ { buf := new(bytes.Buffer) buf.WriteString("hello") _ = buf }}
func BenchmarkWithPool(b *testing.B) { for i := 0; i < b.N; i++ { buf := pool.Get().(*bytes.Buffer) buf.WriteString("hello") buf.Reset() pool.Put(buf) }}Запустите go test -bench=.. Сравните.
Задача 7: CAS-based stack
Заголовок раздела «Задача 7: CAS-based stack»Реализуйте lock-free stack через atomic.Pointer[node]. Методы Push(v) и Pop(). Используйте CAS в цикле.
8. Источники
Заголовок раздела «8. Источники»- sync пакет — https://pkg.go.dev/sync — официальная документация.
- Go Memory Model — https://go.dev/ref/mem — happens-before, atomics, channels.
- Dmitry Vyukov, “Go race detector internals” — https://github.com/google/sanitizers/wiki/ThreadSanitizerAlgorithm
- sync/atomic пакет — https://pkg.go.dev/sync/atomic
- The Go Programming Language, Donovan & Kernighan — chapter 9 (concurrency with shared variables).
- sync/mutex.go — исходник: https://github.com/golang/go/blob/master/src/sync/mutex.go