← Назад к вопросам

Чем отличается sync.Mutex от sync.RWMutex?

2.2 Middle🔥 211 комментариев
#Конкурентность и горутины

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Основное отличие: назначение и гранулярность блокировок

Основное и фундаментальное отличие между sync.Mutex и sync.RWMutex заключается в гранулярности предоставляемых блокировок и, как следствие, в сценариях их применения.

  • sync.Mutex предоставляет взаимоисключающую (exclusive) блокировку. Это классический мьютекс, который в любой момент времени может удерживать только одна горутина. Он гарантирует, что доступ к защищаемым данным (критической секции) получает ровно один поток исполнения. Все остальные горутины, пытающиеся захватить этот мьютекс, будут заблокированы до его освобождения.
  • sync.RWMutex (Read-Write Mutex) предоставляет два типа блокировок: на чтение (RLock) и на запись (Lock). Это позволяет нескольким горутинам одновременно захватывать блокировку на чтение, но блокировка на запись остается взаимоисключающей, как у обычного Mutex. Таким образом, RWMutex оптимизирован для сценариев, где операции чтения значительно превосходят по количеству операции записи.

Детальное сравнение

sync.Mutex: Простая эксклюзивная блокировка

Методы:

  • Lock() – захватывает мьютекс. Если он уже захвачен другой горутиной, текущая горутина блокируется.
  • Unlock() – освобождает мьютекс.

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

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock() // Захват эксклюзивной блокировки
    defer c.mu.Unlock()
    c.v[key]++ // И запись, и чтение защищены одной блокировкой
}

func (c *SafeCounter) Value(key string) int {
    c.mu.Lock() // Даже для чтения нужна эксклюзивная блокировка
    defer c.mu.Unlock()
    return c.v[key]
}

sync.RWMutex: Разделение блокировок на чтение и запись

Методы:

  • Lock() / Unlock() – для эксклюзивной блокировки на запись. Работают аналогично sync.Mutex.
  • RLock() / RUnlock() – для неэксклюзивной блокировки на чтение. Многие горутины могут одновременно захватить RLock().
  • Важное правило: если хотя бы одна горутина держит блокировку на чтение (RLock), то захватить блокировку на запись (Lock) невозможно. И наоборот, если горутина держит блокировку на запись, то захватить блокировку на чтение невозможно.

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

type SafeConfig struct {
    mu sync.RWMutex
    config map[string]string
}

func (c *SafeConfig) Update(key, value string) {
    c.mu.Lock() // Эксклюзивная блокировка ТОЛЬКО для операции записи
    defer c.mu.Unlock()
    c.config[key] = value
}

func (c *SafeConfig) Get(key string) string {
    c.mu.RLock() // Неэксклюзивная блокировка на чтение. Многие горутины могут читать параллельно.
    defer c.mu.RUnlock()
    return c.config[key]
}

Ключевые различия в поведении

Аспектsync.Mutexsync.RWMutex
Тип блокировкиОдин тип: эксклюзивная.Два типа: эксклюзивная запись (Lock) и неэксклюзивное чтение (RLock).
Параллельное чтениеНевозможно. Любая операция (чтение или запись) требует полной эксклюзивности.Возможно. Множество горутин могут одновременно читать данные, удерживая RLock.
ПроизводительностьПроще, меньше накладных расходов на одну операцию.Имеет более сложную внутреннюю логику для управления очередями читателей и писателей. В сценариях с частыми записями или низкой конкуренцией может быть МЕДЛЕННЕЕ обычного Mutex из-за накладных расходов.
Сценарий использованияПодходит для любых критических секций, особенно когда операции записи часты или код выполняет чтение-модификацию-запись (read-modify-write).Идеален для сценариев "частое чтение, редкая запись" (read-heavy workloads). Например, кэши, конфигурации, справочники, которые редко обновляются.

Важные практические замечания

  1. Предпочтение sync.Mutex по умолчанию: Начинайте проектирование с простого sync.Mutex. Вводите sync.RWMutex только после измерений профилировщиком, которые явно показывают, что contention (состязание) за блокировку чтения является узким местом, и что операции чтения доминируют.
  2. Цена RWMutex: Внутренняя реализация RWMutex сложнее. Ему необходимо отслеживать количество активных читателей, управлять приоритетом (в современных версиях Go писатели имеют приоритет, чтобы не происходило starvation — голодания писателей). Из-за этого вызов RLock()/RUnlock() может быть сопоставим по стоимости с вызовом Lock()/Unlock() при отсутствии конкуренции.
  3. Семантика: RWMutex не гарантирует, что читатели увидят "самые свежие" данные, записанные другим писателем, если между операциями чтения и записи нет дополнительных механизмов синхронизации (каналы, sync.WaitGroup и т.д.). Однако он гарантирует, что операции атомарны и данные не будут повреждены.
  4. Блокировка на запись при наличии читателей: Писатель будет ждать, пока все существующие читатели освободят блокировку (RUnlock()), прежде чем он сможет захватить Lock().
  5. Блокировка на чтение при наличии писателя: Новые читатели будут ждать, пока писатель, удерживающий Lock(), не освободит его.

Вывод

sync.Mutex — это базовый и универсальный инструмент для обеспечения эксклюзивного доступа. sync.RWMutex — это его специализированная оптимизация для конкретного, но распространенного сценария, где необходимо максимизировать параллелизм операций чтения. Выбор между ними должен быть осознанным и основанным на реальных данных о нагрузке, а не на предположениях. В сомнительных случаях sync.Mutex является более предсказуемым и зачастую достаточно эффективным выбором.