Mutex и гонка данных
На кухне есть кладовая, а на двери — листок с числом мешков муки. Повар, который принёс мешок, читает число, прибавляет единицу и записывает новое. Что будет, если два повара прочитают листок одновременно? Оба увидят 41, оба запишут 42, и один мешок пропадёт из учёта.
Горутины попадают в ту же ловушку. Тысяча горутин увеличивает одну переменную, каждая на единицу, а main ждёт их всех:
package main
import "fmt"
import "sync"
func main() {
var wg sync.WaitGroup
n := 0
for range 1000 {
wg.Go(func() { n++ })
}
wg.Wait()
fmt.Println(n)
}Программа должна напечатать 1000, но на компьютере с несколькими ядрами обычно печатает меньше, например 970. Каждый запуск даёт другое число, а на одном ядре ошибка может не проявиться ни разу.
Гонка данных
Операция n++ состоит из трёх шагов:
- Прочитать
n. - Прибавить единицу.
- Записать результат.
Две горутины могут одновременно прочитать 41 и обе записать 42, и одно увеличение пропадёт.
Когда две горутины одновременно обращаются к одной переменной и хотя бы одна из них пишет, это гонка данных. Результат такой программы непредсказуем.
Как найти гонку
Найти гонку помогает флаг -race. Команда go run -race main.go печатает WARNING: DATA RACE и строки, где горутины встретились.
Гонка в map
С map гонка заканчивается ещё хуже, если несколько горутин пишут в одну map без защиты:
- среда выполнения часто это замечает и останавливает программу с
fatal error: concurrent map writes; - это не паника: перехватить её через
recoverнельзя.
На собеседовании про это любят спрашивать: обычная map в Go не защищена от одновременной записи.
sync.Mutex
Решение на кухне — один ключ от кладовой. Кто взял ключ, тот заходит и обновляет листок, а остальные ждут у двери. Тип sync.Mutex из пакета sync и есть такой ключ: он пускает к данным одну горутину за раз. У него два метода:
Lockзабирает ключ; горутина, которая вызвалаLockу запертогоMutex, ждёт, пока владелец не вызоветUnlock;Unlockвозвращает ключ.
var wg sync.WaitGroup
var mu sync.Mutex
n := 0
for range 1000 {
wg.Go(func() {
mu.Lock()
n++
mu.Unlock()
})
}
wg.Wait()
fmt.Println(n) // 1000Давайте разберём тело горутины:
mu.Lock()забирает ключ или ждёт, пока его вернут.n++выполняет все три шага, пока горутина держит ключ.mu.Unlock()возвращает ключ следующей горутине.
Другая горутина не может вклиниться между чтением и записью, поэтому ни одно увеличение не теряется, и программа печатает 1000 при каждом запуске.
Нулевое значение и копирование
Mutex объявляют и передают по тем же правилам, что и WaitGroup:
- нулевое значение
Mutexне заперто и готово к работе; - литерал
sync.Mutex{}даёт то же значение, поэтомуvar wg, mu = sync.WaitGroup{}, sync.Mutex{}объявляет обе переменные готовыми к работе; Mutexнельзя копировать;- структуру с полем
sync.Mutexпередают по указателю, а её методы объявляют с получателем-указателем.
Правила работы с Mutex
Здесь аналогия с ключом заканчивается. Настоящая дверь без ключа не откроется, а Mutex никого не держит силой: горутина, которая не вызвала Lock, всё равно дотянется до n. Отсюда три правила:
Lockдолжны вызывать все горутины, которые трогают общие данные;- читать общую переменную тоже нужно под
Mutex, если другие горутины в неё пишут; - между
LockиUnlockдержат как можно меньше кода, потому что остальные горутины в это время ждут.
Частая ошибка
Функция вызвала Lock, но на одном из путей вышла, не вызвав Unlock:
var mu sync.Mutex
var stock = 5
func take(n int) bool {
mu.Lock()
if stock < n {
return false // ✗
}
stock -= n
mu.Unlock()
return true
}Ранний return блокирует все следующие вызовы take:
- После вызова
take(10)Mutexостаётся запертым: повар унёс ключ с собой. - Следующий вызов
takeждёт наLockвечно. - Если так ждут все горутины, программа останавливается с
fatal error: all goroutines are asleep - deadlock!.
Исправление — defer mu.Unlock() сразу после mu.Lock(): тогда Mutex отпирается при любом выходе из функции.
Главное
- Гонка данных — одновременный доступ к переменной, где хотя бы одна горутина пишет; флаг
-raceеё находит. - Одновременная запись в
mapбез защиты может остановить программу сfatal error: concurrent map writes. mu.Lock()иmu.Unlock()пускают к данным одну горутину за раз;Lockвызывают все, кто трогает данные.defer mu.Unlock()сразу послеLockотпираетMutexпри любом выходе;Mutexне копируют.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.