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

Может ли случиться так, что горутина не захочет вытесняться планировщиком?

2.0 Middle🔥 141 комментариев
#Основы Go

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

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

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

Проблема нежелания горутины "вытесняться" в Go

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

Исторический контекст проблемы

В версиях Go до 1.14 планировщик использовал кооперативную многозадачность (cooperative scheduling), где горутина должна была "добровольно" уступить контроль. Это происходило в точках вызова определенных операций:

// До Go 1.14 горутина могла надолго занять поток в таких случаях:

func problematic() {
    // 1. Бесконечный цикл без вызовов функций
    for {
        // Планировщик не мог прервать эту горутину
        // если здесь нет вызовов функций/операций ввода-вывода
    }
    
    // 2. Длительные вычисления без точек yield
    for i := 0; i < 1000000000; i++ {
        // Интенсивные вычисления без вызовов функций
    }
}

Современный механизм вытеснения (Go 1.14+)

Начиная с Go 1.14, реализована полноценная вытесняющая многозадачность на основе сигналов ОС:

package main

import (
    "runtime"
    "time"
)

func main() {
    // Создаем горутину, которая может "не хотеть" уступать
    go func() {
        for i := 0; ; i++ {
            // Даже в таком плотном цикле планировщик
            // сможет прервать выполнение через ~10 мс
            if i % 1000000 == 0 {
                // Но лучше явно дать шанс планировщику
                runtime.Gosched()
            }
        }
    }()
    
    time.Sleep(time.Second)
}

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

Несмотря на улучшения, есть ситуации, когда горутина может чрезмерно долго удерживать ресурсы:

1. Системные вызовы (system calls):

func blockingSyscall() {
    // Длительный системный вызов блокирует поток
    data := make([]byte, 1024*1024*100) // 100MB
    syscall.Read(fd, data) // Может блокировать надолго
}

2. Работа с CGO:

// #include <unistd.h>
import "C"

func cgoBlocking() {
    // Вызов C-функции, которая долго выполняется
    C.sleep(60) // Блокирует поток на 60 секунд
}

3. Неосвобождаемые мьютексы:

func mutexDeadlock() {
    var mu sync.Mutex
    mu.Lock()
    
    // Долгая работа под мьютексом
    performLongComputation()
    
    // Если произойдет паника до Unlock(),
    // другие горутины будут заблокированы
    mu.Unlock()
}

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

Что делать для предотвращения проблем:

  1. Используйте явное уступание:
func worker() {
    for {
        // Полезная работа
        processTask()
        
        // Явно уступаем планировщику
        runtime.Gosched()
    }
}
  1. Разбивайте длительные операции:
func processLargeData(data []byte) {
    chunkSize := len(data) / 100
    for i := 0; i < len(data); i += chunkSize {
        end := i + chunkSize
        if end > len(data) {
            end = len(data)
        }
        
        processChunk(data[i:end])
        runtime.Gosched() // Даем шанс другим горутинам
    }
}
  1. Контролируйте параллелизм с помощью пулов:
func controlledWorkers() {
    sem := make(chan struct{}, runtime.NumCPU())
    
    for task := range tasks {
        sem <- struct{}{}
        go func(t Task) {
            defer func() { <-sem }()
            processTask(t)
        }(task)
    }
}

Мониторинг и диагностика

Для выявления проблем с планировщиком используйте:

// 1. Статистика планировщика
func monitorScheduler() {
    go func() {
        for range time.Tick(time.Second) {
            var stats runtime.MemStats
            runtime.ReadMemStats(&stats)
            // Анализируем stats.NumGoroutine, runtime.NumGoroutine()
        }
    }()
}

// 2. Профилирование горутин
// go tool pprof http://localhost:6060/debug/pprof/goroutine

Вывод

Хотя в современных версиях Go планировщик стал гораздо более эффективным благодаря вытесняющей многозадачности, все еще существуют крайние случаи, когда горутина может чрезмерно долго удерживать ресурсы. Ключевые моменты:

  • Go 1.14+ значительно улучшил ситуацию с автоматическим вытеснением
  • Системные вызовы и CGO остаются потенциальными точками блокировки
  • Явное использование runtime.Gosched() остается хорошей практикой в длительных циклах
  • Проектирование с учетом кооперативности упрощает работу планировщика

Ответственная разработка, понимание модели параллелизма Go и использование доступных инструментов мониторинга помогут избежать проблем с "жадными" горутинами.