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

Как горутины распределяются по потокам?

2.0 Middle🔥 172 комментариев
#Конкурентность и горутины#Производительность и оптимизация

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

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

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

Распределение горутин по потокам в Go

В языке Go горутины (легковесные потоки выполнения) распределяются по потокам операционной системы (OS threads) с помощью компонента рантайма под названием планировщик (scheduler). Это многоуровневая система, обеспечивающая эффективную параллельную и конкурентную работу.

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

Go использует модель G-M-P, где:

  • G (Goroutine) — горутина, содержащая стек, инструкции и контекст выполнения.
  • M (Machine) — поток операционной системы (OS thread), который выполняет код.
  • P (Processor) — логический процессор, который связывает горутины с потоками. Количество P по умолчанию равно количеству логических ядер CPU (GOMAXPROCS).

Процесс распределения

Инициализация: При запуске программы Go создает пул потоков M (зависит от ОС) и набор процессоров P (по GOMAXPROCS). Каждый P имеет локальную очередь горутин (runqueue).

Распределение горутин:

  1. Новая горутина помещается в локальную очередь своего P.
  2. Если локальная очередь переполнена, половина горутин перемещается в глобальную очередь (общую для всех P).
  3. Поток M привязывается к P и выполняет горутины из его локальной очереди по принципу FIFO.
  4. Если локальная очередь пуста, P забирает горутины из глобальной очереди или "крадет" половину горутин из очереди другого P (work-stealing).

Пример кода для наблюдения распределения:

package main

import (
    "fmt"
    "runtime"
    "sync"
)

func main() {
    // Устанавливаем количество логических процессоров
    runtime.GOMAXPROCS(2)
    
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Горутина %d выполняется на P\n", id)
        }(i)
    }
    wg.Wait()
}

Ключевые механизмы распределения

  1. Work-stealing (кража работы)
    Если у P пустая очередь, он случайно выбирает другой P и забирает половину горутин из его очереди. Это обеспечивает балансировку нагрузки.

  2. Hand-off (передача)
    Если горутина блокируется (например, на системном вызове или канале), P может отдать свой поток M другой горутине, чтобы не простаивать.

  3. Системные вызовы
    При блокирующем системном вызове (например, файловый I/O) Go отделяет поток M от P, чтобы P мог обслуживать другие горутины. После завершения вызова горутина возвращается в очередь.

  4. Сетевые операции
    Для неблокирующих сетевых операций используется Netpoller — компонент, интегрирующийся с планировщиком через event-модель (epoll/kqueue). Горутины блокируются на Netpoller, а потоки освобождаются для другой работы.

Управление потоками

  • Создание потоков: Если все M заблокированы, а есть готовые к выполнению горутины, рантайм создает новый поток.
  • Спиннинг (spinning): Небольшое количество потоков M может находиться в активном ожидании работы для уменьшения задержек.
  • Ограничения: Максимальное количество потоков по умолчанию — 10000, но можно изменить через runtime/debug.SetMaxThreads.

Пример с блокирующей операцией

package main

import (
    "fmt"
    "time"
)

func worker(id int) {
    time.Sleep(2 * time.Second) // Блокирующая операция
    fmt.Printf("Worker %d завершил работу\n", id)
}

func main() {
    for i := 0; i < 5; i++ {
        go worker(i)
    }
    time.Sleep(3 * time.Second)
}

Здесь при вызове time.Sleep горутина временно освобождает поток, позволяя выполнять другие горутины.

Настройка и мониторинг

  • GOMAXPROCS: Определяет количество логических процессоров P.
  • Trace tool: Инструмент go tool trace визуализирует распределение горутин.
  • Runtime stats: Через runtime.NumGoroutine(), runtime.NumCPU().

Преимущества подхода Go

  • Эффективность: Миллионы горутин работают на ограниченном числе потоков ОС.
  • Масштабируемость: Work-stealing балансирует нагрузку.
  • Низкие задержки: Netpoller и spin-потоки уменьшают overhead.

Таким образом, планировщик Go динамически распределяет горутины по потокам ОС, обеспечивая высокую производительность конкурентных программ.