Почему нельзя запустить столько же горутин, сколько потоков?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему количество горутин не ограничено числом потоков?
В 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 + дополнительные потоки для блокирующих операций, но количество горутин ограничено лишь памятью и здравым смыслом архитектора системы.