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

Как Go scheduler работает на уровни тредов?

2.8 Senior🔥 92 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Как Go scheduler работает на уровне потоков (threads)

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

Основная архитектура: M-P-G модель

Планировщик Go построен на трех основных абстракциях:

// Концептуальная модель (упрощенно)
G (Goroutine) - легковесный поток выполнения
M (Machine)   - поток операционной системы (OS thread)
P (Processor) - логический процессор (контекст выполнения)

Взаимодействие компонентов:

  1. G (Goroutine) - пользовательский легковесный поток

    • Стек начинается с 2KB (растет/сжимается динамически)
    • Содержит информацию о состоянии выполнения
    • Дешевле потоков ОС в 100+ раз
  2. M (Machine) - поток операционной системы

    • Привязан к ядру CPU
    • Выполняет код Go
    • Управляется планировщиком Go, а не ОС напрямую
  3. P (Processor) - логический процессор

    • Локальная очередь runnable горутин
    • Необходим для выполнения кода M
    • Количество P обычно равно GOMAXPROCS

Принципы работы планировщика на уровне потоков

1. Отображение M на P

Каждому P может быть привязан один M в данный момент времени. Количество P определяется GOMAXPROCS (по умолчанию равно количеству ядер CPU):

// Установка количества логических процессоров
runtime.GOMAXPROCS(4) // 4 P, до 4 M могут работать одновременно

2. Работа с системными вызовами

Когда горутина выполняет блокирующий системный вызов:

func blockingCall() {
    data := make([]byte, 1024)
    // Блокирующий системный вызов
    n, _ := syscall.Read(fd, data) // Горутина блокируется
    
    // Планировщик отсоединяет M от P
    // Создает или использует другой M для работы с этим P
}

Что происходит:

  • Текущий M блокируется в ОС
  • P отсоединяется от заблокированного M
  • P привязывается к свободному M или создает новый
  • Когда системный вызов завершается, горутина возвращается в очередь

3. Сетевые операции без блокировки

Для сетевых операций используется netpoller (интеграция с epoll/kqueue/IOCP):

func networkOperation() {
    conn, _ := net.Dial("tcp", "example.com:80")
    // Неблокирующая операция
    data := make([]byte, 1024)
    n, _ := conn.Read(data) // Горутина пакуется, M не блокируется
    
    // Планировщик переключается на другую горутину
    // M продолжает работу с другим P
}

4. Work-stealing алгоритм

Когда у P заканчиваются локальные горутины:

  • Проверяет глобальную очередь
  • "Крадет" половину горутин из очереди другого случайного P
  • Проверяет netpoller для готовых сетевых операций

Ключевые особенности реализации

Преимущества модели:

  • Не требует переключения контекста ОС для горутин
  • Кооперативная многозадачность с вытеснением
  • Локальные очереди уменьшают contention
  • Автоматическое масштабирование количества потоков М

Ограничения и настройки:

// Экспериментальные настройки (могут меняться)
debug.SetMaxThreads(10000) // Максимум потоков М
runtime.LockOSThread()     // Привязка горутины к конкретному М

Пример жизненного цикла потока М

Инициализация:
1. Программа стартует с GOMAXPROCS потоками М
2. Каждому Р назначается по одному М

При блокировке:
1. M1 блокируется на syscall
2. P отсоединяется от M1
3. P берет M2 из пула или создает новый
4. M1 продолжает блокировку в ОС
5. Когда syscall завершен, M1 возвращает G в очередь

При сетевой операции:
1. G выполняет неблокирующий network call
2. G пакуется в netpoller
3. M переключается на другую G
4. Когда сетевое событие готово, G возвращается в очередь

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

  1. Не нужно управлять потоками вручную - Go автоматически масштабирует количество потоков ОС
  2. GOMAXPROCS задает параллелизм, но не максимальное количество потоков
  3. Блокирующие C-вызовы могут создавать множество потоков М
  4. runtime.LockOSThread() полезен для интеграции с C библиотеками, требующими thread-local storage

Отличие от традиционных thread-pool

// Традиционный подход (Java/C#)
threadPool.execute(task) // Задачи -> потоки из пула

// Go подход
go task() // Горутина -> планировщик -> потоки ОС

В Go планировщик динамически управляет соотношением горутин к потокам ОС, обеспечивая оптимальное использование ресурсов без ручной настройки пулов потоков.

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