Сколько потоков запускается в Runtime приложения?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Сколько потоков запускается в Runtime Go?
Это один из фундаментальных вопросов о внутреннем устройстве Go. Прямого и фиксированного числа нет. Количество потоков операционной системы (далее — OS-потоков), которые создает и использует Go runtime, динамически изменяется в зависимости от нагрузки, конфигурации и выполняемого кода. Однако можно выделить ключевые компоненты и механизмы, которые создают эти потоки.
Основные категории потоков в Go Runtime
- Поток для выполнения goroutine (M)
* В модели планировщика Go (**GMP**: Goroutine, Machine (thread), Processor) `M` — это машина, представляющая собой OS-поток.
* Изначально при старте программы создается один такой поток для выполнения `main` goroutine.
* **Количество `M` динамически растет и сокращается** по мере необходимости. Если все существующие `M` заняты (например, они заблокированы на системных вызовах или ожидают работы), планировщик может создать новый `M`, чтобы продолжить выполнение готовых к работе goroutine. Максимальное количество `M` по умолчанию — 10 000 (управляется переменной `runtime/debug.SetMaxThreads`).
- Потоки для системных задач runtime
* **Сборщик мусора (GC)**: Для выполнения фоновой и параллельной сборки мусора GC использует выделенные OS-потоки. Их количество зависит от значения `GOMAXPROCS` и настроек GC (`GOGC`, `debug.SetGCPercent`).
* **Системный мониторинг (sysmon)**: Запускается специальный **неблокирующий системный поток** `sysmon`. Он не привязан к `P` и выполняет критически важные фоновые задачи:
* Вызывает сборщик мусора.
* Освобождает `P` и `M` от заблокированных на сетевых операциях goroutine, вытесняя их в отдельную сетевую подсистему.
* Вытесняет («preempt») goroutine, которые слишком долго выполняются, чтобы обеспечить справедливость планирования.
- Потоки, созданные для блокирующих системных вызовов и cgo
* Когда goroutine выполняет **блокирующий системный вызов** (например, файловый ввод-вывод, который не интегрирован в сетевой поллинг), runtime может временно отделить `M` от `P` и создать **новый `M`** для работы на освободившемся `P`. Это позволяет другим goroutine продолжать выполнение, не дожидаясь окончания блокирующей операции.
* Вызовы функций через **cgo** также выполняются в отдельном потоке, чтобы не блокировать планировщик Go.
- Потоки, созданные для netpoller (сетевого поллинга)
* Go использует асинхронную модель ввода+вывода для сетевых операций. **Netpoller** — это внутренний компонент, который, используя механизмы поллинга ОС (epoll, kqueue, IOCP), обрабатывает множество сетевых запросов.
* Изначально netpoller может использовать уже существующие потоки планировщика. Важно, что goroutine, ожидающая сетевых данных, **не блокирует поток (`M`)**, а лишь ставится в очередь ожидания события от netpoller.
Практический пример и вывод
Рассмотрим простую программу:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
fmt.Println("Количество логических CPU:", runtime.NumCPU())
fmt.Println("Количество goroutine:", runtime.NumGoroutine())
fmt.Println("Количество потоков (приблизительно): будет расти")
// Запускаем много горутин, часть из которых будет блокироваться
for i := 0; i <促00; i++ {
go func(id int) {
time.Sleep(10 * time.Second) // Блокирующая операция (sleep)
}(i)
}
time.Sleep(2 * time.Second)
// В этот момент runtime уже мог создать дополнительные потоки (M)
// для обслуживания других горутин, пока часть 'M' занята sleep.
}
Итог: Количество потоков в Go runtime — это адаптивная величина. Минимально — это 1 поток для main + поток sysmon + возможно потоки для GC. Максимально — может достигать тысяч, если приложение активно использует блокирующиеся вызовы или CGO. Основная философия заключается в том, чтобы **маппить множество легковесных goroutine на меньшее число OS-
потоков**, эффективно управляя их созданием и утилизацией, что и обеспечивает высокую конкурентность при относительно низких накладных расходах.