> Какие методы у мьютекса для блокировки на чтение и запись в Go (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: МедиСофт

Стек: Go

> Пример ответа

Короткий ответ

В Go мьютекс не разделяет блокировки на чтение и запись - для этого используется sync.RWMutex. У него есть методы Lock() и Unlock() для эксклюзивной записи, а также RLock() и RUnlock() для разделяемого чтения. RWMutex позволяет множеству читателей одновременно, но писатель получает доступ только когда нет активных читателей. Для обычного sync.Mutex доступны только Lock() и Unlock().

Подробное объяснение

sync.RWMutex - это специализированный мьютекс с двумя режимами блокировки:

  • Чтение (RLock/RUnlock): множество горутин могут одновременно удерживать read lock. Блокировка на чтение не блокирует другие read lock, но блокирует write lock.
  • Запись (Lock/Unlock): эксклюзивный доступ. Пока удерживается write lock, ни read lock, ни другие write lock не могут быть получены.

Важные детали реализации:

  • RWMutex не гарантирует справедливость (fairness). Если писатель ждёт, а новые читатели продолжают приходить, писатель может голодать. В Go начиная с версии 1.8 реализация блокирует новых читателей, если есть ожидающий писатель, но это не строгая гарантия.
  • RWMutex нельзя копировать после первого использования - это же касается и sync.Mutex.
  • Методы TryLock и TryRLock появились в Go 1.18 - они пытаются захватить блокировку без блокировки горутины и возвращают bool.
  • RLocker() возвращает интерфейс Locker, который позволяет использовать RWMutex как обычный sync.Locker в режиме чтения - удобно для передачи в функции, ожидающие Locker.

Разница между sync.Mutex и sync.RWMutex:

  • sync.Mutex - простой взаимно-исключающий замок, не различает чтение и запись.
  • sync.RWMutex - оптимизирован для сценариев, где чтение происходит значительно чаще записи. При высокой конкуренции на чтение RWMutex даёт выигрыш в производительности, но сам по себе он тяжелее Mutex из-за дополнительной логики.

На практике

Выбор между Mutex и RWMutex зависит от паттерна доступа:

  • Если операции чтения доминируют (например, кэш, конфигурация, таблица маршрутов) - используйте RWMutex.
  • Если чтение и запись примерно равны, или критична минимальная задержка - Mutex может быть лучше, так как RWMutex имеет накладные расходы на управление читателями.
  • Не используйте RWMutex для защиты структур, где чтение тривиально быстрое (например, чтение одного int) - оверхед от атомарных операций в RWMutex может превысить выгоду.

Паттерн использования:

GO
var mu sync.RWMutex
var data map[string]string
func Read(key string) string {
mu.RLock()
defer mu.RUnlock()
return data[key]
}
func Write(key, value string) {
mu.Lock()
defer mu.Unlock()
data[key] = value
}

Важно: RLock и RUnlock должны вызываться парно, как и Lock/Unlock. Использование defer - стандартный подход, но при длительных критических секциях лучше вызывать Unlock явно, чтобы не держать блокировку дольше необходимого.

Пример кода

GO
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.RWMutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
func main() {
c := SafeCounter{}
var wg sync.WaitGroup
// 10 писателей
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
c.Increment()
}
}()
}
// 5 читателей
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
_ = c.Value()
}
}()
}
wg.Wait()
fmt.Println("final value:", c.Value())
}

Как отвечать на собеседовании

Начните с прямого ответа: в Go нет отдельного мьютекса для чтения и записи - есть sync.RWMutex с методами RLock/RUnlock для чтения и Lock/Unlock для записи. Затем кратко объясните семантику: читатели не блокируют друг друга, писатель блокирует всех. Упомяните, что sync.Mutex - это простой мьютекс без разделения режимов.

Если спросят про производительность - скажите, что RWMutex оправдан при высокой доле чтения, но имеет оверхед. Если спросят про голодание - упомяните, что Go 1.8+ предотвращает голодание писателей, блокируя новых читателей при ожидающем писателе, но это не строгая гарантия.

Покажите понимание практических нюансов: парность вызовов, запрет копирования, TryLock/TryRLock (Go 1.18+), метод RLocker().

Что проверяет интервьюер

Интервьюер проверяет:

  • Знание стандартной библиотеки синхронизации Go.
  • Понимание разницы между Mutex и RWMutex и сценариев их применения.
  • Понимание модели конкурентности в Go: как RWMutex взаимодействует с горутинами и планировщиком.
  • Умение рассуждать о производительности и trade-off'ах.
  • Знание тонкостей: голодание писателей, запрет копирования, атомарность операций.

Типичные ошибки

  • Путаница между sync.Mutex и sync.RWMutex - кандидат утверждает, что Mutex имеет методы для чтения и записи.
  • Непонимание, что RLock блокирует писателей, но не других читателей.
  • Забывают, что RWMutex нельзя копировать - это приводит к data race.
  • Использование RWMutex там, где Mutex был бы эффективнее - кандидат не понимает оверхед.
  • Неправильный порядок вызовов: RLock без RUnlock или Lock без Unlock - кандидат не осознаёт важность парности.
  • Утверждение, что RWMutex гарантирует отсутствие голодания писателей - это не строгая гарантия, а эвристика в реализации.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью