При каких условиях одна горутина уступает место другой
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Условия уступки выполнения (yielding) в Go
В Go планировщик (scheduler) управляет горутинами в кооперативной (невытесняющей) многозадачности, но с элементами вытеснения. Горутина уступает место другой, когда явно или неявно вызывает функции, которые передают контроль планировщику.
Явные точки уступки
-
Вызов
runtime.Gosched()Прямое указание планировщику переключиться на другую горутину:go func() { for i := 0; i < ซ; i++ { if i == 3 { runtime.Gosched() // Явная уступка } } }() -
Блокирующие операции ввода: вывода
- Каналы (channels): операции отправки/получения:
ch <- data // Может блокировать и уступить value := <-ch // Может блокировать и уступить - Сетевые операции:
net.Conn.Read/Write - Файловые операции (если не в неблокирующем режиме)
- Системные вызовы: большинство операций с ОС
- Каналы (channels): операции отправки/получения:
-
Синхронизация
- Мьютексы (
sync.Mutex):Lock()/Unlock() - Группы ожидания (
sync.WaitGroup):Wait() - Условные переменные (
sync.Cond):Wait() - Пулы (
sync.Pool): при конкуренции
- Мьютексы (
Неявные точки уступки
-
Вход в функцию времени выполнения (runtime functions)
runtime.mallocgc(выделение памяти)runtime.gcAssistAlloc(помощь сборщику мусора)
-
Длительные вычисления без точек уступки Хотя 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() } } } -
Работа со сборщиком мусора (GC) Когда горутина участвует в циклах сборки мусора.
Особые случаи и исключения
-
Пустые циклы (empty loops) могут не уступать:
for {} // Может заблокировать планировщик в старых версиях Go -
Неблокирующие каналы с
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+) реализована полная вытесняющая многозадачность на основе асинхронных прерываний, что уменьшает проблему "голодания" горутин. Однако понимание точек уступки остается критически важным для написания эффективного конкурентного кода.