Ответ
Основная проблема — это состояние гонки (race condition) при одновременном доступе нескольких горутин к общему ресурсу, такому как os.Stdout
. Без синхронизации вывод от разных горутин может перемешиваться, что приводит к некорректному и нечитаемому результату.
Пример проблемы
Рассмотрим код, который пытается вывести числа от 0 до 9 из разных горутин:
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Проблема №1: Race condition на stdout
// Проблема №2: Замыкание захватывает последнюю версию `i`
fmt.Print(i)
}()
}
wg.Wait()
}
// Возможный вывод: 9101010101010101010 или что-то подобное
Здесь сразу две проблемы:
- Гонка за
stdout
:fmt.Print
не является атомарной операцией. Вывод одной горутины может прерваться выводом другой. - Ошибка с замыканием: Все горутины захватывают одну и ту же переменную
i
. К моменту их запуска цикл, скорее всего, уже завершится, иi
будет равно10
для всех.
Решения
Для решения этой проблемы необходимо синхронизировать доступ к общему ресурсу.
1. Использование sync.Mutex
Это самый прямой способ. Мы защищаем операцию вывода мьютексом, гарантируя, что только одна горутина может выполнять ее в один момент времени.
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < 10; i++ {
wg.Add(1)
go func(val int) { // Правильная передача переменной
defer wg.Done()
mu.Lock() // Блокируем доступ
fmt.Printf("%d ", val)
mu.Unlock() // Освобождаем
}(i) // Передаем копию `i` в горутину
}
wg.Wait()
}
// Гарантированный вывод: числа от 0 до 9 в случайном порядке, но не перемешанные.
2. Использование каналов
Можно создать одну горутину-"принтер", которая будет получать данные для вывода из канала. Это хороший паттерн для разделения логики.
func main() {
var wg sync.WaitGroup
ch := make(chan int)
// Горутина-принтер
go func() {
for val := range ch {
fmt.Printf("%d ", val)
}
}()
for i := 0; i < 10; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
ch <- val // Отправляем данные в канал, а не в stdout
}(i)
}
wg.Wait()
close(ch) // Закрываем канал, чтобы горутина-принтер завершилась
}
Вывод: Главное — обеспечить, чтобы только одна горутина в один момент времени имела доступ к общему ресурсу вывода. Использование мьютекса — самый простой способ, а каналы предлагают более гибкую архитектуру.