Ответ
Горутины выполняются в одном адресном пространстве, что означает, что у них есть доступ к общей памяти (куче). Каждая горутина имеет свой собственный стек, но куча является общей для всех.
Философия Go гласит: "Не обменивайтесь данными через общую память; обменивайтесь памятью через каналы". Однако на практике используются оба подхода.
Подходы к синхронизации:
1. Каналы (Channels)
Это идиоматичный и предпочтительный способ в Go. Каналы обеспечивают безопасную передачу данных между горутинами. Передавая владение данными через канал, вы гарантируете, что только одна горутина имеет доступ к данным в каждый момент времени.
2. Примитивы синхронизации (пакет sync
)
Традиционный подход, используемый в других языках.
sync.Mutex
: Мьютекс для эксклюзивной блокировки. Только одна горутина может владеть блокировкой в любой момент времени. Используется для защиты критических секций кода.sync.RWMutex
: Блокировка чтения-записи. Позволяет неограниченному числу горутин читать данные одновременно, но запись требует эксклюзивной блокировки.sync.WaitGroup
: Позволяет дождаться завершения группы горутин.sync.Once
: Гарантирует, что определенный участок кода выполнится только один раз.- Атомарные операции (
sync/atomic
): Для простых операций (инкремент, сложение, сохранение/загрузка указателя) атомарные функции могут быть более производительными, чем мьютексы.
Проблема: Гонка данных (Data Race)
Гонка данных возникает, когда две или более горутины одновременно обращаются к одной и той же области памяти, и хотя бы одна из них выполняет запись. Это приводит к непредсказуемому поведению.
Пример гонки данных:
var counter int
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
counter++ // ОПАСНО: одновременная запись из разных горутин
wg.Done()
}()
}
wg.Wait()
fmt.Println("Итоговый счетчик:", counter) // Результат непредсказуем, скорее всего < 1000
}
Решение с помощью sync.Mutex
:
var (
counter int
mu sync.Mutex
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
mu.Lock() // Захватываем блокировку
counter++
mu.Unlock() // Освобождаем блокировку
wg.Done()
}()
}
wg.Wait()
fmt.Println("Итоговый счетчик:", counter) // Всегда 1000
}
Для обнаружения гонок данных в Go есть встроенный инструмент — Race Detector. Его можно активировать флагом -race
:
go run -race main.go