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

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

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

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

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

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

Условия уступки выполнения (yielding) в Go

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

Явные точки уступки

  1. Вызов runtime.Gosched() Прямое указание планировщику переключиться на другую горутину:

    go func() {
        for i := 0; i < ซ; i++ {
            if i == 3 {
                runtime.Gosched() // Явная уступка
            }
        }
    }()
    
  2. Блокирующие операции ввода: вывода

    • Каналы (channels): операции отправки/получения:
      ch <- data // Может блокировать и уступить
      value := <-ch // Может блокировать и уступить
      
    • Сетевые операции: net.Conn.Read/Write
    • Файловые операции (если не в неблокирующем режиме)
    • Системные вызовы: большинство операций с ОС
  3. Синхронизация

    • Мьютексы (sync.Mutex): Lock()/Unlock()
    • Группы ожидания (sync.WaitGroup): Wait()
    • Условные переменные (sync.Cond): Wait()
    • Пулы (sync.Pool): при конкуренции

Неявные точки уступки

  1. Вход в функцию времени выполнения (runtime functions)

    • runtime.mallocgc (выделение памяти)
    • runtime.gcAssistAlloc (помощь сборщику мусора)
  2. Длительные вычисления без точек уступки Хотя Go кооперативен, планировщик вставляет точки вытеснения:

    • При вызове функций (пролог функции)
    • В цикле после определенного количества итераций (примерно каждые 10 мс)

    Пример опасного кода без точек уступки:

    // Плохо: может надолго занять поток
    func busyLoop() {
        for i := 0; i < 1e9; i++ {
            // Нет вызовов функций, каналов, Gosched
        }
    }
    
    // Лучше: периодическая уступка
    func betterLoop() {
        for i := 0; i < 1e9; i++ {
            if i%1000 == 0 {
                runtime.Gosched()
            }
        }
    }
    
  3. Работа со сборщиком мусора (GC) Когда горутина участвует в циклах сборки мусора.

Особые случаи и исключения

  1. Пустые циклы (empty loops) могут не уступать:

    for {} // Может заблокировать планировщик в старых версиях Go
    
  2. Неблокирующие каналы с select и default:

    select {
    case <-ch:
        // Получение
    default:
        // Не блокирует, не уступает
    }
    

Как планировщик принимает решение

Планировщик использует логические процессоры (P), очереди горутин (G), и потоки ОС (M):

// Упрощенная модель планировщика
// 1. Горутина блокируется (канал, мьютекс)
// 2. Контекст P переключается на другую горутину из локальной очереди
// 3. Если локальная очередь пуста - воркстилинг из других P
// 4. Если все P заняты - горутина в глобальной очереди

Практические рекомендации

  • Для CPU.связанных задач периодически вызывайте runtime.Gosched()
  • Используйте каналы для естественного переключения контекста
  • Избегайте длительных вычислений без точек уступки
  • Тестируйте с GOMAXPROCS=1 для выявления проблем
  • Профилируйте с помощью pprof и trace для анализа переключений
// Пример правильного паттерна
func worker(ch chan int) {
    for {
        select {
        case data := <-ch:
            process(data)
        case <-time.After(time.Millisecond):
            // Периодическая точка уступки
        }
    }
}

Эволюция планировщика

В современных версиях Go (1.14+) реализована полная вытесняющая многозадачность на основе асинхронных прерываний, что уменьшает проблему "голодания" горутин. Однако понимание точек уступки остается критически важным для написания эффективного конкурентного кода.