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

Кто с кем конкурирует при конкурентности?

2.0 Middle🔥 171 комментариев
#Другое#Основы Go

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

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

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

Природа конкурентности в Go: кто с кем соревнуется?

В контексте программирования на Go (Golang) под конкурентностью понимается структура программы, в которой несколько вычислительных процессов (горутин) выполняются как бы одновременно и независимо, даже если физически они работают на одном ядре CPU. Это — модель проектирования, а не физическая одновременность. Вопрос о том, «кто с кем конкурирует», глубоко связан с внутренним устройством Go и требует многоуровневого рассмотрения.

Главные участники «соревнования»

  1. Горутины (Goroutines) — основные «спортсмены»:
    * Это легковесные потоки, управляемые **рантаймом Go**. Их можно создавать тысячами и миллионами.
    * **Они конкурируют между собой за ресурсы планировщика**. Ресурсы CPU — главный приз. Планировщик Go (часть рантайма) решает, какую горутину запустить на каком **логическом процессоре (M)**, обеспечивая видимость параллельного выполнения.

  1. Планировщик Go (Scheduler) — «рефери и организатор»:
    * Он работает по модели **M:N**:
        * **M (Machine)** — поток ОС (kernel thread). Именно на нём выполняется код.
        * **G (Goroutine)** — сама горутина.
        * **P (Processor)** — логический контекст процессора (до Go 1.14) или абстракция для локальной очереди (ныне). **P** — ключевой ресурс: горутина должна быть привязана к **P**, чтобы выполняться на **M**.
    * Конкуренция происходит **за доступ к `P`** и, как следствие, к потоку **M**.

Условия возникновения реальной «конкуренции»

1. Неблокирующие горутины (CPU-bound задачи)

Когда горутина выполняет расчёты, не вызывая системных вызовов или операций ввода-вывода (I/O), она занимает P и M. Чтобы дать ход другим, планировщик принудительно переключает горутины в ключевых точках (напр., при вызове функций, в бесконечных циклах без вызовов runtime.Gosched()). Пример:

func main() {
    runtime.GOMAXPROCS(1) // Ограничимся одним P для наглядности
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("Горутина 1:", i)
        }
    }()
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("Горутина 2:", i)
        }
    }()
    time.Sleep(time.Second)
}

Здесь две горутины конкурируют за один P. Планировщик будет переключать их, создавая иллюзию параллелизма.

2. Блокирующие операции (I/O, системные вызовы, синхронизация)

При вызове time.Sleep, чтении из сети или канала, блокировке мьютексом горутина отпускает P. Этот P немедленно может быть передан другой готовой горутине из очереди. Пример конкуренции за канал:

func worker(id int, ch chan string) {
    ch <- fmt.Sprintf("Работник %d выполнил задачу", id)
}

func main() {
    ch := make(chan string, 2)
    for i := 1; i <= 5; i++ {
        go worker(i, ch)
    }
    // Все 5 горутин пытаются записать в буферизованный канал ёмкостью 2.
    // Первые две получат доступ сразу, остальные будут ждать (блокироваться),
    // конкурируя за освободившееся место.
}

3. Конкуренция за доступ к общим данным (Race Condition)

Разные горутины могут конкурировать за изменение одной переменной в памяти. Без синхронизации это приводит к состоянию гонки (data race):

var counter int
var wg sync.WaitGroup

func increment() {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        counter++ // Здесь горутины конкурируют за доступ к `counter`
    }
}

func main() {
    wg.Add(2)
    go increment()
    go increment()
    wg.Wait()
    fmt.Println(counter) // Результат может быть меньше 2000!
}

Для защиты используется мьютексы (sync.Mutex), за которые горутины также конкурируют, чтобы получить эксклюзивный доступ.

Ключевые выводы

  • Главные конкурентыгорутины, борющиеся за ресурсы планировщика (P и M) и процессорное время.
  • Уровни конкуренции:
    1. **Системный**: горутины → P → M → ядро CPU.
    2. **Пользовательский**: конкуренция за общие данные (требует примитивов синхронизации: `mutex`, `atomic`, каналы).
  • Задача разработчика — правильно организовать эту конкуренцию:
    * Использовать каналы и `select` для коммуникации.
    * Применять `sync.Mutex`, `sync.RWMutex` или атомарные операции для защиты данных.
    * Проектировать программу так, чтобы горутины не блокировали P надолго (разбивать CPU-bound задачи).

Таким образом, конкуренция — это соревнование горутин за вычислительные ресурсы и доступ к общему состоянию, а планировщик Go выступает арбитром, обеспечивающим чередование и реализующим принцип «Не общайтесь через общую память; вместо этого делитесь памятью через общение» (каналы).