Mutex, RWMutex и каналы как синхронизация

О лекции

Mutex защищает общую память. Канал передаёт владение значением между горутинами. RWMutex выгоден, когда чтений много, а запись редкая.

Конкурентность Go golang собеседование

В Go есть два честных стиля. Первый: не делить память, а передавать сообщения. Второй: делить память и охранять её замком. Документация любит первое. Прод любит оба. Счётчик, кэш в структуре, карта сессий — обычно mutex. Пайплайн стадий, пул задач, «отдай ресурс воркеру» — канал.

Нет правила «каналы всегда правильнее». Канал на каждый инкремент метрики — лишние горутины и сложность. Мьютекс вокруг всего хендлера, включая HTTP-вызов, держит всех и ловит взаимную блокировку. Выбор — про то, что является инвариантом: ячейка памяти или передача владения.

Аналогия: общий блокнот на столе — нужен замок (mutex). Передать блокнот коллеге из рук в руки — канал: после передачи вы его больше не пишете.

Mutex

sync.Mutex — нерекурсивный замок. Два Lock подряд из той же горутины — взаимная блокировка. Unlock без Lock — panic. Копировать структуру, в которой лежит mutex, нельзя. Копия уносит состояние «заперт / открыт». Две горутины думают, что владеют разными замками, а память общая. Поэтому методы с мьютексом — на указателе.

type Counter struct {
	mu sync.Mutex
	n  int
}

func (c *Counter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n++
}

func (c *Counter) Value() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.n
}

Счётчик состояния — mutex. Unlock в defer, методы на указателе, структуру не копируют.

Пример. Сделали c2 := c1, где у Counter внутри mutex. Одна горутина лочит c1, другая — копию c2. Это разные замки, поле n одно. Получается гонка.

Долгая работа под замком (RPC, диск) держит всех остальных. Под замком оставляют короткий участок: поправили map, сняли. Unlock не в defer легко забыть на ветке return с ошибкой.

RWMutex и каналы

RWMutex пускает много читателей или одного писателя. Имеет смысл, когда чтение доминирует и само короткое. Писатель может голодать, если читатели не кончаются. Рекурсии нет: RLock и затем Lock в той же горутине — взаимная блокировка. «Я читал, теперь хочу писать, не отпуская» так не делают: отпустили, взяли Lock, перепроверили данные.

Канал уместнее, когда передаёте задачу или единственное владение буфером: после send отправитель больше не трогает те же байты. Давление очереди получается само: медленный читатель тормозит писателя. Для счётчика метрик канал обычно хуже атомика или mutex.

Пример. Кэш конфига читают на каждый запрос, пишут раз в минуту — RWMutex. Очередь писем воркеру — канал. Счётчик RPS — atomic или mutex, не канал.

Типичная ошибка

Скопировать структуру с mutex (в том числе встроить mutex и отдать значение по ошибке). Unlock не в defer. RLock и затем Lock в одной горутине. И догма «только каналы»: для двух полей в одной структуре mutex проще и яснее.

На собеседовании

Нет единого победителя. Счётчик и кэш в структуре — mutex или atomic. Передача задачи воркеру — канал. RWMutex — много коротких чтений. Замок не рекурсивный, писатель не должен голодать.