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

Как можно явно сообщить планировщику, что нужно переключиться на другую горутину?

2.0 Middle🔥 101 комментариев
#Конкурентность и горутины#Производительность и оптимизация

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

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

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

Контроль выполнения горутин в Go

В Go не существует прямых способов явного указания планировщику переключиться на конкретную горутину. Это принципиальная особенность языка — планировщик работает кооперативно (cooperative), а не прерываемо (preemptive), и переключения происходят автоматически на определённых точках. Однако есть методы, которые создают условия для переключения.

Как работает планировщик горутин

Планировщик Go (scheduler) переключается между горутинами в точках вызова (scheduling points):

package main

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

func main() {
    // Пример горутины
    go func() {
        fmt.Println("Горутина запущена")
    }()
    
    // Точки вызова возникают здесь:
    time.Sleep(time.Millisecond) // Вызов функции
    runtime.Gosched()            // Явный вызов
    fmt.Println("Операция")      // Вызов функции
}

Методы создания точек переключения

1. runtime.Gosched()

Это единственная стандартная функция для "уступки" текущего потока выполнения.

func worker() {
    for i := 0; i < 3; i++ {
        fmt.Println("Работаю:", i)
        runtime.Gosched() // Уступаем планировщику
    }
}

func main() {
    go worker()
    go worker()
    time.Sleep(time.Second)
}

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

2. Системные вызовы и операции блокировки

Любой вызов, который может привести к блокировке, создаёт точку переключения:

// Примеры операций, создающих точки переключения
func blockingOperations() {
    // Операции с каналами
    ch := make(chan int)
    go func() { ch <- 1 }()
    <-ch // Блокировка при чтении
    
    // Системные вызовы
    time.Sleep(1 * time.Millisecond) // Вызов time.Sleep
    
    // Операции ввода-вывода
    file, _ := os.Open("test.txt")
    data := make([]byte, 100)
    file.Read(data) // Блокировка I/O
    
    // Синхронизация
    var mu sync.Mutex
    mu.Lock()   // Блокировка при захвате мьютекса
    mu.Unlock()
}

3. Вызовы функций

Каждый вызов функции является потенциальной точкой переключения, особенно если функция достаточно долгая.

Почему нет явного контроля?

Принципы Go в многозадачности:

  • Простота: Программист не управляет планировщиком явно
  • Эффективность: Планировщик оптимизирован для автоматического балансирования
  • Декларативный подход: Вы описываете что должно выполняться, а не как
// Пример декларативного параллельного выполнения
func processData(data []int) {
    results := make(chan int)
    
    // Запускаем горутины без контроля над планировщиком
    for _, item := range data {
        go func(x int) {
            results <- x * 2 // Планировщик решит, когда выполнять
        }(item)
    }
    
    // Сбор результатов
    for i := 0; i < len(data); i++ {
        fmt.Println(<-results)
    }
}

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

  1. Используйте runtime.Gosched() для демонстрационных целей или когда одна горутина может monopolize CPU в tight loop:
func tightLoop() {
    for i := 0; i < 1000000; i++ {
        // Полезная работа
        if i % 1000 == 0 {
            runtime.Gosched() // Периодически уступаем
        }
    }
}
  1. Создавайте естественные точки переключения через каналы и блокировки:
func cooperativeWorker(ch chan bool) {
    for {
        select {
        case <-ch:
            // Получили сигнал - точка переключения здесь
            return
        default:
            // Небольшая работа, затем проверка канала
            doWork()
            runtime.Gosched() // Опционально
        }
    }
}
  1. Не пытайтесь управлять планировщиком напрямую — это противоречит философии Go и может привести к нестабильности.

Заключение

В Go вы не можете явно указать планировщику переключиться на конкретную горутину. Вместо этого создаются условия для переключения через:

  • Вызов runtime.Gosched() (уступка выполнения)
  • Операции блокировки (каналы, I/O, мьютексы)
  • Системные вызовы
  • Вызовы функций

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