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

Что такое Starvation в Go?

3.0 Senior🔥 52 комментариев
#Конкурентность и горутины

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

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

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

Starvation (Голодание) в Go

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

Основные причины голодания в Go

1. Несправедливое планирование

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

func main() {
    done := make(chan bool)
    go func() {
        for {
            select {
            case <-done:
                return
            default:
                // Интенсивные вычисления без вызовов, уступающих управление
            }
        }
    }()
    
    // Другие горутины могут "голодать"
    go func() {
        time.Sleep(time.Second)
        fmt.Println("Эта горутина может никогда не выполниться")
    }()
    
    time.Sleep(time.Millisecond * 100)
    done <- true
}

2. Проблемы с мьютексами

Наиболее частая причина — неправильное использование мьютексов:

var mu sync.Mutex
var sharedResource int

func greedyWorker() {
    for {
        mu.Lock()
        // Длительная обработка с захваченным мьютексом
        sharedResource++
        time.Sleep(time.Millisecond * 500) // Длительная операция
        mu.Unlock()
    }
}

func otherWorker() {
    for {
        mu.Lock() // Эта горутина будет "голодать"
        sharedResource--
        mu.Unlock()
        time.Sleep(time.Millisecond * 10)
    }
}

3. Каналы с недостаточной пропускной способностью

func consumer(ch <-chan int) {
    for {
        select {
        case v := <-ch:
            // Медленная обработка
            process(v)
        }
    }
}

func producer(ch chan<- int) {
    for i := 0; ; i++ {
        ch <- i // Может блокироваться, если потребитель медленный
    }
}

Типичные сценарии голодания

  • CPU-bound горутины, которые не вызывают планировщик
  • Горутины с высоким приоритетом, постоянно захватывающие ресурсы
  • Несправедливые алгоритмы планирования в пользовательских пулах
  • Взаимные блокировки (deadlocks) частного случая голодания

Методы обнаружения и диагностики

Использование трассировки исполнения

import (
    "os"
    "runtime/trace"
)

func main() {
    f, _ := os.Create("trace.out")
    trace.Start(f)
    defer trace.Stop()
    
    // Код с потенциальным голоданием
}

Профилирование планировщика

go tool trace trace.out

Способы предотвращения голодания

1. Использование справедливых примитивов синхронизации

// Используем RWMutex вместо обычного Mutex для чтения/записи
var rwMu sync.RWMutex

func reader() {
    rwMu.RLock() // Множество читателей могут работать одновременно
    defer rwMu.RUnlock()
    // Операции чтения
}

func writer() {
    rwMu.Lock()
    defer rwMu.Unlock()
    // Операции записи
}

2. Внедрение механизмов уступки управления

func worker() {
    for {
        // Интенсивные вычисления
        doWork()
        
        // Явное уступление управления
        runtime.Gosched()
    }
}

3. Использование select с таймаутами

func worker(ch <-chan int) {
    for {
        select {
        case v := <-ch:
            process(v)
        case <-time.After(time.Millisecond * 100):
            // Защита от вечного ожидания
            log.Println("possible starvation detected")
        }
    }
}

4. Балансировка нагрузки

// Использование worker pool с балансировкой
type WorkerPool struct {
    tasks chan Task
    sem   chan struct{} // Семафор для ограничения параллелизма
}

func (wp *WorkerPool) Submit(task Task) {
    select {
    case wp.tasks <- task:
    case <-time.After(time.Second):
        // Обработка таймаута
    }
}

Особенности планировщика Go, влияющие на голодание

  • Вытесняющая многозадачность (начиная с Go 1.14)
  • Кооперативная модель через вызовы функций
  • Work-stealing алгоритм для балансировки нагрузки
  • P, M, G модель (Processor, Machine, Goroutine)

Мониторинг и метрики

import "runtime"

func monitorStarvation() {
    go func() {
        for {
            var m runtime.MemStats
            runtime.ReadMemStats(&m)
            
            // Мониторинг количества горутин
            numGoroutines := runtime.NumGoroutine()
            
            // Мониторинг блокировок
            // (требует использования специальных инструментов)
            
            time.Sleep(time.Second * 5)
        }
    }()
}

Голодание — серьезная проблема в concurrent-программах на Go, которая может приводить к снижению производительности, увеличению latency и даже полной остановке прогресса отдельных компонентов системы. Ключ к предотвращению — понимание механизмов планировщика, использование appropriate synchronization primitives, внедрение мониторинга и проектирование систем с учетом fairness требований. Современные версии Go (1.14+) значительно улучшили ситуацию с вытесняющей многозадачностью, но разработчикам все равно необходимо consciously design системы для минимизации рисков голодания.