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

Почему можно запустить большое количество горутин на Go, а на других языках нет?

1.0 Junior🔥 251 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Почему Go эффективно поддерживает тысячи горутин

Горутины (goroutines) в Go действительно позволяют запускать десятки и даже сотни тысяч параллельных задач на скромном аппаратном обеспечении, в то время как в большинстве других языков аналогичные механизмы (потоки ОС) имеют серьезные ограничения. Это достигается благодаря уникальной архитектуре модели параллелизма Go, которая фундаментально отличается от подхода, используемого в языках вроде Java, C++ или Python.

Ключевые архитектурные отличия

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

Горутины являются пользовательскими потоками (green threads), управляемыми средой выполнения Go (runtime), а не операционной системой. Это главное концептуальное отличие.

// Запуск 10000 горутин — это тривиально и дешево
for i := 0; i < 10000; i++ {
    go func(id int) {
        // Работа горутины
    }(i)
}

На одном потоке ОС может выполняться множество горутин благодаря кооперативной многозадачности на уровне рантайма.

2. Эффективное управление памятью (стеком)

  • Стек горутины динамический и начинается с маленького размера (обычно 2 КБ в современных версиях).
  • Стек может расти и сжиматься по мере необходимости, экономя память.
  • В отличие от потоков ОС, где стек фиксирован (часто 1-8 МБ на поток, зависит от ОС).
АспектГорутина (Go)Поток ОС (Java/C++)
Начальный размер~2 КБ1-8 МБ (зависит от ОС)
УправлениеРантайм GoЯдро ОС
ПереключениеБыстрое (на уровне Go)Медленное (системный вызов)
Макс. количествоДесятки/сотни тысячОбычно сотни-тысячи

Следствие: 100 000 горутин потребуют ~200 МБ памяти только на стеки, а 100 000 потоков ОС — минимум 100 ГБ, что непрактично.

3. Планировщик Go (scheduler)

Планировщик Go — это умный менеджер пользовательских потоков, который мультиплексирует горутины на ограниченном количестве потоков ОС (обычно равном количеству ядер CPU). Он работает в пространстве пользователя, избегая дорогих системных вызовов.

// Планировщик эффективно распределяет горутины по потокам ОС
// M (thread) — поток ОС, P (processor) — контекст планировщика, G — горутина
// M:P:G модель обеспечивает эффективное планирование

Принципы работы планировщика:

  • Work-stealing: незанятые потоки ОС могут "украсть" задачи у занятых
  • Кооперативная многозадачность: горутины сами уступают контроль в точках блокировки
  • Интеграция с каналами и системными вызовами: планировщик знает о блокировках

4. Точки уступки (yield points)

Горутины добровольно уступают управление в определенных точках:

  • При операциях с каналами (chan)
  • При системных вызовах (через интегрированный сетевой планировщик)
  • При вызовах time.Sleep(), runtime.Gosched()
  • При работе с мьютексами и другими примитивами синх\ронизации
// Пример точки уступки — операция с каналом
msg := <-ch // Здесь горутина может быть приостановлена

Сравнение с другими языками

Java/C++ (потоки ОС)

// Создание 10000 потоков в Java — дорого и непрактично
for (int i = 0; i < 10000; i++) {
    new Thread(() -> {
        // Работа потока
    }).start();
}
  • Каждый поток = отдельный стек 1+ МБ
  • Переключение контекста требует системного вызова (дорого)
  • ОС ограничивает общее количество потоков

Python (async/await)

# Асинхронные задачи в Python
async def task():
    await asyncio.sleep(0.1)

# Можно запустить много задач, но...
  • GIL (Global Interpreter Lock) ограничивает истинный параллелизм
  • Асинхронность есть, но параллелизма нет на CPU-задачах
  • Все задачи выполняются в одном потоке

Erlang/Elixir (процессы)

  • Наиболее близкая к Go модель (легковесные процессы)
  • Каждый процесс имеет свой стек и heap
  • Виртуальная машина BEAM эффективно планирует процессы
  • Но Go предлагает более простую модель памяти и С-подобный синтаксис

Почему это важно на практике

  1. Микросервисная архитектура: Один сервис может обрабатывать десятки тысяч одновременных подключений
  2. Высоконагруженные API: Каждый запрос может быть обработан в отдельной горутине
  3. Параллельная обработка данных: Легко распараллелить обработку без опасений исчерпать ресурсы
  4. Реактивные системы: Множество независимых агентов могут работать параллельно
// Пример: HTTP-сервер обрабатывающий 10000 одновременных запросов
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    // Каждый запрос — отдельная горутина
    go processRequest(r)
})
http.ListenAndServe(":8080", nil)

Ограничения и предостережения

Хотя горутины дешевые, они не бесплатные:

  • Управление памятью: Большое количество горутин увеличивает нагрузку на GC
  • Блокирующие операции: Горутина, выполняющая CPU-интенсивную работу без точек уступки, может "заморозить" планировщик
  • Ресурсы: Каждая горутина потребляет память, хоть и небольшую

Вывод

Возможность запускать десятки тысяч горутин в Go — результат:

  1. Дизайна языка с учетом параллелизма с самого начала
  2. Эффективной реализации стека (растущий, начинается с 2 КБ)
  3. Умного планировщика в пространстве пользователя, избегающего системных вызовов
  4. Кооперативной модели с точками уступки контроля

Это делает Go идеальным выбором для высоконагруженных сетевых сервисов, где необходимо обрабатывать множество одновременных подключений с разумным потреблением ресурсов. В то время как другие языки либо полагаются на дорогие потоки ОС, либо предлагают асинхронность без истинного параллелизма, Go нашел баланс между производительностью, простотой использования и эффективностью использования ресурсов.

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