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

Как устроены горутины в Go?

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

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

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

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

Как устроены горутины в Go?

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

Архитектура и компоненты

  1. Планировщик Go (Goroutine Scheduler): Это кооперативный планировщик, работающий в пространстве пользователя (user-space). Он не использует прерывания от ОС, а основывается на кооперативной многозадачности: горутины сами сигнализируют планировщику, когда они готовы уступить контроль (например, при операциях с каналами, системных вызовах, вызовах runtime.Gosched()).

  2. Системные потоки (OS Threads): Планировщик работает на пуле системных потоков (обычно равном количеству логических ядер CPU). Каждая горутина не закреплена на конкретный поток — планировщик может перемещать горутины между потоками для балансировки нагрузки и эффективного использования ресурсов.

  3. Контекст горутины: Горутина имеет очень маленький контекст (начальный размер стека около 2 КБ, динамически расширяется/сжимается), что позволяет создавать их массово без больших затрат памяти. В отличие от системных потоков (где контекст обычно 1-2 МБ), это одна из ключевых причин их легковесности.

Управление жизненным циклом

package main

import (
    "fmt"
    "time"
)

func main() {
    // Создание горутины с ключевым словом `go`
    go func() {
        fmt.Println("Горутина запущена")
    }()
    
    // Планировщик автоматически управляет выполнением
    time.Sleep(100 * time.Millisecond) // Для демонстрации
}
  • Создание: Используется ключевое слово go. Runtime создает структуру данных, представляющую горутину, и помещает ее в очередь планировщика.
  • Выполнение: Планировщик выбирает горутину из очереди и связывает ее с доступным системным потоком (M).
  • Блокировка: Если горутина блокируется (например, на операции с каналами или I/O), планировщик может отделить ее от потока и запустить другую горутину на этом же потоке, чтобы сохранить производительность.
  • Завершение: Горутина завершается при возврате из своей функции. Планировщик освобождает ресурсы и может очистить стек.

Внутренние структуры runtime

Планировщик Go использует три основные абстракции:

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

Механизм работает примерно так:

  1. Каждый P имеет локальную очередь G.
  2. M связывается с P для выполнения горутин.
  3. Когда локальная очередь P пуста, он забирает горутины из глобальной очереди или других P.

Ключевые преимущества архитектуры

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

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

package main

import (
    "fmt"
    "sync"
)

func worker(id int, ch chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for task := range ch {
        fmt.Printf("Worker %d processing task %d\n", id, task)
    }
}

func main() {
    ch := make(chan int, 10)
    var wg sync.WaitGroup
    
    // Запускаем несколько горутин-воркеров
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, ch, &wg)
    }
    
    // Отправляем задачи в канал
    for j := 1; j <= 5; j++ {
        ch <- j
    }
    
    close(ch)
    wg.Wait() // Ожидаем завершения всех горутин
}

В этом примере планировщик Go автоматически распределяет три горутины-воркера между доступными системными потоками, обеспечивая параллельную обработку задач из канала. При блокировке на операции range ch горутина может быть перепланирована, позволяя другим использовать тот же системный поток.

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

Как устроены горутины в Go? | PrepBro