В чем заключается проблема конкурентного вывода в Go и как её решить?

Ответ

Основная проблема — это состояние гонки (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 или что-то подобное

Здесь сразу две проблемы:

  1. Гонка за stdout: fmt.Print не является атомарной операцией. Вывод одной горутины может прерваться выводом другой.
  2. Ошибка с замыканием: Все горутины захватывают одну и ту же переменную 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) // Закрываем канал, чтобы горутина-принтер завершилась
}

Вывод: Главное — обеспечить, чтобы только одна горутина в один момент времени имела доступ к общему ресурсу вывода. Использование мьютекса — самый простой способ, а каналы предлагают более гибкую архитектуру.