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

Всегда ли планировщик вытесняет горутину?

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

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

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

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

Краткий ответ

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

Механизм планирования в Go

Кооперативная модель (основная)

Планировщик Go в основном полагается на кооперативное переключение, где горутины добровольно уступают управление в определенных точках:

// Примеры точек кооперативного вытеснения:
func main() {
    // 1. Вызов time.Sleep - явная уступка управления
    time.Sleep(time.Millisecond)
    
    // 2. Канальные операции (send/receive)
    ch := make(chan int)
    go func() { ch <- 1 }()
    <-ch // Уступка при ожидании
    
    // 3. Системные вызовы (ввод-вывод)
    file, _ := os.Open("file.txt")
    data := make([]byte, 100)
    file.Read(data) // Уступка во время блокировки I/O
    
    // 4. Вызов runtime.Gosched()
    runtime.Gosched() // Явная уступка
    
    // 5. Работа со sync.Mutex и другими примитивами
    var mu sync.Mutex
    mu.Lock()
    // ... критическая секция ...
    mu.Unlock() // Возможная точка уступки
}

Вытесняющая модель (ограниченная)

Начиная с Go 1.14, была добавлена ограниченная вытесняющая поддержка, но с важными ограничениями:

func problematic() {
    // Эта горутина может заблокировать поток!
    for i := 0; ; i++ {
        // Вытеснение ВОЗМОЖНО, но не гарантировано здесь
        if i % 1000000 == 0 {
            // Нет точек вытеснения - планировщик может не сработать
        }
    }
}

func better() {
    for i := 0; ; i++ {
        if i % 10000 == 0 {
            runtime.Gosched() // Явная уступка - хорошая практика
        }
    }
}

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

Когда вытеснение СРАБАТЫВАЕТ:

  1. Горутина выполнялась слишком долго (примерно 10+ миллисекунд)
  2. Функция содержит вызовы функций (точки вытеснения отмечаются компилятором)
  3. Горутина находится в бесконечном цикле без вызовов функций

Когда вытеснение НЕ СРАБАТЫВАЕТ:

  1. Тугие циклы без вызовов функций (tight loops)
  2. Длительные вычисления в одной функции без точек вытеснения
  3. Горутины, удерживающие мьютексы длительное время

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

Проблемный пример:

func cpuIntensive() {
    // Этот цикл может заблокировать планировщик!
    var sum int
    for i := 0; i < 1e10; i++ {
        sum += i * i
        // Нет вызовов функций - вытеснение маловероятно
    }
}

func solution() {
    // Решение 1: Добавить периодическую уступку
    for i := 0; i < 1e10; i++ {
        if i % 1e6 == 0 {
            runtime.Gosched()
        }
    }
    
    // Решение 2: Разбить на несколько горутин
    var wg sync.WaitGroup
    for j := 0; j < 4; j++ {
        wg.Add(1)
        go func(start int) {
            defer wg.Done()
            for i := start; i < 1e10; i += 4 {
                // Вычисления
            }
        }(j)
    }
    wg.Wait()
}

Рекомендации для разработчиков

Лучшие практики:

  1. Избегайте длительных синхронных вычислений в одной горутине
  2. Используйте runtime.Gosched() в интенсивных циклах
  3. Разбивайте тяжелые задачи на несколько горутин
  4. Используйте буферизованные каналы для балансировки нагрузки
  5. Мониторьте планировщик через pprof и trace

Отладка проблем:

# Запуск с трассировкой планировщика
go run -trace=trace.out main.go

# Анализ блокировок
go tool pprof http://localhost:6060/debug/pprof/goroutine

# Использование GODEBUG
GODEBUG=schedtrace=1000,scheddetail=1 go run main.go

Заключение

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

Ключевой вывод: Не полагайтесь исключительно на вытеснение планировщика — проектируйте горутины так, чтобы они регулярно уступали управление через каналы, мьютексы или явные вызовы runtime.Gosched(). Это обеспечит справедливое распределение процессорного времени и предотвратит "голодание" других горутин.

Всегда ли планировщик вытесняет горутину? | PrepBro