Mutex — взаимное исключение, блокирует доступ к ресурсу для всех горутин. RWMutex позволяет множеству читателей или одному писателю. Более эффективен при частых чтениях.
sync.Mutex и sync.RWMutex в Go
sync.Mutex — базовый мьютекс
Назначение: Обеспечивает взаимное исключение (mutual exclusion) — доступ к защищаемому ресурсу получает только одна горутина в момент времени.
Основные методы:
Lock()— захватывает блокировку. Если блокировка уже удерживается другой горутиной, текущая горутина блокируется до её освобождения.Unlock()— освобождает блокировку. Паникует, если вызван на незаблокированном мьютексе.
Типичное использование:
var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
Важные особенности:
- Не поддерживает реентерабельность (reentrancy) — если горутина уже удерживает Lock и вызывает Lock снова, возникает deadlock.
- После Unlock мьютекс можно снова захватить, но гарантий порядка пробуждения ожидающих горутин нет.
- Копирование sync.Mutex после первого использования приводит к неопределённому поведению — всегда передавайте через указатель.
sync.RWMutex — мьютекс с разделением на чтение/запись
Назначение: Оптимизация для сценариев с частыми чтениями и редкими записями. Позволяет множеству горутин одновременно читать, но блокирует всех при записи.
Методы:
Lock()/Unlock()— эксклюзивная блокировка для записи.RLock()/RUnlock()— разделяемая блокировка для чтения.
Правила работы:
- Несколько горутин могут одновременно удерживать RLock.
- Lock ждёт, пока все RLock будут освобождены.
- RLock ждёт, если удерживается Lock.
- Новые RLock блокируются, если в очереди уже ждёт Lock (честность — fair locking).
Пример:
var mu sync.RWMutex
var cache = make(map[string]string)
func read(key string) string {
mu.RLock()
defer mu.RUnlock()
return cache[key]
}
func write(key, value string) {
mu.Lock()
defer mu.Unlock()
cache[key] = value
}
Ключевые различия
| Характеристика | Mutex | RWMutex |
|---|---|---|
| Конкурентные читатели | Нет | Да |
| Производительность при низкой конкуренции | Выше | Ниже (дополнительные накладные расходы) |
| Память | Меньше | Больше (счётчик читателей) |
| Когда выбирать | Записи частые или чтения редкие | Чтения значительно преобладают над записями |
Практические рекомендации
-
Минимизируйте критическую секцию — держите блокировку только на время работы с общими данными, не выполняйте внутри блокировки I/O или медленные операции.
-
Используйте defer для Unlock/RLock — это гарантирует освобождение даже при панике:
mu.Lock() defer mu.Unlock() -
Не копируйте мьютексы — передавайте через указатель или используйте структуру с встроенным мьютексом:
type SafeCounter struct { mu sync.Mutex value int } -
Не смешивайте RLock и Lock в одной горутине — это приведёт к deadlock:
mu.RLock() mu.Lock() // deadlock! -
Проверяйте через go vet — утилита обнаруживает многие ошибки работы с мьютексами.
Типичные ошибки
- Забытый Unlock — приводит к вечной блокировке. Всегда используйте defer.
- Lock в defer без явной необходимости — может удерживать блокировку дольше, чем нужно.
- Использование RWMutex при равном соотношении чтений/записей — накладные расходы на атомарные операции для счётчика читателей могут превысить выгоду.
- Предположение о порядке пробуждения — Go не гарантирует fairness, хотя в текущей реализации RWMutex отдаёт приоритет ожидающим писателям.
Вопрос