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

Как горутина связана с операционным тредом?

2.0 Middle🔥 181 комментариев
#Конкурентность и горутины#Операционные системы и Linux

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

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

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

Связь горутин с операционными тредами в Go

Горутина — это легковесная виртуальная нить выполнения, реализованная в рантайме Go, в то время как операционный тред (OS thread) — это низкоуровневый объект ядра операционной системы. Их связь осуществляется через планировщик Go (scheduler), который является частью рантайма языка и играет ключевую роль в управлении конкурентностью.

Модель выполнения: M-P-G

Планировщик Go использует модель M-P-G, где:

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

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

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

Ключевые аспекты связи

1. Многопоточность без 1:1 соответствия

В отличие от классических потоков ОС, где каждый поток соответствует отдельному треду ядра, горутины имеют много-ко-многим (M:N) отношение с тредами. Несколько горутин могут выполняться на одном треде ОС, и одна горутина в течение жизни может выполняться на разных тредах.

2. Работа планировщика Go

Планировщик управляет распределением горутин по тредам:

// Демонстрация работы планировщика
package main

import (
    "runtime"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    
    // Блокирующая операция - планировщик может переключить контекст
    for i := 0; i < 3; i++ {
        println("Worker", id, "итерация", i)
        time.Sleep(50 * time.Millisecond)
    }
}

func main() {
    runtime.GOMAXPROCS(2) // Используем 2 логических процессора
    
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }
    wg.Wait()
}

3. Кооперативная многозадачность

Горутины используют кооперативное планирование (cooperative scheduling), а не вытесняющее. Переключение между горутинами происходит в точках, называемых "preemption points":

  • Вызовы каналов (chan)
  • Системные вызовы
  • Операции с блокировками (mutex, waitgroup)
  • Явный вызов runtime.Gosched()

4. Системные вызовы и блокирующие операции

При выполнении блокирующего системного вызова:

  • Текущий тред ОС блокируется
  • Планировщик создает новый тред ОС для выполнения других горутин
  • После завершения системного вызова горутина возвращается в очередь

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

Эффективность памяти

// Горутины потребляют значительно меньше памяти
package main

import (
    "runtime"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    
    // Запуск 10000 горутин
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // Легковесная операция
            _ = 1 + 1
        }()
    }
    wg.Wait()
    
    // Для сравнения: 10000 потоков ОС потребовали бы гигабайты памяти
}

Быстрое создание и переключение

  • Создание горутины: ~2KB стека (динамически растущего)
  • Создание потока ОС: ~1-2MB стека
  • Переключение горутин: десятки наносекунд
  • Переключение потоков ОС: микросекунды

Автомасштабирование

Планировщик Go автоматически управляет пулом тредов ОС:

  • Начальный пул тредов создается при старте программы
  • Новые треды добавляются при блокировках
  • Неиспользуемые треды удаляются через некоторое время

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

  1. Нет гарантии параллельности — горутины конкурентны, но параллельность зависит от количества ядер CPU
  2. Состояние гонки (race conditions) — хотя горутины легковесны, разделяемая память требует синхронизации
  3. Работа с блокирующими операциями — длительные блокирующие вычисления могут "заморозить" планировщик
// Проблема блокирующих вычислений
package main

import (
    "runtime"
    "time"
)

func blockingTask() {
    // Длительное вычисление без точек прерывания
    sum := 0
    for i := 0; i < 1e10; i++ {
        sum += i
    }
}

func main() {
    // Запускаем горутину с блокирующим вычислением
    go blockingTask()
    
    // Другие горутины могут не получить время CPU
    go func() {
        for i := 0; i < 5; i++ {
            println("Другая горутина", i)
            time.Sleep(100 * time.Millisecond)
        }
    }()
    
    time.Sleep(time.Second)
}

Заключение

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

Как горутина связана с операционным тредом? | PrepBro