← Назад к вопросам

Что происходит при записи в канал из двух горутин одновременно?

2.7 Senior🔥 81 комментариев
#Конкурентность и горутины

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Общий принцип работы каналов

При записи в канал из двух или более горутин "одновременно" (параллельно) происходит следующее: операции записи выполняются последовательно, а не одновременно, поскольку каналы в Go являются синхронизирующими примитивами и обеспечивают взаимное исключение (mutex) для операций отправки/приема.

Механизм синхронизации

Каналы в Go реализованы как структуры данных с внутренней очередью (обычно FIFO) и механизмом блокировки. Когда несколько горутин пытаются записать в небуферизованный канал или в буферизованный канал с заполненным буфером:

  1. Только одна горутина может выполнить запись в любой момент времени
  2. Остальные горутины блокируются до тех пор, пока не освободится возможность записи
  3. Порядок разблокировки горутин зависит от реализации планировщика Go (обычно в порядке FIFO)

Пример с небуферизованным каналом:

package main

import (
    "fmt"
    "time"
)

func sender(id int, ch chan<- int) {
    fmt.Printf("Горутина %d пытается записать\n", id)
    ch <- id
    fmt.Printf("Горутина %d успешно записала\n", id)
}

func main() {
    ch := make(chan int) // Небуферизованный канал
    
    go sender(1, ch)
    go sender(2, ch)
    
    time.Sleep(100 * time.Millisecond)
    
    // Читаем два значения
    fmt.Println("Прочитано:", <-ch)
    fmt.Println("Прочитано:", <-ch)
    
    time.Sleep(100 * time.Millisecond)
}

Особенности для разных типов каналов

Небуферизованные каналы:

  • Операция записи блокируется до тех пор, пока другая горутина не прочитает из канала
  • При конкуректной записи из нескольких горутин, первая разблокированная горутина запишет значение
  • Гарантируется синхронизация "точка-в-точку"

Буферизованные каналы:

  • Запись происходит мгновенно, если в буфере есть свободное место
  • Если буфер заполнен, поведение аналогично небуферизованному каналу
  • При частично заполненном буфере несколько горутин могут записывать без блокировки, пока есть свободные слоты
func exampleBuffered() {
    ch := make(chan int, 2) // Буферизованный канал с емкостью 2
    
    // Эти две записи выполнятся мгновенно без блокировки
    go func() { ch <- 1 }()
    go func() { ch <- 2 }()
    
    time.Sleep(10 * time.Millisecond)
    
    // Третья запись заблокируется, так как буфер заполнен
    go func() { 
        ch <- 3
        fmt.Println("Третья запись разблокирована")
    }()
    
    time.Sleep(100 * time.Millisecond)
    <-ch // Освобождаем место в буфере
    time.Sleep(100 * time.Millisecond)
}

Внутренняя реализация

Под капотом каналы используют:

  1. Мьютекс (sync.Mutex) для защиты общего доступа
  2. Очередь ожидающих горутин (отправителей и получателей)
  3. Атомарные операции для счетчиков

Когда горутина блокируется при операции с каналом:

  • Она помещается в соответствующую очередь ожидания (sendq или recvq)
  • Ее контекст сохраняется для последующего возобновления
  • Планировщик Go переключается на выполнение других горутин

Практические последствия

Гарантии:

  • Отсутствие гонок данных (data races) при корректном использовании
  • Гарантированный порядок доставки (значения не теряются)
  • Детерминированное поведение при правильной архитектуре

Рекомендации:

  1. Используйте select с default для неблокирующих операций:
select {
case ch <- value:
    // Успешная запись
default:
    // Канал занят, выполняем альтернативное действие
}
  1. Избегайте блокировок с помощью буферизации или пулов воркеров
  2. Закрывайте каналы только отправителем для предотвращения паники

Распространенный паттерн - конкурентные воркеры:

func workerPool() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    
    // Запускаем пул воркеров
    for w := 1; w <= 5; w++ {
        go func(id int) {
            for job := range jobs {
                // Обработка задачи
                results <- job * 2
            }
        }(w)
    }
    
    // Конкурентная отправка заданий
    for j := 1; j <= 1000; j++ {
        jobs <- j
    }
    close(jobs)
}

Вывод

Каналы в Go обеспечивают безопасную конкурентность за счет внутренней синхронизации. При одновременной записи из нескольких горутин операции сериализуются, исключая состояние гонки. Это фундаментальное свойство делает каналы идеальным инструментом для организации связи между горутинами и построения конкурентных, но потокобезопасных систем.