Ответ
Go достигает высокой производительности в I/O операциях благодаря умной работе планировщика (scheduler) и интеграции с сетевым поллером (network poller). Это позволяет горутинам выглядеть блокирующими в коде, но быть неблокирующими для операционной системы.
Вот как это работает по шагам:
Инициация вызова: Горутина (G) собирается выполнить блокирующую операцию, например,
net.Read()
.Перехват вызова: Вместо прямого системного вызова, который заблокировал бы поток ОС (M), стандартная библиотека Go обращается к runtime.
Делегирование поллеру: Runtime регистрирует файловый дескриптор (например, сокет) в сетевом поллере. Сетевой поллер — это внутренняя сущность Go, которая использует эффективные механизмы опроса событий ОС (такие как
epoll
в Linux,kqueue
в macOS/BSD,IOCP
в Windows).Парковка горутины: После регистрации дескриптора, планировщик паркует текущую горутину (G). Она переходит в состояние ожидания, но не блокирует системный поток (M), на котором выполнялась. Контекст (P) и поток (M) освобождаются.
Выполнение другой работы: Поскольку поток (M) свободен, планировщик немедленно передает ему другую готовую к выполнению горутину из очереди. Таким образом, поток ОС не простаивает.
Пробуждение горутины: Сетевой поллер в отдельной системной нити постоянно опрашивает ОС. Когда данные становятся доступны для чтения (или буфер готов для записи), поллер уведомляет планировщик.
Возвращение в очередь: Планировщик получает уведомление и перемещает запаркованную горутину (G) из состояния ожидания обратно в очередь готовых к выполнению.
Продолжение работы: Как только освободится какой-либо поток (M), планировщик назначит ему эту горутину, и она продолжит свое выполнение с того места, где остановилась (например, прочитает данные из сокета, которые уже гарантированно доступны).
Результат
Этот механизм позволяет Go эффективно управлять сотнями тысяч горутин, выполняющих I/O операции, используя лишь небольшое количество системных потоков. Для программиста код остается простым и синхронным, а вся асинхронная магия происходит "под капотом" в runtime.
Важное замечание: Этот механизм идеально работает для сетевых I/O. Для блокирующих файловых I/O (до Go 1.18) и вызовов через cgo
, которые runtime не может перехватить, планировщик может прибегнуть к другой стратегии — временно создать новый системный поток, чтобы не блокировать текущий.