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

Сколько может быть потоков?

1.0 Junior🔥 91 комментариев
#Конкурентность и горутины#Операционные системы и Linux

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

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

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

Отличный и очень глубокий вопрос, который касается самой сути многопоточности и работы Go. Если коротко, то количество потоков (kernel threads) в Go-программе не ограничивается языком и может быть очень большим, но его эффективное количество определяется планировщиком Go (scheduler), основанным на концепции M:N, и обычно близко к количеству доступных логических процессоров (vCPUs). Давайте разберем это подробно.

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

Чтобы понять логику, нужно знать три ключевые абстракции:

  • G (Goroutine) — горутина, легковесный поток, управляемый рантаймом Go. Это то, что вы создаете с помощью go func(). Их могут быть сотни тысяч и миллионы.
  • M (Machine) — машина, или поток операционной системы (OS thread). Именно он исполняется на физическом/логическом ядре. Планировщик ОС управляет им.
  • P (Processor) — процессор, или контекст планировщика. Это виртуальный ресурс, который связывает M и G. Количество P по умолчанию равно GOMAXPROCS (обычно числу логических ядер).

Планировщик Go реализует модель M:N, где M OS-потоков обслуживает N горутин. Это значит, что количество OS-потоков (M) не равно количеству горутин.

От чего зависит количество потоков (M)?

  1. Базовое количество (для CPU-работы):
    *   Для эффективного использования CPU-ресурсов планировщик стремится иметь число **"рабочих" M примерно равным числу P**.
    *   Каждый P привязан к одному M, который исполняет горутины из локальной очереди этого P. Если `GOMAXPROCS=8`, то у вас будет до 8 "рабочих" потоков, выполняющих полезную CPU-нагрузку.

  1. Дополнительные потоки для системных вызовов (syscalls):
    *   Когда горутина выполняет **блокирующий системный вызов** (например, чтение файла, сетевой запрос без использования netpoller), текущий поток (M) блокируется.
    *   Чтобы не простаивал P, рантайм Go создает **новый OS-поток (M)**, который берет этот P и продолжает исполнять другие горутины.
    *   **Теоретически, каждый P может породить такой поток при блокировке.** Это означает, что в пике потоков может быть больше, чем `GOMAXPROCS`. Существует лимит `runtime.SetMaxThreads()` (по умолчанию 10 000), который защищает от полного исчерпания системных ресурсов.

```go
// Пример, где могут создаваться дополнительные потоки
for i := 0; i < 100; i++ {
    go func() {
        // Блокирующий системный вызов
        data, err := os.ReadFile("largefile.txt")
        // ...
    }()
}
```

3. Потоки для cgo и другие служебные потоки:

    *   Вызовы **C-кода через cgo** могут выполняться в специальных, отдельно созданных потоках, чтобы не мешать планировщику.
    *   Также есть служебные потоки для сборщика мусора (GC), мониторинга `sysmon` и т.д.

  1. Работа с сетью (Netpoller) - ключевая оптимизация:
    *   Go использует асинхронный механизм ввода-вывода (на основе `epoll`/`kqueue`/`IOCP`), интегрированный в планировщик.
    *   Когда горутина выполняет сетевую операцию, она не блокирует поток (M). Вместо этого она регистрируется в **netpoller**, а поток M освобождается для выполнения других горутин.
    *   **Это кардинально уменьшает необходимость в создании новых потоков для сетевых операций** и является одной из причин высокой эффективности Go для сетевых сервисов.

```go
// Пример, где НЕ создаются новые потоки благодаря netpoller
for i := 0; i < 10000; i++ {
    go func() {
        resp, err := http.Get("https://example.com") // Неблокирующий вызов
        // ...
    }()
}
```

Практические лимиты и рекомендации

  • Мягкий лимит по умолчанию: runtime/debug.SetMaxThreads(10000). При его достижении программа упадет с ошибкой runtime: program exceeds 10000-thread limit.
  • Реальный эффективный лимит сильно зависит от нагрузки:
    *   **CPU-bound задачи**: оптимально ~`GOMAXPROCS` потоков.
    *   **IO-bound задачи с блокирующими syscalls (файлы, синхронный диск)**: потоков может быть `GOMAXPROCS + число одновременно блокированных горутин`. Много.
    *   **IO-bound задачи с неблокирующим IO (сеть)**: потоков остается около `GOMAXPROCS`, так как netpoller эффективно переиспользует их.

Итог

Количество потоков в Go — это динамическая величина, управляемая рантаймом.

  1. Минимум/ядро: GOMAXPROCS потоков для выполнения CPU-задач.
  2. Максимум/пик: Может достигать тысяч, если множество горутин одновременно выполняют блокирующие системные вызовы, не охваченные асинхронным планировщиком (например, файловый ввод-вывод, синхронные вызовы C-библиотек). Лимит по умолчанию — 10 000.
  3. Оптимизация: Для сетевых операций благодаря netpoller количество потоков остается близким к минимуму, что позволяет одной программе эффективно обрабатывать десятки и сотни тысяч одновременных соединений.

Вывод для разработчика: Не нужно думать о потоках. Нужно думать о горутинах. Создавайте их столько, сколько нужно логике приложения. Но помните, что блокирующие операции вне контроля планировщика (тяжелые вычисления в CGO, синхронный файловый ввод-вывод) могут привести к неконтролируемому росту числа потоков и деградации производительности. Для файлового IO, например, стоит использовать асинхронные методы или выносить такие операции в отдельный пул воркеров с ограниченным числом горутин.

Сколько может быть потоков? | PrepBro