Назад к вопросам
Middle
134
questionbank

Расскажи о планировании goroutine в Go.

Sobes Copilot

Получайте ответы в реальном времени

Ответ от нейросети

sobes.tech AI

Планировщик Go использует многопоточную модель M:N, где M goroutine отображаются на N потоков ОС. Работа планировщика основана на модели GMP (Goroutine, Machine, P).

  • G (Goroutine): Легковесный поток выполнения. Создаётся по ключевому слову go.
  • M (Machine): Поток ОС. Выполняет код goroutine. Горячий поток, блокирующие системные вызовы выполняются на отдельном M.
  • P (Processor): Локальная очередь goroutine, к которой привязан M. Количество P по умолчанию равно числу логических ядер CPU и контролируется переменной GOMAXPROCS.

Принцип работы:

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

Преимущества такого подхода:

  • Низкие накладные расходы на создание и переключение goroutine (в разы меньше, чем у потоков ОС).
  • Эффективное использование ресурсов CPU за счёт балансировки нагрузки между P.
  • Асимптотически лучшая производительность по сравнению с моделью 1:1 (каждая goroutine - поток ОС).

Модели планирования:

  • Runqueue: Каждому P соответствует локальная очередь готовых к выполнению goroutine.
  • Global Runqueue: Глобальная очередь для "украденных" или готовых к выполнению goroutine, когда локальная очередь пуста. Используется реже.

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

  • При блокирующих операциях (I/O, мьютексы, каналы).
  • При вызове функций, которые могут привести к плану переключения (например, вызовы runtime).
  • При "voluntary yield" с помощью runtime.Gosched().
  • При time-based preemption (запланировано с Go 1.14).
// Пример использования goroutine
package main

import (
	"fmt"
	"time"
)

func worker(id int) {
	fmt.Printf("Работник %d начал работу\n", id)
	time.Sleep(time.Second) // Имитация работы
	fmt.Printf("Работник %d закончил работу\n", id)
}

func main() {
	for i := 1; i <= 5; i++ {
		go worker(i) // Создание и запуск goroutine
	}

	time.Sleep(2 * time.Second) // Ждем завершения всех goroutine
	fmt.Println("Все работники завершили работу")
}