---
title: "Мьютексы и общее состояние"
description: "sync.Mutex и RWMutex в Go: как мьютекс закрывает гонку данных, критическая секция, defer Unlock, границы секции, race detector -race и тизер sync/atomic."
course: "Продвинутые возможности Go: от замыканий до рефлексии"
url: "https://golangmaster.ru/courses/go-advanced/lessons/08-muteksy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 22
---

# Мьютексы и общее состояние

*Курс «Продвинутые возможности Go: от замыканий до рефлексии», урок 8 из 25. ~22 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-advanced/lessons/08-muteksy/*

мьютекс go — запрос, который набирают сразу после первой настоящей гонки: счётчик обещал 2000, а насчитал 1682, и каждый запуск даёт новое число. В уроке про горутины мы увидели эту рану вживую и назвали лекарство — sync.Mutex. Теперь лечим по всей программе: критическая секция и defer Unlock, границы секции, RWMutex для читателей, race detector — детектор гонок, который ловит злодеяние по горячим следам, и честные границы замка: что мьютекс умеет, а что нет. Все примеры запускаются прямо на странице.

### Мьютекс в Go: общий доступ под замком

В [уроке про горутины](/courses/go-advanced/lessons/04-gorutiny/) мы одной фразой поставили диагноз, который стоит повторить здесь: `counter++` из нескольких горутин без синхронизации — это гонка данных, приращения затирают друг друга, и счётчик из двух горутин по тысяче прибавлений выдал у нас 1682, 1877 и один раз честные 2000. Лекарство было названо там же — **sync.Mutex** — и обещано «в уроке про мьютексы». Это и есть текущий урок: держать обещание и научиться защищать общее состояние так, чтобы итог перестал зависеть от жеребьёвки планировщика.

Слово mutex — сокращение от mutual exclusion, «взаимное исключение». Идея механическая, как у санузла в самолёте: замок один, войти может один, следующий стучится, пока первый не выйдет. В Go это значение пакетного типа sync.Mutex с двумя методами: `mu.Lock()` — войти и держать замок, `mu.Unlock()` — выйти и освободить. Всё, что написано между ними, называется **критической секцией**: участком, внутри которого в каждый момент времени находится не больше одной горутины.

Самое приятное свойство — нулевое значение готово к работе: `var mu sync.Mutex` не требует конструктора, инициализации и настроек. Замок только что создан — и уже можно Lock. За это нулевое значение Go хвалят по всему стандартному пакету sync: WaitGroup из [прошлого урока](/courses/go-advanced/lessons/07-waitgroup/) устроен так же.

### Критическая секция: замок защищает код, а не переменную

Первое, что надо честно понять: мьютекс ничего не знает о ваших переменных. Он не «прикреплён» к счётчику, не отслеживает записи и не умеет блокировать данные. Он защищает **участок кода**: кто успел взять замок — работает; остальные стоят на `mu.Lock()` и ждут своей очереди. Какой именно участок окажется критическим — решаете вы, расставляя Lock и Unlock, и от этого решения зависит буквально всё: поставили не вокруг того — защита мнимая.

Вернёмся к инкременту. `counter++` выглядит одной операцией, но на уровне машины это три шага: прочитать значение, прибавить единицу, записать обратно. Две горутины, выполняющие эти шаги вразнобой, теряют обновления: обе прочитали одно и то же, обе записали «своё» — одно приращение исчезло. Критическая секция превращает одновременность в очередь: шаги одной горутины закончились — только тогда начинается вторая.

Отсюда и правило расстановки: замок берут **непосредственно перед** доступом к общему состоянию и отдают **сразу после** него. Между Lock и Unlock должно жить только то, что работает с общими данными, — ни сна, ни сети, ни тяжёлых вычислений, до которых замок не имеет отношения. Почему «сразу» — отдельная тема ниже; сначала посмотрим на замок в деле: тот же счётчик, что пострадал в четвёртом уроке, теперь под защитой.

### Проверьте себя

**Вопрос:** Что именно делает sync.Mutex, когда две горутины встречаются на общем counter++?

- Делает counter++ одной атомарной инструкцией процессора
- ✅ Ничего не меняет в самой операции — он выстраивает горутины в очередь: между Lock и Unlock в каждый момент находится одна
- Копирует счётчик каждой горутине, а в конце складывает копии
- Блокирует вторую горутину навсегда после первого конфликта

Мьютекс защищает участок кода, а не переменную: он ничего не знает о counter++ и не делает операцию атомарной. Атомарность — следствие дисциплины: внутри секции в каждый момент одна горутина, значит три шага инкремента не перемешиваются. Поставили Lock и Unlock не вокруг того кода — защиты нет, хотя замок и «работает».

### Счётчик под замком: десять горутин, тысяча прибавлений каждая

Запустите несколько раз: всегда 10000 и всегда «потеряно приращений: 0» — десять горутин по тысяче прибавлений, ни одно не исчезло, при любом жребии планировщика. Эксперимент: уберите mu.Lock() и mu.Unlock() — вернётся гонка из четвёртого урока, и итог перестанет быть гарантированным. Заметьте дисциплину: counter++ стоит между Lock и Unlock — внутри секции ровно то, что работает с общими данными, и ничего больше.

```go
package main

import (
        "fmt"
        "sync"
)

func main() {
        var mu sync.Mutex // нулевое значение готово к работе
        var wg sync.WaitGroup
        counter := 0

        for i := 0; i < 10; i++ {
                wg.Add(1)
                go func(i int) {
                        defer wg.Done()
                        for j := 0; j < 1000; j++ {
                                mu.Lock() // вход в критическую секцию
                                counter++ // три шага машины — теперь по очереди
                                mu.Unlock() // выход: замок не тащим дальше
                        }
                }(i)
        }

        wg.Wait() // бригада из седьмого урока дожидается всех
        fmt.Println("счётчик:", counter)
        fmt.Println("потеряно приращений:", 10000-counter)
}
```

### defer mu.Unlock(): страховка, знакомая по WaitGroup

Если дисциплина «первая строка после Lock» показалась вам знакомой — не совпадение: это та же грамматика, что `defer wg.Done()` из прошлого урока. Обычная строка `mu.Unlock()` в конце функции обещает, что исполнение дойдёт до неё по прямой. Не дойдёт по двум причинам, обе уже изученные. Ранний выход: `if` с `return` посреди функции — и замок остался при вас навсегда; каждый, кто стучится в `Lock`, будет ждать вечно. Паника: любое исключительное событие внутри секции прерывает функцию — и `Unlock` обычной строкой не исполняется; замок умер вместе с горутиной, а с ним и всё общее состояние: следующий `Lock` блокируется навсегда.

`defer mu.Unlock()` в первой же строке после `Lock` снимает оба сценария: отложенный вызов выполняется при разворачивании стека — при любом `return` и при панике. Панику замок переживёт: если её перехватит recover-обёртка выше, программа продолжит работу, и замок уже свободен. Это стоит прочувствовать: пара `mu.Lock()` / `defer mu.Unlock()` — самая частая строчка concurrency-кода на Go, и пишут её почти машинально, как открывающую и закрывающую скобку.

Тонкость, о которой стоит знать заранее: страховка от забытого Unlock — defer; а вот страховки от **двойного** Unlock в языке нет, и это самая громкая ошибка семейства. Разберём её честно ниже, после predict-а про забытый замок — там контраст виден лучше всего.

### Паттерн Counter: структура со встроенным мьютексом

Запустите несколько раз: итог 55 — сумма чисел от 1 до 10, независимая от порядка, в котором горутины добрались до замка. Обратите внимание на Value(): чтение общего состояния — такой же доступ, как запись, и тоже стоит в критической секции. Эксперимент: уберите замок из Inc, оставив в Value, — пишете без защиты, читаете с защитой; гонка никуда не делась, потому что замок защищает данные, а не методы.

```go
package main

import (
        "fmt"
        "sync"
)

// Counter — общий счётчик со встроенной защитой.
// Мьютекс живёт рядом с полем, которое охраняет.
type Counter struct {
        mu    sync.Mutex
        total int
}

// Inc прибавляет n: критическая секция на всю правку
func (c *Counter) Inc(n int) {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.total += n
}

// Value читает итог — чтение тоже доступ, тоже под замком
func (c *Counter) Value() int {
        c.mu.Lock()
        defer c.mu.Unlock()
        return c.total
}

func main() {
        var c Counter // нулевое значение готово: mu открыт, total = 0
        var wg sync.WaitGroup

        for i := 0; i < 10; i++ {
                wg.Add(1)
                go func(i int) {
                        defer wg.Done()
                        c.Inc(i + 1) // каждый вносит свой вклад
                }(i)
        }

        wg.Wait()
        fmt.Println("итог:", c.Value())
}
```

### Инкапсуляция: мьютекс не выносят наружу

Counter из runner-а — не просто учебная фигня, а канонический паттерн стандартной библиотеки: общий тип со встроенным замком. Правила паттерна стоит зафиксировать. Мьютекс и данные, которые он защищает, живут **в одной структуре** — глядя на тип, вы сразу видите, что состояние общее и как оно защищено. Поле `mu` не экспортируется: строчная буква делает его приватным, и никто снаружи не сможет ни взять замок без ведома методов, ни обойти его. Доступ — только через методы типа, каждый из которых сам открывает и закрывает секцию.

Так устроены типы по всему стандартному пакету sync и вокруг него: у sync.WaitGroup внутри мьютекс, у счётчиков статистики, у кэшей, у пулов соединений. Имя метода `Value()` на чтение — тоже каноника: чтение под `Lock` выглядит перестраховкой ровно до первого дня, когда кто-то добавляет вторую горутину чтения и третью записи; тогда перестраховка уже встроена.

И осторожность, переходящая из прошлого урока: структуру с мьютексом **не копируют**. Копия замка — независимый замок, и защита расщепляется пополам: половина горутин стоит у одного замка, половина у другого, а состояние у них уже разное. Компилятор молчит, но `go vet` помечает копирование любого типа с методами Lock/Unlock всё той же фразой, что и у WaitGroup: «passes lock by value». Поэтому методы таких структур объявляют на указателе — `func (c *Counter) Inc` — и передают структуру только `&c`.

### Границы секции: не носить замок дольше нужного

Замок корректен, даже когда секция слишком широкая — и это коварно. Возьмите Lock перед циклом на тысячу итераций и Unlock после: итог тот же честный 2000, программа правильная. Что сломалось? Скорость. Пока одна горутина держит замок на весь цикл, остальные стоят на Lock и не работают, даже если им нужно совсем другое. Корректность от ширины секции не зависит; параллельность — зависит напрямую.

Правило минимализма: секция должна покрывать доступ к общим данным и заканчиваться. Приращения посчитали — Unlock — дальше функция может хоть спать, хоть писать в сеть: замок уже у вас не в руках. Хрестоматийный анти-паттерн — I/O внутри секции: сетевой запрос под замком на полсекунды останавливает всех желающих на эти полсекунды, хотя общие данные вы правили первые несколько микросекунд. Замок — не периметр функции, а периметр доступа к данным.

С тонкой секцией связана и обратная ловушка, о которой говорят реже: **чужой код внутри секции**. Вызов метода чужой структуры, держащего тот же замок, — само-дедлок: замок в Go не реентерабельный, второй Lock из той же горутины ждёт сам себя вечно. Поэтому в секции — свои данные и свои строки; сложные взаимодействия выносятся за Unlock.

### Правила гигиены мьютекса

- Замок и данные живут вместе: мьютекс — поле той же структуры, что и состояние, которое он защищает; глядя на тип, видно, что общее и чем закрыто.
- `mu.Lock()` — непосредственно перед доступом, `defer mu.Unlock()` — первой строкой после: замок освобождается при любом выходе, включая ранний return и панику.
- Внутри секции — только работа с общими данными: ни сети, ни сна, ни вызовов чужого кода, который может взять тот же замок — само-дедлок.
- Один мьютекс на единицу состояния, а не один на всё приложение; если замков несколько — берите их в одном и том же порядке во всём коде, иначе получите дедлок из перекрёстного ожидания.
- Мьютекс не копируют: методы на указателе, передача по &; копия замка — независимый замок, а go vet пометит «passes lock by value».

### Тот же счётчик из четвёртого урока — теперь с замком

```go
package main

import (
        "fmt"
        "sync"
)

func main() {
        var mu sync.Mutex
        var wg sync.WaitGroup
        counter := 0

        for i := 0; i < 2; i++ {
                wg.Add(1)
                go func() {
                        defer wg.Done()
                        for j := 0; j < 1000; j++ {
                                mu.Lock()
                                counter++
                                mu.Unlock()
                        }
                }()
        }

        wg.Wait()
        fmt.Println("счётчик:", counter)
}
```

**Что выведет программа?**

- Случайное число между 1600 и 2000: замок на инкремент не влияет
- ✅ Ровно 2000: обе горутины прошли через замок, ни одно приращение не потеряно
- 0: counter, изменённый в горутинах, невидим main
- Паника: два Lock на одном мьютексе

Это та же сцена из урока про горутины, где счётчик давал 1682 и 1996, — но каждая тройка шагов читай-прибавь-запиши заключена в критическую секцию. Пары Lock/Unlock выстраивают горутины в очередь, приращения перестают затирать друг друга, и итог детерминирован: 2000 при каждом запуске. Два Lock из разных горутин — не паника, а нормальная очередь: паника была бы при двойном Unlock.

### RWMutex: читатели в параллель

Мьютекс честен до суровости: он не различает, зачем вы вошли в секцию — прочитать или изменить. А ведь одновременных **чтений** можно не бояться: пока никто ничего не пишет, сколько угодно горутин могут читать общую строку, карту или слайс параллельно, ничего не ломая. Для этого в пакете sync живёт второй тип — **sync.RWMutex**: замок с двумя режимами.

Режим чтения — `rw.RLock()` и `rw.RUnlock()`: читателей внутри может быть сколько угодно одновременно, они друг другу не мешают. Режим записи — знакомые `rw.Lock()` / `rw.Unlock()`: писатель требует **наедине со всеми**. Пока писатель держит Lock, ни один новый RLock не входит; и наоборот — писатель ждёт, пока не выйдут все читатели, включая тех, кто пришёл до него. Именно поэтому RWMutex чуть дороже обычного: внутри счётчики читателей и более хитрая арифметика на атомарных операциях.

Правила гигиены те же, что у Mutex, плюс одно: каждый RLock требует RUnlock — и точно так же в defer. Забытый RUnlock не даст писателю когда-либо войти: счётчик читателей навсегда больше нуля. А попытка RUnlock без RLock — не паника, а **fatal error** на уровне рантайма, о ней честный разговор ниже.

### Проверьте себя ещё раз

**Вопрос:** Три горутины вызвали rw.RLock() одновременно, писателей нет. Сколько горутин сейчас находится внутри секций чтения?

- Ни одной: RLock всегда выстраивает очередь, как Lock
- Одна: RWMutex пропускает читателей строго по одному
- ✅ Все три: RLock пропускает сколько угодно читателей, пока нет писателя
- Зависит от числа ядер машины

R в RLock — read: замок чтения не исключает других читателей, счётчик читателей растёт, и все три горутины работают параллельно. Исключительность начинается с писателя: rw.Lock() ждёт выхода всех читателей и держит секцию наедине со всеми; новые RLock-и после писателя тоже ждут. Число ядер влияет на скорость, а не на допуск внутрь.

### RWMutex: бригада читателей и один писатель

Запустите несколько раз: читатели всегда видят один и тот же старый адрес — каждый писал только в свой слот, а WaitGroup из седьмого урока дождался бригады. Писатель идёт после Wait: rw.Lock() требует наедине со всеми, и обновление видит следующее чтение. Эксперимент: перенесите обновление config до wg.Wait() — получите гонку между записью и чтениями: часть читателей может увидеть старое, часть новое, а детектор гонок (о нём ниже) пометил бы сам доступ.

```go
package main

import (
        "fmt"
        "sync"
)

func main() {
        var rw sync.RWMutex
        config := "старый адрес" // общее состояние, читают многие

        readers := 3
        results := make([]string, readers) // слоты — как в седьмом уроке
        var wg sync.WaitGroup

        for i := 0; i < readers; i++ {
                wg.Add(1)
                go func(i int) {
                        defer wg.Done()
                        rw.RLock() // замок чтения: читателей не ограничиваем
                        defer rw.RUnlock()
                        results[i] = config // каждый пишет только в свой слот
                }(i)
        }
        wg.Wait()

        fmt.Println("читатели видят:", results[0], results[1], results[2])

        rw.Lock() // писатель: наедине со всеми
        config = "новый адрес"
        rw.Unlock()

        rw.RLock() // проверка чтением — тоже под замком
        fmt.Println("после обновления:", config)
        rw.RUnlock()
}
```

### Когда RWMutex действительно выигрывает

Соблазн заменить все мьютексы на RWMutex велик — «у нас же много чтения». Умерьте его честной инженерной оговоркой: RWMutex сложнее внутри, его операции дороже арифметики обычного Mutex, и выигрыш появляется не всегда. Он реален, когда чтения **длинные** (разбирают большую структуру, считают агрегат), а записи **редкие и короткие**: тогда параллельность читателей окупает накладные расходы. Если секции крошечные — пара наносекунд на доступ — накладные расходы на координацию читателей могут съесть весь выигрыш, и обычный Mutex окажется не хуже.

Второй практический нюанс — справедливость к писателю. Пока писатель ждёт, новые RLock-и не входят: режим «читатели никогда не ждут вечно» куплен ценой того, что поток читателей не блокирует писателя навсегда. Это правильное поведение, но знайте о нём, когда проектируете сильно read-heavy нагрузки: постоянный поток новых читателей не утопит писателя.

Стартовая эвристика для урока и для практики: по умолчанию берите обычный Mutex; RWMutex — осознанное решение под измеренную нагрузку с явным перекосом в чтение. А когда состояние — одно число, есть третий вариант, ещё более лёгкий, о нём в конце урока.

### Race detector: детектор гонок по горячим следам

В четвёртом уроке мы назвали диагностический прибор, но не разобрали. Настало время: по запросу **гонка данных golang** поисковики первым делом ведут в Memory Model Go — формальное определение, при каких доступах память считается согласованной, — а вторым делом к встроенному инструменту. **Race detector** — детектор гонок, встроенный в инструментарий Go: компилятор умеет собирать программу с инструментированием памяти, и тогда рантайм в каждый момент знает, какая горутина каким адресом владеет.

Пользоваться просто: локально запускайте `go run -race main.go`, тесты — `go test -race`. Как только случается несинхронизированный доступ, хотя бы один из которых — запись, детектор печатает **WARNING: DATA RACE** и два стека: кто читал и кто писал, обе стороны происшествия, плюс указание на замки, которые могли бы спасти. Это не статический анализатор: race detector — **динамический**, он ловит только те гонки, которые реально случились в этом прогоне. Чистый прогон — не гарантия отсутствия гонок; зато пойманная гонка — доказательство, оформленное двумя стеками.

Здесь же — честная нота про песочницу: флага `-race` в ней нет, инструментирование остаётся за пределами браузера. И ещё одна нота о редакторе: код гонки (тот же counter++ без замка) мы в этом уроке нарочно не даём в интерактивных блоках — его вывод недетерминирован, вы видели это вживую в четвёртом уроке. Гонку показывают числами-бродягами в тексте, а ловят — детектором локально.

### Забытый Unlock: тихая мина

```go
package main

import (
        "fmt"
        "sync"
)

func update(mu *sync.Mutex) {
        mu.Lock() // замок взят...
        fmt.Println("обновил")
        // ...а Unlock вызвать забыли
}

func main() {
        var mu sync.Mutex
        update(&mu)
        mu.Lock() // кто-то другой хочет войти в секцию
        fmt.Println("вошли второй раз")
        mu.Unlock()
}
```

**Что выведет программа?**

- «обновил», затем «вошли второй раз»: замок освобождается при выходе из функции
- ✅ «обновил» — и зависание: второй Lock ждёт вечно, потому что Unlock забыли; песочница оборвёт ожидание по лимиту времени
- Паника на втором Lock: «unlock of unlocked mutex»
- Программа не скомпилируется: компилятор проверяет парность Lock/Unlock

Замок освобождает только явный Unlock — ни выход из функции, ни что-либо ещё. update вернулся с замком в руках, второй Lock в main ждёт вечно: в песочнице вы увидите «обновил» и ошибку «превышен лимит времени», в настоящем Go — зависание, а при одиночной горутине детектор дедлоков объявит «all goroutines are asleep - deadlock!». Лечение одно и уже знакомое: defer mu.Unlock() первой строкой после Lock. Компилятор парность не проверяет — это ваша дисциплина.

### Честно об ошибках семейства: двойной Unlock и само-дедлок

Predict показал мягкий финал — зависание. У другой ошибки этого семейства финал громкий. **Двойной Unlock** — вызов `mu.Unlock()` на уже свободном замке — это не паника и не зависание, а **fatal error**: `sync: unlock of unlocked mutex`. Fatal отличается от паники принципиально: recover его не перехватывает, разворачивания стека нет — процесс умирает на месте, вместе со всеми горутинами и несохранёнными данными. То же касается RWMutex: `RUnlock` без `RLock` — fatal error `sync: RUnlock of unlocked RWMutex`.

Почему мы не даём этих примеров в редакторе — по той же честной причине, что и код гонки: в песочнице фатальная ошибка рантайма убивает не «программу в окошке», а всю исполняющую обёртку, и вы получите не аккуратное сообщение об ошибке, а мёртвый воркер. Поэтому — текстом, с точными сообщениями: пусть сообщения останутся в памяти, а не в консоли.

Третья ошибка — **само-дедлок**: повторный `mu.Lock()` в той же горутине, пока замок уже у неё. Замок в Go не реентерабельный: «я сам» для него не аргумент, горутина ждёт сама себя. В песочнице это оборвётся лимитом времени — как predict выше; в настоящем Go программа либо зависнет, либо детектор дедлоков поймает полное затишье. Все три ошибки лечатся одинаково: defer сразу после Lock, парность вызовов, и никаких вложенных Lock одного и того же замка.

### Шпаргалка: чем защищать общее состояние

| Инструмент | Ключевые вызовы | Годится для | Осторожно |
| --- | --- | --- | --- |
| sync.Mutex | Lock / Unlock | любые критические секции: счётчики, карты, слайсы, инварианты из нескольких полей | не копировать; забытый Unlock — дедлок; двойной Unlock — fatal error |
| sync.RWMutex | RLock/RUnlock, Lock/Unlock | много параллельных чтений, редкие короткие записи | каждому RLock — RUnlock в defer; RUnlock без RLock — fatal error |
| sync/atomic | AddInt64, LoadInt64, CompareAndSwap | одиночные счётчики и флаги без сложных инвариантов | защищает одно значение, а не согласованность группы полей |
| Каналы | ch <- v, v := <-ch | передача владения данными между горутинами, конвейеры | это не замок, а труба: очередь сообщений вместо общего доступа |
| sync.WaitGroup | Add / Done / Wait | дождаться конца бригады горутин | не защищает состояние — сообщает только факт завершения |

### sync/atomic: когда мьютекс — из пушки по воробьям

Если общее состояние — одно число, то и защита может быть одним вызовом. Пакет **sync/atomic** даёт атомарные операции над одиночными значениями: `atomic.AddInt64(&n, 1)` прибавляет единицу как неделимое действие процессора — без замка, без очереди, без критической секции; `atomic.LoadInt64(&n)` и `atomic.StoreInt64(&n, v)` читают и пишут атомарно, `atomic.CompareAndSwapInt64` — «поменяй, если никто не успел раньше», основа lock-free конструкций. На простых счётчиках запросов, метрик и флагов это заметно дешевле мьютекса.

Граница у атомарности жёсткая: она защищает **одно значение**, а не согласованность нескольких. Если инвариант держится на паре полей — «баланс дебета равен кредиту», «имя и возраст обновляются вместе» — атомарные операции не спасут: горутина увидит половину обновления. Там нужен мьютекс, под которым оба поля меняются в одной секции. Правило выбора короткое: одно число — atomic; больше одного поля или инвариант — Mutex; много чтений — RWMutex; передача владения — канал.

В песочнице sync/atomic доступен в основном движке — можете проверить атомарный счётчик против мьютексного прямо в редакторе этого урока, скопировав код runner-а и заменив пару Lock/Unlock на atomic.AddInt64. Это и будет лучшим домашним экспериментом.

> **Честно о двух движках песочницы.** Кнопка «Запустить» в этом уроке работает на основном движке песочницы — настоящем Go (yaegi, собран в WASM): пакет sync там полный, Mutex, RWMutex и WaitGroup ведут себя как в локальном go run. Если WASM не загрузился, песочница тихо включает резервный упрощённый движок, а в нём пакет sync недоступен — при импорте он отвечает «пакет sync недоступен в песочнице (доступны: fmt, strings, strconv, math, sort, errors, time)». Поэтому все интерактивы с кодом этого урока — три runner-а, два predict-а, fill и order — проверяйте в основном движке; резервный для них честно покажет ту самую ошибку недоступности.

### Впишите скелет мьютекс-счётчика

Восстановите защиту общего счётчика: имя типа замка, страховка отчёта бригады, вход и выход из критической секции — по одному слову на пропуск.

```go
package main

import (
        "fmt"
        "sync"
)

var (
        mu      sync.___ // замок над общим состоянием
        counter = 0      // само состояние
)

func main() {
        var wg sync.WaitGroup
        wg.Add(2)
        for i := 0; i < 2; i++ {
                go func() {
                        defer wg.___() // отчёт бригаде — только со страховкой
                        for j := 0; j < 500; j++ {
                                mu.___()  // вход в критическую секцию
                                counter++ // читай-прибавь-запиши — теперь по очереди
                                mu.___()  // выход: замок не тащим дальше
                        }
                }()
        }
        wg.Wait()
        fmt.Println("счётчик:", counter) // всегда 1000
}
```

Подсказка: Четыре слова из двух пакетов: тип замка из sync, отложенный отчёт бригаде и пара вход-выход из секции.

Правильные ответы в пропусках: `Mutex`, `Done`, `Lock`, `Unlock`

### Соберите счётчик под замком

Соберите каноническую программу: два воркера через общий мьютекс прибавляют по 1000, отчёт — через defer, main дожидается бригады и печатает 2000. Замок и WaitGroup передаются указателями.

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `import "sync"`
4. `func worker(id int, mu *sync.Mutex, c *int, wg *sync.WaitGroup) {`
5. `defer wg.Done()`
6. `for i := 0; i < 1000; i++ {`
7. `mu.Lock()`
8. `*c++`
9. `mu.Unlock()`
10. `}`
11. `}`
12. `func main() {`
13. `var mu sync.Mutex`
14. `var wg sync.WaitGroup`
15. `counter := 0`
16. `wg.Add(2)`
17. `go worker(1, &mu, &counter, &wg)`
18. `go worker(2, &mu, &counter, &wg)`
19. `wg.Wait()`
20. `fmt.Println("счётчик:", counter)`
21. `}`

### Что дальше: защита доступа — ещё не вся координация

Мьютекс закрывает одну ось конкурентности — доступ к общим данным. Но у живых схем есть и вторая: управление самим ходом работы. Бригада воркеров под замком правит общую память, но её ещё нужно вовремя остановить, передать ей отмену или дедлайн — и вы уже умеете половину этого: широковещательный stop-канал из [урока про select](/courses/go-advanced/lessons/06-select/) в связке с WaitGroup закрывает и приём, и счётчик. Следующий урок курса — про контекст: context.Context передаёт отмену и дедлайны по цепочке вызовов как часть сигнатуры, и это прямое продолжение линии синхронизации: мьютекс говорит «не одновременно», контекст — «долго не надо». Все инструменты вместе — замки, каналы, WaitGroup, контекст — складываются в конструктор конкурентных схем [всего курса](/courses/go-advanced/): разучив по одному за урок, дальше вы только комбинируете.

### Карточки урока

- **sync.Mutex — что это** — Замок взаимного исключения: Lock — войти и держать, Unlock — освободить. Нулевое значение готово к работе. Защищает участок кода (критическую секцию), а не переменную: атомарность — следствие дисциплины расстановки.
- **Критическая секция** — Код между Lock и Unlock: в каждый момент внутри не больше одной горутины. В секции — только доступ к общим данным; сеть, сон и чужой код с тем же замком — за её границами.
- **defer mu.Unlock()** — Освобождает замок при любом выходе — ранний return и паника включительно. Забытый Unlock — вечное ожидание на чужом Lock (в песочнице — лимит времени). Двойной Unlock — fatal error «sync: unlock of unlocked mutex», recover его не ловит.
- **sync.RWMutex** — Замок с двумя режимами: RLock пропускает сколько угодно читателей одновременно, Lock требует наедине со всеми. Каждому RLock — RUnlock в defer; RUnlock без RLock — fatal error. Выигрывает при длинных чтениях и редких записях.
- **race detector (-race)** — go run -race и go test -race: при несинхронизированном доступе печатает WARNING: DATA RACE с двумя стеками — читатель и писатель. Детектор динамический: ловит только гонки, случившиеся в прогоне. В песочнице флага нет.
- **sync/atomic** — Атомарные операции над одиночными значениями: AddInt64, LoadInt64, StoreInt64, CompareAndSwap. Дешевле мьютекса на простых счётчиках, но не защищает согласованность нескольких полей — там нужен Mutex.

## Коротко: что запомнить

- sync.Mutex — замок взаимного исключения с нулевым значением, готовым к работе: mu.Lock() — вход, mu.Unlock() — выход, между ними критическая секция.
- Мьютекс защищает участок кода, а не переменную: counter++ остаётся тремя шагами машины, но внутри секции они исполняются по очереди — и итог перестаёт плыть.
- defer mu.Unlock() первой строкой после Lock — та же страховка, что defer wg.Done(): замок освобождается при раннем выходе и при панике.
- Секция покрывает только доступ к общим данным: I/O и чужой код под замком тормозят всех, а второй Lock из той же горутины — само-дедлок.
- sync.RWMutex: RLock — сколько угодно читателей параллельно, Lock — писатель наедине со всеми; каждому RLock — RUnlock в defer.
- Race detector: go run -race и go test -race печатают WARNING: DATA RACE с двумя стеками; детектор динамический, в песочнице флага нет.
- Двойной Unlock и RUnlock без RLock — fatal error, recover не спасает; для одиночных чисел смотрите в сторону sync/atomic.

## Домашнее задание

Возьмите воркер-пул из седьмого урока: три воркера разбирают срез из тридцати чисел и прибавляют каждое к общей сумме. Сначала без замка — наблюдайте, как итог плавает от запуска к запуску; затем с sync.Mutex вокруг критической секции — сумма стабилизируется на честном значении при каждом прогоне. После этого оберните счётчик в структуру с непубличным полем-замком и методом Add, а чтение итога вынесите в метод Value под RLock, заменив Mutex на RWMutex. Напоследок сломайте всё по очереди: уберите Unlock и поймайте зависание по лимиту времени; вызовите Unlock дважды — в настоящем Go получите fatal error «sync: unlock of unlocked mutex» (в песочнице не делайте этого: падает вся обёртка); и запустите окончательную версию локально с go run -race, чтобы увидеть пустой отчёт детектора — лучший вид отчёта.

## Вопросы и ответы по уроку

### Чем мьютекс отличается от канала?

Мьютекс — замок на общее состояние: стучаться могут многие, внутри секции один. Канал — труба передачи владения: «не разделяйте память, передавайте её». Защитить переменную — мьютексом; передать работу и результаты — каналом; в воркер-пуле они работают в связке.

### Что будет, если вызвать mu.Unlock() дважды?

fatal error: sync: unlock of unlocked mutex. Это не паника: recover её не перехватывает, процесс падает целиком со всеми горутинами. Поэтому Unlock держат в defer сразу после Lock — отложенный вызов выполняется ровно один раз.

### Когда брать RWMutex вместо обычного Mutex?

Когда чтений много и они длинные, а записи редкие и короткие: RLock пропускает читателей параллельно. На крошечных секциях накладные расходы RWMutex могут съесть выигрыш — решение принимайте измерением, по умолчанию берите Mutex.

### Как поймать гонку данных до продакшена?

Запускайте и тесты с флагом -race: go run -race main.go, go test -race. Детектор гонок печатает WARNING: DATA RACE с двумя стеками — обе стороны доступа. Он динамический: видит только гонки, случившиеся в прогоне, поэтому гоняйте с -race постоянно. В песочнице флаг недоступен — это локальный инструмент.
