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

Что такое паттерн GMP?

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

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

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

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

Паттерн GMP (Goroutines, Channels, Multiplexing)

GMP — это архитектурный паттерн в Go для управления конкурентностью. Аббревиатура может означать разные интерпретации, но чаще всего здесь имеется в виду архитектура G:P:M (Goroutines : Processors : Machine threads) в runtime Go.

Архитектура G:P:M в Go Runtime

G — Goroutine

  • Легковесный поток, управляемый runtime
  • Может быть тысячи и миллионы
  • Переключение контекста очень быстрое

M — Machine thread (OS Thread)

  • Реальный поток операционной системы
  • Стоит дорого (память, переключение контекста)
  • Создаётся ограниченное количество

P — Processor (логический процессор)

  • Виртуальный процессор в Go
  • Количество обычно равно runtime.GOMAXPROCS()
  • Привязан к одному M в момент времени

Визуализация архитектуры

┌─────────────────────────────────────┐
│  Heap (общая память для всех G)     │
└─────────────────────────────────────┘
         ↓           ↓           ↓
    ┌────────┐  ┌────────┐  ┌────────┐
    │   P1   │  │   P2   │  │   P3   │ (GOMAXPROCS)
    └────────┘  └────────┘  └────────┘
        ↓           ↓           ↓
┌──────────────┬──────────────┬──────────────┐
│   M1 (OS)    │   M2 (OS)    │   M3 (OS)    │
└──────────────┴──────────────┴──────────────┘
     ↓              ↓              ↓
┌────────┐    ┌────────┐    ┌────────┐
│ G1 G2  │    │ G3 G4  │    │ G5 G6  │
│ G7 ... │    │ ...    │    │ ...    │
└────────┘    └────────┘    └────────┘

Практический пример: M:N scheduling

package main

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

func main() {
    // По умолчанию GOMAXPROCS = количество ядер
    fmt.Printf("CPU cores: %d\n", runtime.NumCPU())
    fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(-1))
    
    // Можно явно ограничить
    runtime.GOMAXPROCS(2)
    
    // Создаём много горутин
    for i := 0; i < 10; i++ {
        go func(n int) {
            for j := 0; j < 5; j++ {
                fmt.Printf("G%d iteration %d\n", n, j)
                time.Sleep(10 * time.Millisecond)
            }
        }(i)
    }
    
    time.Sleep(2 * time.Second)
}

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

Шаг 1: Инициализация

  • Runtime создаёт P'ы (процессоры) согласно GOMAXPROCS
  • Для каждого P создаётся локальная очередь (Local Run Queue, LRQ)
  • Глобальная очередь (Global Run Queue, GRQ) для избытка горутин

Шаг 2: Распределение работы

go func() { // Создаёт новую G
    // эта G попадает в LRQ ближайшего P
    // или в GRQ если все LRQ полны
}()

Шаг 3: Планирование

  • Каждый M:P выполняет G из своей LRQ
  • Если LRQ пуста, берёт из GRQ
  • Если и там нет, занимается работой stealing (берёт из LRQ другого P)

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

func main() {
    // Блокирующий системный вызов
    go func() {
        file, _ := os.Open("file.txt") // Блокирует M
        // Если P был привязан к этому M,
        // runtime отвязывает P и привязывает к свободному M
        fmt.Println(file.Name())
    }()
    
    time.Sleep(time.Second)
}

Это ключевое отличие от других систем: когда goroutine выполняет системный вызов, P переключается на другой M, чтобы выполнять другие горутины.

Типичный сценарий с M:N

Стартовое состояние:
G: 100 горутин
P: 4 процессора (GOMAXPROCS=4)
M: 1-2 OS потока

Сценарий:
1. Горутины распределены в LRQ каждого P
2. Каждый M:P выполняет G из своей очереди
3. Когда G заканчивается, M:P берёт следующую G
4. Если G выполняет блокирующий вызов:
   - M блокируется в системном вызове
   - Runtime создаёт новый M для P
   - Новый M:P продолжает выполнять другие G
5. Когда блокирующий вызов завершается:
   - M пробуждается, но может быть распределён на свободную P

Как настроить GOMAXPROCS

import (
    "runtime"
    "runtime/debug"
)

func main() {
    // Получить текущее значение
    current := runtime.GOMAXPROCS(-1)
    fmt.Println("Current GOMAXPROCS:", current)
    
    // Установить новое значение
    old := runtime.GOMAXPROCS(2)
    fmt.Printf("Changed from %d to 2\n", old)
    
    // Совет: для CPU-bound задач GOMAXPROCS должен быть ≈ CPU cores
    optimalProcs := runtime.NumCPU()
    runtime.GOMAXPROCS(optimalProcs)
}

Практические выводы

  • GMP архитектура позволяет эффективно управлять конкурентностью
  • Горутины очень дешёвые — можно создавать десятки тысяч
  • OS потоки дорогие — runtime создаёт ровно столько, сколько нужно
  • Work stealing гарантирует справедливое распределение нагрузки
  • GOMAXPROCS должен быть равен количеству ядер для CPU-bound работы
  • Блокирующие вызовы не замораживают другие горутины — runtime переключает P

Эта архитектура — одна из главных причин популярности Go для высоконагруженных систем.

Что такое паттерн GMP? | PrepBro