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

Всегда ли Go сам переключал горутины?

2.0 Middle🔥 242 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Механизм планирования горутин в Go

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

Ключевые моменты переключения горутин

Планировщик Go не вытесняет горутины (non-preemptive) по умолчанию, но переключение происходит в определенных точках:

  1. На явных операциях синхронизации или блокировки

    • Вызовы ch<-, <-ch (операции с каналами)
    • Использование sync.Mutex, sync.WaitGroup и других примитивов
    • Системные вызовы, такие как сетевые операции или файловый I/O
  2. На вызовах runtime, включая сборку мусора (GC)

    • При запуске GC некоторые горутины могут быть приостановлены
    • Вызовы функций runtime, например runtime.Gosched()
  3. На точках вызова функций

    • Планировщик может переключать горутины при входе/выходе из функций, но это не гарантировано
  4. При использовании runtime.Gosched()

    • Эта функция явно уступает процессорное время другим горутинам
package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("Горутина 1:", i)
            runtime.Gosched() // Явное переключение
        }
    }()

    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("Горутина 2:", i)
        }
    }()

    runtime.Gosched() // Даем возможность другим горутинам запуститься
}

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

Планировщик Go использует кооперативную модель (cooperative scheduling) с элементами вытеснения в новых версиях:

  • В версиях до Go 1.14 планировщик был полностью кооперативным
  • С Go 1.14 появилась вытесняющая схема (preemptive scheduling) на основе сигналов асинхронной обработки
  • Планировщик теперь может прерывать длительные вычисления без точек синхронизации
// До Go 1.14 эта горутина могла monopolize процессор
func intensiveCompute() {
    for {
        // Длительные вычисления без точек синхронизации
        // В Go 1.14+ планировщик может прервать эту горутину
    }
}

Модель M-P-G планировщика

Планировщик использует три уровня абстракции:

  • M (Machine) - поток ОС (thread)
  • P (Processor) - логический процессор, контекст для выполнения горутин
  • G (Goroutine) - сама горутина

Логика переключения:

  1. Каждый P имеет локальную очередь готовых к выполнению G
  2. Когда горутина блокируется (например, на канале), P переключается на другую G из своей очереди
  3. Если локальная очередь пуста, P может взять горутину из глобальной очереди или "украсть" из очереди другого P
  4. При системных вызовах M может отделиться от P для выполнения блокирующей операции

Примеры автоматического переключения

package main

import (
    "fmt"
    "time"
)

func example1() {
    ch := make(chan int)
    
    go func() {
        // Эта горутина будет приостановлена при отправке в канал
        // если нет готового получателя
        ch <- 42
        fmt.Println("Отправлено")
    }()
    
    go func() {
        // Планировщик переключится на эту горутину при блокировке первой
        time.Sleep(10 * time.Millisecond)
        val := <-ch
        fmt.Println("Получено:", val)
    }()
    
    time.Sleep(100 * time.Millisecond)
}

func example2() {
    var mu sync.Mutex
    
    go func() {
        mu.Lock() // Может вызвать переключение, если мьютекс занят
        defer mu.Unlock()
        fmt.Println("Горутина 1 захватила мьютекс")
    }()
    
    go func() {
        mu.Lock() // Эта горутина будет приостановлена
        defer mu.Unlock()
        fmt.Println("Горутина 2 захватила мьютекс")
    }()
}

Важные выводы

  1. Go переключает горутины автоматически, но не произвольно - переключение происходит в определенных точках
  2. Разработчик может влиять на переключение через:
    • Использование каналов и примитивов синхронизации
    • Вызов runtime.Gosched()
    • Создание точек синхронизации в коде
  3. С Go 1.14 переключение стало более вытесняющим - планировщик может прерывать долго выполняющиеся горутины
  4. Нельзя полагаться на порядок переключения - он недетерминирован и зависит от многих факторов

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

Всегда ли Go сам переключал горутины? | PrepBro