Ответ
Планировщик Go (Go Scheduler) переключает контекст между горутинами в нескольких ключевых случаях. Эти события можно разделить на две категории: кооперативное и принудительное (preemptive) переключение.
1. Кооперативное переключение (Voluntary Yielding)
Горутина добровольно отдает управление планировщику, когда выполняет операцию, которая может занять время.
- Блокирующие системные вызовы: операции ввода-вывода (I/O), такие как чтение/запись в файлы или сетевые сокеты.
- Операции с каналами: отправка или чтение из канала, если он заблокирован (например, отправка в полный канал или чтение из пустого).
- Операции с примитивами синхронизации: блокировка мьютекса (
sync.Mutex
), ожидание наsync.WaitGroup
. - Вызов
time.Sleep
: горутина "засыпает" на указанное время. - Явный вызов
runtime.Gosched()
: прямое указание планировщику немедленно переключиться на другую горутину.
2. Принудительное переключение (Preemptive Scheduling)
Начиная с Go 1.14, планировщик может принудительно прерывать горутины, которые выполняются слишком долго без кооперативного переключения.
- Истечение кванта времени: если горутина выполняется непрерывно (например, в долгом цикле без блокирующих вызовов) дольше определённого кванта времени (~10 мс), планировщик может её прервать и передать управление другой горутине. Это предотвращает "зависание" одного из системных потоков из-за одной долгой горутины.
- Вызовы функций: компилятор вставляет специальные проверки в прологи функций. Если планировщик пометил горутину для прерывания, эта проверка вызовет переключение контекста.