Ответ
Когда горутина выполняет блокирующий системный вызов (например, чтение из файла или сети), планировщик Go выполняет специальную последовательность действий, чтобы не блокировать выполнение других горутин. Это одна из ключевых особенностей Go.
В основе лежит модель G-M-P:
- G — Goroutine (горутина).
- M — Machine (поток ОС, "машина" для выполнения).
- P — Processor (контекст для выполнения Go-кода, планировщик).
Процесс выглядит так:
- Горутина (G), выполняющаяся на потоке (M) через процессор (P), инициирует блокирующий
syscall
. - Планировщик Go обнаруживает это. Он отсоединяет P от текущего M, который теперь заблокирован в ядре ОС в ожидании завершения
syscall
. - Планировщик находит другой свободный M (или создает новый, если свободных нет).
- Он присоединяет P к этому новому, незаблокированному M.
- Этот P продолжает выполнять другие горутины из своей очереди на новом M.
Таким образом, блокируется только один поток ОС (M), а не вся программа. Другие горутины продолжают работать.
Когда системный вызов завершается, исходная горутина (G) снова становится готовой к выполнению (runnable
) и помещается в общую очередь, откуда ее заберет свободный P.
package main
import (
"fmt"
"os"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Reading from stdin... (this is a blocking syscall)")
// В этот момент горутина и ее поток M блокируются.
// Планировщик отсоединит P и запустит его на другом M.
var buf [1]byte
os.Stdin.Read(buf[:])
fmt.Println("Read completed.")
}()
fmt.Println("Main goroutine continues to run.")
wg.Wait() // Ожидаем завершения другой горутины
}