Планировщик в Go
В этой теме мы рассмотрим, как на семантическом уровне работает планировщик Go, и сосредоточимся на поведении высокого уровня. Планировщик Go — сложная система, и мелкие механические детали не важны. Важно иметь представление хорошей модели того, как все работает и ведет себя. Это позволит тебе принимать лучшие инженерные решения.
Программа на Go
Сущности планировщика:
-
G — горутина, количество может достигать нескольких тысяч и более.
-
P — логический поток в Go, обычно равен количеству логических потоков процессора.
-
M — потоки предоставляемые операционной системой.
Когда программа Go запускается, ей назначается логический процессор (P) для каждого виртуального ядра, идентифицированного на хост-компьютере. Если у тебя есть процессор с несколькими аппаратными потоками на физическое ядро ( Hyper-Threading ), каждый аппаратный поток будет представлен программе Go как виртуальное ядро. Чтобы лучше понять это, взгляни на системный отчет MacBook Pro.
Как видишь, в этой модели один процессор с четырьмя физическими ядрами. Что этот отчет не раскрывает, так это количество аппаратных потоков, которые есть в Macbook на физическое ядро. Процессор Intel Core i7 поддерживает технологию Hyper-Threading, что означает, что на каждое физическое ядро приходится два логических потока. Это сообщит программе Go, что восемь виртуальных ядер доступны для параллельного выполнения потоков ОС.
Чтобы проверить это, рассмотрим листинг №1.
Когда мы запускаем эту программу на своем локальном компьютере, результатом вызова функции NumCPU() будет значение 8. Любая программа Go, которую запустим на данной модели Macbook, получит 8 P.
Каждому P назначается поток ОС («M»). Буква «М» означает «машина». Этот поток по-прежнему управляется ОС, и ОС по-прежнему отвечает за размещение потока на ядре для выполнения. Это означает, что когда мы запускаем программу Go на приведенной модели Macbook, у нас имеется восемь потоков, доступных для выполнения программы, каждый из которых индивидуально подключен к P.
Каждой программе Go также дается начальная горутина («G»), которая является началом выполнения программы Go. Горутина — это сопрограмма , но это Go, поэтому мы заменяем букву «C» на «G» и получаем слово Goroutine. Ты можешь думать о Goroutines как о потоках уровня приложения (userspace), и они во многом похожи на потоки ОС. Подобно тому, как потоки ОС включаются и выключаются в зависимости от контекста ядра (kernel space), горутины включаются и выключаются в зависимости от контекста M.
Последняя часть головоломки — это очереди выполнения. В планировщике Go есть две разные очереди выполнения:
-
глобальная очередь выполнения (GRQ);
-
локальная очередь выполнения (LRQ).
Каждому P присваивается LRQ, который управляет горутинами, назначенными для выполнения в контексте P. Эти горутины по очереди включаются и выключаются в зависимости от контекста M, назначенного этому P. GRQ предназначен для горутин, которые не были назначены для этого P. Существует также процесс перемещения горутин из GRQ в LRQ.
На изображении ниже представлены все эти компоненты вместе.