MBL
Go / demo / Mutex vs каналы
Go вводный

Mutex vs каналы

concurrencysync

Философия Go — "не общайтесь через разделяемую память, разделяйте память через общение" (Do not communicate by sharing memory; instead, share memory by communicating). Но это не значит, что sync.Mutex не нужен: каналы и мьютекс решают разные задачи.

Когда мьютекс, а не канал

Канал хорош, когда одна горутина передаёт владение данными другой — после передачи первая больше их не трогает. Мьютекс хорош, когда несколько горутин совместно владеют одним состоянием и по очереди его читают/меняют — счётчик, кэш, map.

type Counter struct {
    mu sync.Mutex
    n  int
}

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

Реализовать то же самое через канал технически можно (горутина-владелец счётчика читает из канала команд), но это больше кода ради того же результата — канал здесь не даёт ничего сверх мьютекса, только усложняет. Сравнение одной и той же задачи (инкремент общего счётчика из нескольких горутин) двумя подходами:

Mutex
type Counter struct {
    mu sync.Mutex
    n  int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}
Channel (владелец состояния)
type Counter struct {
    inc  chan struct{}
    read chan chan int
}

func (c *Counter) run() {
    n := 0
    for {
        select {
        case <-c.inc:
            n++
        case r := <-c.read:
            r <- n
        }
    }
}

Справа втрое больше кода ради того же результата — отдельная горутина-владелец, два канала, select. Mutex здесь не компромисс, а прямое решение задачи.

RWMutex

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

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(k string) string {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.data[k]
}

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

Самопроверка 0 / 2
Знаю, когда взять sync.Mutex вместо канала — защита общего состояния, не передача владения
Понимаю, когда RWMutex быстрее обычного Mutex, а когда нет разницы
Как усвоено?