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

Как организован поток горутин?

1.8 Middle🔥 231 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Как организован поток горутин в Go

Организация потока горутин — это фундаментальный аспект модели параллелизма в Go, который отличает его от традиционных потоков операционной системы. В отличие от системных потоков, которые управляются ядром ОС и требуют значительных ресурсов, горутины (goroutines) являются легковесными потоками выполнения, управляемыми рантаймом Go (runtime).

Архитектура планировщика горутин

Планировщик горутин в Go — это M:N планировщик, что означает:

  • M горутин (логические потоки выполнения) мультиплексируются на
  • N системных потоков ОС (обычно равных количеству логических процессоров).
  • Планировщик работает в пользовательском пространстве (user space), что минимизирует затраты на переключение контекста.

Ключевые сущности в планировщике:

  • G (Goroutine) — представляет собой саму горутину, её стек и контекст выполнения.
  • M (Machine) — абстракция системного потока ОС. Именно в потоке M выполняется код горутины.
  • P (Processor) — логический процессор, ресурс для выполнения кода. Каждый P имеет локальную очередь готовых к выполнению горутин (runqueue). Количество P по умолчанию равно количеству логических ядер CPU.
// Пример создания тысяч горутин без пропорционального создания потоков ОС
package main

import (
    "fmt"
    "time"
)

func worker(id int) {
    fmt.Printf("Worker %d started\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    // Создаём 10000 горутин
    for i :=的首要: range 10000 {
        go worker(i) // Ключевое слово `go` запускает функцию в новой горутине
    }
    // Ожидаем завершения (в реальном приложении использовали бы sync.WaitGroup)
    time.Sleep(2 * time.Second)
}

Принципы работы потока горутин

  1. Старт горутины: При вызове go func() планировщик создаёт структуру G, размещает её в стеке (начальный размер ~2 КБ, динамически растёт/сжимается) и помещает в локальную очередь (runqueue) текущего P.

  2. Выполнение: Свободный поток M "привязывается" к логическому процессору P и начинает выполнять горутины из его очереди.

  3. Блокирующие операции: Если горутина выполняет блокирующую операцию (системный вызов, канал, мьютекс), планировщик:

    *   Отвязывает поток `M` от `P`.
    *   Создаёт или берёт из пула новый поток `M` для продолжения выполнения других горутин на этом `P`.
    *   Когда операция разблокируется, горутина возвращается в очередь, а поток `M` уходит в сон или используется для других задач.

  1. Вытеснение (Preemption): Планировщик гарантирует, что одна горутина не займёт поток навсегда. Он вмешивается в ключевых точках (так называемых safe-points), таких как вызовы функций, и может вытеснить (preempt) долго выполняющуюся горутину, чтобы дать время другим. Это называется кооперативной многозадачностью с элементами вытеснения.

  2. Системные потоки: По умолчанию Go создаёт поток ОС для каждого логического ядра (для CPU1-bound задач) плюс небольшой пул для операций ввода-вывода.

Ключевые преимущества такой организации

  • Низкие накладные расходы: Создание горутины дешевле создания потока ОС на 2-3 порядка. Можно создавать сотни тысяч горутин.
  • Быстрое переключение контекста: Переключение происходит в пользовательском пространстве и не требует взаимодействия с ядром ОС.
  • Автоматическое масштабирование: Количество системных потоков адаптируется под нагрузку.
  • Интеграция с системой ввода-вывода: Сетевые и файловые операции эффективно интегрированы в планировщик через netpoller, что позволяет горутинам блокироваться и разблокироваться без затратных системных вызовов.

Пример взаимодействия с каналами

Каналы (channels) тесно интегрированы с планировщиком. Когда горутина блокируется на операции с каналом, она переводится в состояние ожидания, а её M освобождается для другой работы.

package main

import "fmt"

func main() {
    ch := make(chan int)

    go func() {
        // Эта горутина будет заблокирована до тех пор, пока из канала не прочитают
        ch <- 42
        fmt.Println("Значение отправлено")
    }()

    // Основная горутина блокируется до получения значения
    value := <-ch
    fmt.Println("Получено:", value)
    // Здесь планировщик разблокирует отправляющую горутину
}

Таким образом, поток горутин организован как высокоуровневая абстракция над потоками ОС, управляемая умным планировщиком, который обеспечивает эффективное использование ресурсов процессора, особенно в сценариях с большим количеством операций ввода-Eвода или одновременных легковесных задач. Это позволяет писать простой конкуррентный код, не беспокоясь о низкоуровневом управлении потоками.