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

В чем разница между горутиной и системным thread?

1.3 Junior🔥 212 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Разница между горутиной и системным потоком

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

Ключевые различия

АспектГорутина (Goroutine)Системный поток (OS Thread)
Уровень абстракцииПользовательский (управляется рантаймом Go)Системный (управляется ядром ОС)
Размер стекаДинамический (от 2 КБ, может расти/уменьшаться)Фиксированный (обычно 1-8 МБ, зависит от ОС)
Стоимость созданияОчень низкая (~2 КБ памяти, быстрая инициализация)Высокая (~1 МБ памяти, медленный системный вызов)
Стоимость переключения контекстаНизкая (пользовательское переключение в пространстве процесса)Высокая (переход в ядро, сохранение регистров)
ПланировщикПланировщик Go (кооперативный + вытесняющий)Планировщик ОС (вытесняющий)
КоличествоТысячи/миллионы одновременноСотни/тысячи (ограничено ресурсами)

Архитектурная модель Go: M-P-G

Рантайм Go использует модель M-P-G для управления параллелизмом:

  • G (Goroutine) — исполняемая горутина
  • M (Machine) — системный поток (OS thread)
  • P (Processor) — логический процессор (контекст выполнения)
// Пример: создание тысяч горутин практически без нагрузки
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    start := time.Now()
    
    // Создаем 10000 горутин
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // Имитация работы
            time.Sleep(1 * time.Millisecond)
        }(i)
    }
    
    wg.Wait()
    fmt.Printf("10000 горутин выполнены за %v\n", time.Since(start))
}

Детальное сравнение

1. Управление памятью и стеком

  • Горутины имеют сегментированные динамические стеки, которые начинаются с ~2 КБ и могут автоматически расти (до 1 ГБ) и сокращаться. Это позволяет эффективно использовать память при миллионах горутин.
  • Системные потоки используют фиксированные стеки, выделяемые при создании (обычно 1-8 МБ). Создание тысяч потоков быстро исчерпывает память.

2. Планирование выполнения

  • Планировщик Go управляет горутинами кооперативно-вытесняющим образом:
    • Горутины добровольно уступают выполнение в точках блокировки (каналы, системные вызовы)
    • С версии Go 1.14 работает асинхронное вытеснение при длительных вычислениях
  • Планировщик ОС использует вытесняющую многозадачность с фиксированными квантами времени, что требует дорогостоящих переключений контекста.

3. Отображение на системные потоки

Горутины мультиплексируются на пуле системных потоков:

// Пример, показывающий конкурентное выполнение на ограниченном числе потоков
package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    // Устанавливаем максимальное количество системных потоков
    runtime.GOMAXPROCS(2)
    
    for i := 0; i < 10; i++ {
        go func(id int) {
            for j := 0; j < 3; j++ {
                fmt.Printf("Горутина %d, итерация %d\n", id, j)
                time.Sleep(10 * time.Millisecond)
            }
        }(i)
    }
    
    time.Sleep(1 * time.Second)
}

Практические последствия

Преимущества горутин:

  • Масштабируемость: возможность создавать миллионы одновременных горутин
  • Эффективность: быстрый запуск и переключение без перехода в ядро
  • Интеграция с каналами: встроенная синхронизация через каналы (channels) и примитивы sync
  • Простота программирования: легковесная альтернатива callback-ам и async/await

Случаи, когда важны системные потоки:

  • Системные вызовы: блокирующие операции ОС (например, файловый ввод/вывод до Go 1.5)
  • C-библиотеки: вызовы функций, блокирующих текущий поток
  • Параллелизм CPU-bound задач: эффективное использование нескольких ядер

Специфическое поведение в Go

  1. Блокирующие операции:

    • Сетевые операции через netpoller не блокируют системные потоки
    • Системные вызовы могут блокировать поток, но планировщик создает новый при необходимости
  2. Runtime LockOSThread:

    // Привязка горутины к конкретному системному потоку
    func main() {
        runtime.LockOSThread()
        defer runtime.UnlockOSThread()
        // Код выполняется на закрепленном системном потоке
    }
    
  3. Состояния горутин:

    • Выполняемые (executing)
    • Ожидающие (waiting)
    • Runnable (готовы к выполнению)
    • Системные вызовы (syscall)

Заключение

Горутины — это высокоуровневая абстракция над системными потоками, обеспечивающая более эффективный параллелизм за счет:

  • Динамического управления памятью стека
  • Кооперативного планирования в пользовательском пространстве
  • Мультиплексирования на пуле системных потоков

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

В чем разница между горутиной и системным thread? | PrepBro