Что такое mutex, какие они бывают и как их использовать
Mutex (от Mutual Exclusion — взаимное исключение) — это примитив синхронизации, который гарантирует, что только одна горутина (или поток) может одновременно выполнять критическую секцию кода, защищающую общие данные. В Go мьютексы предоставляются пакетом sync и используются для предотвращения гонок данных при конкурентном доступе к разделяемым ресурсам.
Как это работает
Мьютекс имеет два основных состояния: захвачен (locked) и свободен (unlocked). Перед входом в критическую секцию горутина вызывает метод Lock(). Если мьютекс уже захвачен другой горутиной, вызывающая горутина блокируется до тех пор, пока мьютекс не будет освобождён. После завершения работы с общими данными горутина вызывает Unlock(), позволяя другим горутинам захватить мьютекс.
В Go существует два основных типа мьютексов:
- Блокирующий мьютекс (
sync.Mutex): при попытке захвата занятого мьютекса горутина блокируется и переходит в состояние ожидания. Это эффективно, когда критические секции могут быть длительными, так как не тратится CPU на активное ожидание. - Неблокирующий мьютекс (спинлок): горутина активно проверяет состояние мьютекса в цикле, не блокируясь. Это оправдано только для очень коротких критических секций, когда время ожидания меньше, чем затраты на переключение контекста. В стандартной библиотеке 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). - Избегайте вложенных мьютексов и защищайте все обращения к общим данным, иначе возможны гонки и взаимоблокировки.
