Как планировщик Go обрабатывает системные вызовы (syscall) в горутинах?

Ответ

Когда горутина выполняет блокирующий системный вызов (syscall), например, чтение файла, планировщик Go действует эффективно, чтобы не блокировать выполнение других горутин.

Ключевой механизм (модель M:P:G):

  • M: Поток операционной системы (OS Thread).
  • P: Процессор, контекст для выполнения Go кода. У каждого P есть локальная очередь горутин.
  • G: Горутина (Goroutine).

Что происходит по шагам:

  1. Горутина (G), выполняющаяся на потоке (M) под управлением процессора (P), инициирует блокирующий syscall.
  2. Планировщик отсоединяет поток M вместе с блокированной горутиной G от процессора P.
  3. Планировщик ищет свободный поток M или создает новый, чтобы "прикрепить" его к процессору P.
  4. Процессор P продолжает выполнять другие горутины из своей локальной очереди на новом или свободном потоке M.
  5. Когда системный вызов завершается, исходная горутина G становится готовой к выполнению (runnable) и помещается в общую очередь горутин, чтобы планировщик снова мог назначить её для выполнения.

Таким образом, блокировка одной горутины на системном вызове не останавливает всю программу, а лишь временно выводит из строя один поток ОС, позволяя остальным горутинам продолжать работу.

// Пример: os.ReadFile выполняет блокирующий системный вызов.
// Планировщик не даст этой горутине заблокировать другие.
func main() {
    go func() {
        // Этот вызов заблокирует текущую горутину, но не поток ОС для других задач.
        _, err := os.ReadFile("file.txt") 
        if err != nil {
            fmt.Println(err)
        }
    }()

    // Другие горутины могут выполняться в это время.
    time.Sleep(time.Second)
}