Как работает планировщик в Golang?
Планировщик в Golang - это механизм, который отвечает за распределение и выполнение горутин (G) на логических процессорах (P), которые в свою очередь работают на физических потоках (M) операционной системы. Планировщик в Golang использует модель M:N, то есть может запускать произвольное количество горутин на произвольном количестве потоков.
Горутина (G) - это легковесный поток выполнения, который может быть создан, переключен и уничтожен планировщиком Go без взаимодействия с операционной системой. Горутины занимают мало памяти и могут быть заспавнены в большом количестве. Горутины не привязаны к конкретному потоку или процессору, а могут динамически менять свой контекст выполнения.
Логический процессор (P) - это абстракция, которая представляет собой ресурс, необходимый для выполнения горутин. Каждый P имеет свою локальную очередь (LRQ), в которой хранятся горутины, ожидающие запуска. Количество P обычно равно количеству ядер в системе, чтобы максимально использовать параллелизм. P не может выполнять горутину самостоятельно, а нуждается в M для этого.
Физический поток (M) - это поток операционной системы, который может быть связан с P и выполнять горутину из LRQ. M может переключаться между разными P в зависимости от ситуации. Например, если M блокируется на системном вызове или операции ввода-вывода, он может быть отсоединен от P и заменен другим M. Также M может участвовать в процессе work stealing, когда он пытается украсть горутину из LRQ или глобальной очереди (GRQ) другого P.
Планировщик Go использует алгоритм work stealing для балансировки нагрузки между P. Если LRQ одного P пуста, он может попытаться украсть горутину из LRQ или GRQ другого P. Это позволяет эффективно использовать ресурсы и избегать простоя.
Источники:
В чем разница между вытесняющим и кооперативным планировщиком?
Вытесняющий планировщик - это такой планировщик, который может прервать выполнение текущей задачи в любой момент и передать управление другой задаче, если он посчитает это необходимым. Вытесняющий планировщик обычно использует таймер или другие прерывания, чтобы определить, когда нужно сменить задачу. Вытесняющий планировщик позволяет более эффективно распределять ресурсы между задачами и обеспечивать более высокую отзывчивость системы.
Кооперативный планировщик - это такой планировщик, который не может прервать выполнение текущей задачи, пока она сама не отдаст управление. Кооперативный планировщик полагается на то, что задачи будут добровольно освобождать процессор, когда они закончат свою работу или будут ждать ввода-вывода. Кооперативный планировщик проще в реализации и потребляет меньше ресурсов, но он может привести к проблемам с производительностью и справедливостью, если одна из задач будет занимать процессор слишком долго.
Если вы хотите узнать больше о том, в чем разница между вытесняющим и кооперативным планировщиком, вы можете посмотреть следующие источники:
- Планирование в Go: Часть II — Планировщик Go
- Разница между вытесняющим и невытесняющим планированием в операционных системах
- Упреждающее и невытесняющее планирование
Какой тип планировщика в Golang?
Планировщик в Golang - это вытесняющий планировщик с кооперативными элементами.
Это означает, что он может прервать выполнение текущей горутины в любой момент и передать управление другой горутине, если он посчитает это необходимым, но он также полагается на то, что горутины будут добровольно освобождать процессор, когда они будут ждать ввода-вывода или блокироваться на каналах.
Планировщик в Golang использует таймер или другие прерывания, чтобы определить, когда нужно сменить горутину, а также учитывает приоритеты и аффинность горутин к процессорам.
Источники:
В планировщике до версии 1.15 какие операции приводят к переключению контекста горутин?
В планировщике Golang до версии 1.15 переключение контекста горутин происходило только при выполнении следующих операций:
- Ожидание ввода-вывода (I/O), такого как чтение или запись в файл, сеть, канал или таймер.
- Блокировка на мьютексе (mutex) или другом примитиве синхронизации.
- Вызов функции runtime.Gosched, которая явно передает управление другой горутине.
- Вызов функции runtime.GC, которая запускает сборку мусора и приостанавливает все горутины.
- Вызов функции runtime.LockOSThread, которая привязывает горутину к одному системному потоку и не позволяет ей переключаться на другой.
В версии 1.15 планировщик Golang стал вытесняющим, то есть он может прервать выполнение любой горутины, которая работает слишком долго, и передать управление другой горутине, если он посчитает это необходимым. Это улучшило отзывчивость и справедливость системы, а также уменьшило вероятность возникновения состояний гонки и взаимных блокировок.
Можно ли руками переключить контекст горутины?
В общем случае, нет, нельзя руками переключить контекст горутины, так как это делает планировщик Golang автоматически, когда он посчитает это необходимым.
Однако, в некоторых редких случаях, можно руками переключить контекст горутины, используя низкоуровневые функции из пакета runtime, такие как runtime.Gopark и runtime.Goready, которые позволяют приостановить и возобновить горутину по собственному условию. Эти функции не рекомендуются для обычного использования, так как они могут привести к ошибкам и несовместимостям с планировщиком Golang.
Источники:
Сколько потоков операционной системы мы можем создать?
Количество потоков операционной системы, которые мы можем создать в Golang, зависит от нескольких факторов, таких как:
- Количество горутин (goroutines), которые мы запускаем в нашей программе.
- Количество процессоров или ядер на компьютере. По умолчанию Golang создает столько системных потоков, сколько доступно логических процессоров на компьютере, но это можно изменить с помощью переменной окружения GOMAXPROCS.
- Размер стека для каждого системного потока. Стек - это область памяти, которая используется для хранения локальных переменных, параметров и адресов возврата функций. Размер стека ограничивает количество системных потоков, которые могут быть созданы, так как память не бесконечна.
Расскажи про глобальную и локальную очереди
Глобальная и локальная очереди в Golang - это структуры данных, которые используются планировщиком Go для распределения горутин между логическими процессорами (P). Горутина - это легковесный поток выполнения, который может быть переключен планировщиком без блокировки операционной системы.
Глобальная очередь выполнения (GRQ) содержит горутины, которые еще не были назначены ни одному логическому процессору. Когда логический процессор создается или освобождается, он может взять горутину из GRQ и поместить ее в свою локальную очередь выполнения (LRQ). LRQ содержит горутины, которые принадлежат конкретному логическому процессору и ожидают своего запуска.
Планировщик Go использует алгоритм work stealing для балансировки нагрузки между логическими процессорами. Если LRQ одного логического процессора пуста, он может попытаться украсть горутину из LRQ другого логического процессора или из GRQ. Это позволяет эффективно использовать ресурсы и избегать простоя.
Источники:
Может ли горутина начать работу на одном P, приостановиться и продолжить работу на другом P?
Да, горутина может переключаться между разными логическими процессорами (P) во время своей работы. Это может произойти по нескольким причинам, например:
- Горутина блокируется на операции ввода-вывода или системном вызове, и планировщик Go переназначает ее P другой горутине.
- Горутина добровольно отступает от своего P, вызывая функцию yield или resume, которые позволяют использовать корутины в Go.
- Горутина истекает свой квант времени, и планировщик Go прерывает ее выполнение и переводит ее в конец своей локальной очереди (LRQ).
- Горутина становится жертвой work stealing, когда другой P, у которого пустая LRQ, пытается украсть горутину из LRQ или глобальной очереди (GRQ) другого P.
Таким образом, горутины в Go не привязаны к конкретному P на всю свою жизнь, а могут динамически менять свой контекст выполнения в зависимости от ситуации. Это повышает эффективность и отзывчивость приложений, написанных на Go.
Источники:
Может ли одна очередь украсть горутины у другой?
Да, одна очередь может украсть горутины у другой в процессе work stealing. Это алгоритм, который позволяет планировщику Go балансировать нагрузку между логическими процессорами (P). Когда один P имеет пустую локальную очередь (LRQ), он может попытаться украсть горутину из LRQ или глобальной очереди (GRQ) другого P. Это увеличивает вероятность того, что каждый P будет иметь работу, и избегает простоя.
В чем профит горутин, если мы можем заспавнить миллион потоков в процессе, и почему упадет процесс с таким количеством потоков, или нет?
Профит горутин в том, что они легковеснее и эффективнее обычных потоков. Горутины занимают меньше памяти (порядка килобайтов), чем потоки (порядка мегабайтов), и могут динамически расти и уменьшаться в зависимости от потребностей. Горутины также имеют меньший оверхед на создание, переключение и уничтожение, чем потоки, которые требуют взаимодействия с операционной системой.
Если мы попытаемся заспавнить миллион потоков в процессе, то скорее всего процесс упадет из-за нехватки памяти или ресурсов. Каждый поток требует выделения стека, регистров, дескрипторов и других ресурсов, которые ограничены операционной системой. Кроме того, большое количество потоков приводит к частому переключению контекста, которое снижает производительность и увеличивает задержки.
В отличие от потоков, горутины могут быть заспавнены в большом количестве без существенных потерь производительности и памяти. Планировщик Go оптимально распределяет горутины между логическими процессорами (P), используя глобальную и локальные очереди, а также алгоритм work stealing. Таким образом, горутины позволяют писать высокопроизводительные и масштабируемые приложения на Go.