Встречается на 25% собеседований по Go

Что такое mutex, какие они бывают и как их использовать

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

Как это работает

Мьютекс имеет два основных состояния: захвачен (locked) и свободен (unlocked). Перед входом в критическую секцию горутина вызывает метод Lock(). Если мьютекс уже захвачен другой горутиной, вызывающая горутина блокируется до тех пор, пока мьютекс не будет освобождён. После завершения работы с общими данными горутина вызывает Unlock(), позволяя другим горутинам захватить мьютекс.

В Go существует два основных типа мьютексов:

  • Блокирующий мьютекс (sync.Mutex): при попытке захвата занятого мьютекса горутина блокируется и переходит в состояние ожидания. Это эффективно, когда критические секции могут быть длительными, так как не тратится CPU на активное ожидание.
  • Неблокирующий мьютекс (спинлок): горутина активно проверяет состояние мьютекса в цикле, не блокируясь. Это оправдано только для очень коротких критических секций, когда время ожидания меньше, чем затраты на переключение контекста. В стандартной библиотеке Go спинлоки не предоставляются напрямую, но их можно реализовать с помощью атомарных операций.

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

Рассмотрим пример защиты банковского счёта от гонки данных:

go
package main

import (
    "fmt"
    "sync"
)

var (
    balance int
    mutex   sync.Mutex
)

func deposit(value int, wg *sync.WaitGroup) {
    mutex.Lock()   // Захват мьютекса перед изменением balance
    fmt.Printf("Depositing %d to account with balance: %d\n", value, balance)
    balance += value
    mutex.Unlock() // Освобождение мьютекса после изменения
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go deposit(100, &wg)
    go deposit(200, &wg)
    wg.Wait()
    fmt.Printf("New Balance %d\n", balance)
}

Здесь две горутины вызывают deposit, и мьютекс гарантирует, что операции чтения и записи balance выполняются атомарно. Без мьютекса результат мог бы быть непредсказуемым из-за гонки данных.

Подводные камни и рекомендации

  • Минимизируйте время удержания мьютекса: держите критическую секцию как можно короче, чтобы не блокировать другие горутины.
  • Избегайте вложенных мьютексов: захват нескольких мьютексов в разном порядке может привести к взаимоблокировке (deadlock). Если необходимо несколько мьютексов, всегда захватывайте их в одном и том же порядке.
  • Не забывайте про Unlock: используйте defer mutex.Unlock() сразу после Lock(), чтобы гарантировать освобождение даже при панике.
  • Защищайте все обращения к данным: мьютекс защищает только те блоки кода, где он явно используется. Обращения к тем же данным вне критической секции всё ещё могут вызвать гонку.
  • Рассмотрите альтернативы: в Go часто предпочтительнее использовать каналы для синхронизации, так как они более идиоматичны и безопасны. Мьютексы уместны, когда нужно защитить сложные структуры данных или когда каналы не подходят.

Коротко

  • Mutex — примитив синхронизации для защиты критических секций от одновременного доступа нескольких горутин.
  • В Go используется sync.Mutex с методами Lock() и Unlock(); есть также sync.RWMutex для ситуаций с частым чтением и редкой записью.
  • Критическая секция должна быть минимальной, а Unlock — гарантированным (лучше через defer).
  • Избегайте вложенных мьютексов и защищайте все обращения к общим данным, иначе возможны гонки и взаимоблокировки.
как отвечать на вопрос
пример собеседования
фреймворки на собеседовании
типичные вопросы junior
интервью вопросы и ответы