Ответ
Когда горутина выполняет блокирующий системный вызов (например, синхронное чтение файла с диска), планировщик Go (Go scheduler) выполняет специальную процедуру, чтобы не блокировать выполнение других горутин.
Для понимания процесса нужно знать ключевые компоненты планировщика:
- G (Goroutine): Легковесный поток, управляемый Go.
- M (Machine): Системный поток ОС (OS thread), который выполняет код.
- P (Processor): Контекст для выполнения. Связывает M и очередь горутин (G).
Процесс обработки блокирующего syscall:
Горутина G1, выполняющаяся на потоке M1 под управлением процессора P1, инициирует блокирующий системный вызов (например,
os.Stdin.Read
).Go runtime перехватывает этот вызов. Он понимает, что поток M1 будет заблокирован ядром ОС на неопределенное время.
Чтобы не допустить простоя, планировщик выполняет следующие действия:
- Отсоединяет процессор P1 от потока M1. Теперь M1 остается один на один с блокирующим вызовом, ожидая ответа от ОС. Горутина G1 остается связанной с M1.
- Планировщик ищет свободный поток M2 или создает новый, если свободных нет.
- Присоединяет процессор P1 (со всей его локальной очередью готовых к выполнению горутин) к новому потоку M2.
- Поток M2 начинает выполнять другие горутины из очереди P1. Таким образом, работа приложения продолжается без остановок.
После завершения syscall:
- Системный вызов в потоке M1 завершается.
- Горутина G1 становится готовой к выполнению (runnable).
- Планировщик помещает G1 в глобальную очередь горутин, откуда ее сможет забрать любой свободный процессор P.
- Поток M1 либо "паркуется" (уходит в режим ожидания), либо уничтожается, если простаивал слишком долго.
// Пример, иллюстрирующий блокировку
go func() {
fmt.Println("Горутина 1: начинаю блокирующий вызов")
// Этот syscall заблокирует текущий поток М,
// но планировщик запустит другие горутины на новом М.
data := make([]byte, 100)
_, _ = os.Stdin.Read(data)
fmt.Println("Горутина 1: разблокирована")
}()
go func() {
fmt.Println("Горутина 2: я выполняюсь, пока первая заблокирована")
}()
time.Sleep(5 * time.Second) // Даем время на выполнение
Важное замечание: Для сетевых операций (network I/O) Go использует более эффективный механизм с netpoller, который интегрирован с epoll/kqueue/iocp. Это позволяет обрабатывать тысячи сетевых соединений без блокировки системных потоков M, что делает Go исключительно производительным для сетевых приложений.