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

В чем разница между состояниями Runnable и Waiting в горутинах?

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

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

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

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

Состояния Runnable и Waiting в горутинах: ключевые различия

В Go горутины проходят через несколько состояний в своём жизненном цикле. Runnable и Waiting — два фундаментальных состояния, которые напрямую влияют на планирование и выполнение параллельного кода. Понимание различий между ними критически важно для написания эффективных конкурентных программ и диагностики проблем с производительностью.

Состояние Runnable (Готовность к выполнению)

Состояние Runnable означает, что горутина готова к выполнению и ожидает, когда планировщик Go выделит ей поток операционной системы (M) и процессорное время.

Характеристики состояния Runnable:

  • Горутина готова к немедленному выполнению, но в данный момент не выполняется
  • Находится в очереди исполнения (runqueue) — локальной очереди своего процессора (P) или глобальной очереди
  • Не заблокирована на операциях ввода-вывода, синхронизации или системных вызовах
  • Планировщик может выбрать её для выполнения в любой момент
package main

import (
    "runtime"
    "time"
)

func worker() {
    // Эта горутина большую часть времени находится в состоянии Runnable,
    // конкурируя за процессорное время с другими горутинами
    for i := 0; i < 1000; i++ {
        // Короткие вычисления без блокировок
    }
}

func main() {
    // Создаём несколько горутин, которые будут конкурировать за процессор
    for i := 0; i < 10; i++ {
        go worker()
    }
    
    time.Sleep(time.Second)
}

Состояние Waiting (Ожидание)

Состояние Waiting означает, что горутина приостановлена и ожидает наступления какого-либо события. Она не может быть выполнена до тех пор, пока не произойдёт это событие.

Типичные причины перехода в Waiting:

  • Блокировка на канале — операция отправки или получения, которая не может быть немедленно выполнена
  • Синхронизация — ожидание мьютекса, группы ожидания (WaitGroup), или условной переменной
  • Системные вызовы — операции ввода-вывода (сеть, файлы)
  • Таймеры — вызов time.Sleep() или ожидание таймера/тикера
package main

import (
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    ch := make(chan int)
    
    // Эта горутина переходит в состояние Waiting:
    // 1. Сначала на wg.Wait() (ожидание группы)
    // 2. Затем на <-ch (ожидание данных из канала)
    go func() {
        wg.Wait()          // Переход в Waiting здесь
        value := <-ch      // И здесь тоже
        println(value)
    }()
    
    wg.Add(1)
    time.Sleep(100 * time.Millisecond)
    wg.Done()              // Пробуждение горутины
    
    time.Sleep(100 * time.Millisecond)
    ch <- 42               // Пробуждение горутины снова
}

Ключевые различия

АспектRunnableWaiting
ГотовностьГотова к немедленному выполнениюНе может выполняться до определённого события
ПричинаГорутина выполнима, но не назначена на потокБлокировка на синхронизации или I/O
ПланированиеНаходится в очереди планировщикаУбрана из очереди планировщика
Потребление ресурсовКонкурирует за CPUНе потребляет CPU
ДиагностикаМожет указывать на конкуренцию за CPUМожет указывать на блокировки

Практическое значение для разработчика

Оптимизация состояния Runnable:

  • Слишком много горутин в состоянии Runnable может указывать на конкуренцию за процессор (CPU-bound)
  • Может потребоваться увеличение GOMAXPROCS или оптимизация алгоритмов
  • Использование runtime.Gosched() для явной передачи управления

Анализ состояния Waiting:

  • Длительные периоды в Waiting могут указывать на блокировки (I/O-bound или synchronization)
  • Необходимость оптимизации: пулы соединений, батчинг операций, настройка таймаутов
  • Использование профилировщика (pprof) для идентификации узких мест
// Пример, демонстрирующий оба состояния
func processData(dataCh <-chan Data, resultCh chan<- Result) {
    for data := range dataCh {  // Может перейти в Waiting, если канал пуст
        // Вычисления - состояние Runnable
        result := expensiveCalculation(data)
        
        // Отправка результата - может перейти в Waiting,
        // если получатель не готов
        resultCh <- result  // Возможный переход в Waiting
    }
}

Диагностика через runtime и pprof

package main

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

func printGoroutineStates() {
    for {
        // Получение статистики горутин
        var stats runtime.MemStats
        runtime.ReadMemStats(&stats)
        
        fmt.Printf("Горутин: %d\n", runtime.NumGoroutine())
        
        // На практике состояния можно анализировать через pprof:
        // go tool pprof http://localhost:6060/debug/pprof/goroutine
        
        time.Sleep(2 * time.Second)
    }
}

Заключение

Основное различие между Runnable и Waiting заключается в готовности горутины к выполнению. Runnable — активное состояние конкуренции за ресурсы процессора, в то время как Waiting — пассивное ожидание внешних событий.

Для эффективной работы приложений на Go необходимо:

  1. Минимизировать время в Waiting через асинхронные операции и оптимизацию синхронизации
  2. Балансировать нагрузку в Runnable, избегая как избыточной конкуренции за CPU, так и простаивающих горутин
  3. Использовать инструменты профилирования для анализа распределения состояний горутин

Понимание этих состояний позволяет не только писать более эффективный код, но и правильно интерпретировать данные профилировщика при оптимизации производительности конкурентных приложений на Go.

В чем разница между состояниями Runnable и Waiting в горутинах? | PrepBro