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

Почему нельзя запустить столько же горутин, сколько потоков?

2.0 Middle🔥 131 комментариев
#Основы Go

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

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

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

Почему количество горутин не ограничено числом потоков?

В Go используется модель многопоточности M:N, где множество горутин (G) планируется на меньшем количестве потоков операционной системы (M), привязанных к ядрам процессора. Это фундаментальное отличие от прямого соответствия "один поток = одна горутина".

Архитектура планировщика Go (GMP)

  • G (Goroutine) — легковесная сопрограмма, управляемая рантаймом Go.
  • M (Machine) — поток ОС (kernel thread), который выполняет код.
  • P (Processor) — логический процессор, контекст для планирования горутин.
// Пример: запуск 1000 горутин при 4 ядрах процессора
package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    fmt.Printf("Логических процессоров: %d\n", runtime.GOMAXPROCS(0))
    
    for i := 0; i < 1000; i++ {
        go func(id int) {
            time.Sleep(time.Second)
            fmt.Printf("Горутина %d завершена\n", id)
        }(i)
    }
    
    time.Sleep(2 * time.Second)
}

Ключевые причины отсутствия жесткой связи

1. Горутины — не потоки ОС

  • Поток ОС имеет фиксированный размер стека (~1-8 МБ), создание тысяч потоков расходует гигабайты памяти. Горутины начинают с 2 КБ стека, динамически растущего.
  • Переключение потоков требует дорогостоящего контекстного переключения в ядре ОС. Горутины переключаются в пользовательском пространстве, что в 10-100 раз дешевле.

2. Блокирующие операции не парализуют потоки Когда горутина выполняет блокирующий вызов (системный вызов, I/O), планировщик Go отсоединяет поток M от процессора P и создает новый поток для выполнения других горутин:

func handleConnection(conn net.Conn) {
    buf := make([]byte, 1024)
    // Блокирующее чтение: поток может быть освобожден
    n, err := conn.Read(buf) // Планировщик может запустить другую горутину
    // ...
}

3. Эффективное использование CPU The runtime позволяет переиспользовать потоки ОС для выполнения многих горутин по очереди. Даже на 4-ядерной машине можно эффективно запустить 100 000 горутин.

Практические ограничения

Хотя технически можно запустить миллионы горутин, существуют практические лимиты:

  • Память: каждая горутина потребляет минимум 2 КБ, 1 млн горутин = ~2 ГБ.
  • Накладные расходы планировщика: при десятках тысяч горутин увеличивается время планирования.
  • Ресурсоемкие задачи: CPU-bound горутины эффективнее ограничивать количеством GOMAXPROCS.
// Пример проблем при чрезмерном количестве горутин
func problematic() {
    for i := 0; i < 1000000; i++ {
        go func() {
            // Все горутины одновременно пытаются аллоцировать память
            data := make([]byte, 1024)
            _ = data
        }()
    }
    // Может привести к thrashing планировщика и OOM
}

Заключение

Модель Go сознательно отделяет логическую конкурентность (горутины) от физического параллелизма (потоки ОС). Это позволяет:

  • Декомпозировать задачи на тысячи мелких конкурентных единиц
  • Эффективно обрабатывать I/O-bound задачи (веб-серверы, микросервисы)
  • Минимизировать накладные расходы на переключение контекста
  • Писать простой конкурентный код без ручного управления пулами потоков

Количество потоков ОС обычно равно GOMAXPROCS + дополнительные потоки для блокирующих операций, но количество горутин ограничено лишь памятью и здравым смыслом архитектора системы.

Почему нельзя запустить столько же горутин, сколько потоков? | PrepBro