Menu

Мьютексы в Golang: sync.Mutex, RWMutex, atomic и sync.Once

Как защищать общее состояние между горутинами через sync.Mutex и sync.RWMutex, когда достаточно sync/atomic, как sync.Once выполняет инициализацию ровно один раз и какие ошибки блокировок приводят к deadlock.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Защита общих данных

sync.Mutex пропускает в код между Lock и Unlock только одну горутину за раз. Кладите мьютекс рядом с данными, которые он охраняет, обычно в ту же структуру:

Программа всегда печатает hits: 10000. Без блокировки 100 горутин, пишущих в одну мапу, обычно уронили бы программу с fatal error: concurrent map writes. Эта ошибка идёт от проверки, которую среда выполнения делает по мере возможности, после неё нельзя восстановиться, а отсутствие падения не доказывает, что код корректен.

Важные детали:

  • Нулевое значение sync.Mutex разблокировано и готово к работе. Конструктор не нужен.
  • Методы используют получатель-указатель (*Counter). Получатель-значение заблокировал бы копию мьютекса, которая ничего не защищает.
  • defer c.mu.Unlock() сразу после Lock означает, что любой путь возврата, как и паника, освобождает блокировку.
  • Любой доступ идёт через блокировку, включая чтение. Чтение без блокировки, пока другая горутина пишет, это всё равно гонка данных.

Держите критическую секцию маленькой

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

func (s *Store) Save(key string) error {
	s.mu.Lock()
	data := s.items[key] // copy what you need
	s.mu.Unlock()

	return writeToDisk(key, data) // slow I/O, outside the lock
}

Удержание блокировки во время сетевых вызовов, дискового ввода-вывода или отправки в канал это самая частая причина медленной конкурентной программы, а отправка в канал под блокировкой частая причина deadlock.

RWMutex для данных, которые в основном читают

У sync.RWMutex два режима. RLock/RUnlock берут разделяемую блокировку на чтение, которую одновременно могут держать многие горутины. Lock/Unlock берут исключительную блокировку на запись, которая ждёт, пока уйдут все читатели.

RWMutex окупается, когда чтения преобладают и каждое чтение делает под блокировкой реальную работу. Для крошечных критических секций вроде одного поиска в мапе обычный Mutex часто не медленнее, потому что у блокировки на чтение есть собственные накладные расходы. Сначала проведите бенчмарк.

Повысить блокировку на чтение до блокировки на запись нельзя. Вызов Lock при удержании RLock в той же горутине приводит к deadlock. Сначала отпустите блокировку на чтение, затем возьмите блокировку на запись и снова проверьте условие, ведь за это время другой писатель мог изменить данные.

sync/atomic для одиночных значений

Для одного счётчика или флага sync/atomic проще и дешевле мьютекса. Используйте типизированные обёртки (Go 1.19):

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

sync.Once

sync.Once выполняет функцию ровно один раз, сколько бы горутин её ни вызывали одновременно. Все, кто вызывает Do, ждут, пока первый вызов не завершится. Это стандартный способ ленивой инициализации:

Каждая строка «runs once» появляется ровно один раз. Если функция, переданная в Do, паникует, Once всё равно считает её выполненной и повторять не будет. sync.OnceValues делает то же для функций, возвращающих два значения, обычно значение и ошибку.

Мьютекс, канал или sync.Map

СитуацияЧто использовать
Структура или мапа, которую несколько горутин обновляют на местеsync.Mutex в структуре
В основном чтения, изредка записи, чтения делают реальную работуsync.RWMutex
Один счётчик или флагsync/atomic
Однократная инициализацияsync.Once, sync.OnceValue
Передача данных от одной горутины к другойканал
Кеш, ключи которого записываются один раз и читаются много раз, или горутины работают с непересекающимися ключамиsync.Map

sync.Map не универсальная замена мапе с блокировкой. У неё нет параметров типа, поэтому значения возвращаются как any, и быстрее она только в двух случаях из таблицы. Начинайте с мьютекса и обычной мапы.

Частые ошибки

  • Копирование мьютекса. Передача по значению структуры, которая содержит sync.Mutex, или получатель-значение копируют блокировку. go vet сообщает passes lock by value или copies lock value.
  • Двойная блокировка в одной горутине. Мьютексы в Go не реентерабельны. Если Inc вызывает Get и оба берут блокировку, Inc заблокируется навсегда. Пусть публичные методы блокируют, а приватные помощники считают, что блокировка уже взята.
  • Забытая разблокировка при раннем возврате. Используйте defer, если нет причины поступить иначе.
  • Блокировки в разном порядке. Если одна горутина берёт блокировку A, затем B, а другая B, затем A, они могут вечно ждать друг друга. Всегда берите несколько блокировок в одном порядке.
  • Выдача охраняемых данных наружу. Если метод возвращает внутреннюю мапу, вызывающий код может читать и писать её без блокировки. Возвращайте копию (maps.Clone, Go 1.21) или одно значение.
  • Защита только записей. Чтения без блокировки одновременно с записями под блокировкой всё равно гонки. Запускайте тесты через go test -race.

Часто задаваемые вопросы

Что такое мьютекс в Go?

sync.Mutex это блокировка, которая позволяет только одной горутине за раз выполнять код между mu.Lock() и mu.Unlock(). Им защищают данные, которые читают и пишут несколько горутин, например мапу или структуру. Его нулевое значение это разблокированный мьютекс, готовый к работе.

Когда использовать RWMutex вместо Mutex?

Когда чтений гораздо больше, чем записей, и каждое чтение держит блокировку заметное время. RLock пускает одновременно сколько угодно читателей, а Lock ждёт исключительного доступа. Для коротких критических секций обычный Mutex часто так же быстр или быстрее, поэтому сначала измерьте.

Безопасна ли map в Go для конкурентного использования?

Нет. Конкурентные чтения допустимы, но запись одновременно с любым другим чтением или записью это гонка данных, и среда выполнения обычно её обнаруживает и роняет программу с fatal error: concurrent map writes (или concurrent map read and map write). Защищайте мапу через sync.Mutex или sync.RWMutex либо используйте sync.Map в её конкретных сценариях.

Является ли sync.Mutex в Go реентерабельным?

Нет. Если горутина, которая держит блокировку, снова вызывает Lock, она навсегда блокируется, ожидая саму себя. Стройте код так, чтобы экспортированные методы брали блокировку и вызывали неэкспортированные помощники, которые считают, что блокировка уже взята.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ