Mutex vs каналы
Философия 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++
}Реализовать то же самое через канал технически можно (горутина-владелец счётчика читает из канала команд), но это больше кода ради того же результата — канал здесь не даёт ничего сверх мьютекса, только усложняет. Сравнение одной и той же задачи (инкремент общего счётчика из нескольких горутин) двумя подходами:
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}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 только когда чтения доказанно доминируют.