Назад к вопросам
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.
Принцип работы:
- Когда нужно запустить goroutine, она помещается в локальную очередь P.
- M, привязанный к P, берёт goroutine из очереди и начинает её выполнение.
- Если локальная очередь P пуста, M может "украсть" goroutine из очереди другого P.
- Если goroutine блокируется (например, при системном вызове), текущий M отвязывается от P, а P привязывается к другому доступному M или создается новый M. Блокированная goroutine остаётся на заблокированном M до завершения блокирующей операции.
- Когда блокирующая операция завершается, goroutine возвращается в очередь другого P или текущего P, если он свободен.
- Планировщик периодически (каждые 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("Все работники завершили работу")
}