Ответ
Планировщик Go стремится максимально эффективно использовать потоки ОС (M), не допуская их простоя. Подход к обработке блокирующих вызовов различается для сетевых и файловых операций.
1. Сетевой ввод-вывод (неблокирующий для потока ОС)
Когда горутина выполняет сетевой вызов (например, чтение из TCP-сокета), Go runtime использует интегрированный сетевой поллер (network poller), который под капотом работает с системными вызовами epoll
(Linux), kqueue
(macOS) или IOCP
(Windows).
Процесс выглядит так:
- Горутина инициирует сетевую операцию.
- Runtime передает файловый дескриптор сокета сетевому поллеру.
- Горутина переводится в состояние ожидания, а поток ОС (M) освобождается и может выполнять другие горутины.
- Когда сетевая операция завершена (например, пришли данные), поллер уведомляет рантайм.
- Рантайм снова ставит исходную горутину в очередь на выполнение.
Таким образом, поток ОС не блокируется и продолжает работать.
// Этот вызов заблокирует только горутину, но не поток ОС.
// Планировщик сможет выполнять другие горутины в это время.
resp, err := http.Get("https://example.com")
2. Файловый ввод-вывод (потенциально блокирующий для потока ОС)
Стандартные операции чтения/записи файлов в большинстве ОС являются блокирующими на уровне ядра. Сетевой поллер здесь не поможет.
Когда горутина выполняет такой блокирующий системный вызов:
- Рантайм Go обнаруживает, что вызов может заблокировать текущий поток ОС (M) надолго.
- Чтобы не блокировать выполнение других горутин, которые могли бы работать на этом потоке, планировщик может создать новый поток ОС и передать ему на выполнение другие готовые горутины из очереди.
- Исходный поток ОС остается заблокированным до завершения файловой операции.
Это предотвращает "зависание" всего пула потоков из-за одной медленной дисковой операции.
// Этот вызов может заблокировать и горутину, и поток ОС, на котором она выполняется.
// Рантайм может запустить новый поток, чтобы продолжить выполнение других горутин.
data, err := os.ReadFile("file.txt")
Итог: Ключевое различие в том, что Go оптимизировал работу с сетью через асинхронный API (сетевой поллер), что позволяет избежать блокировки потоков ОС. Для других блокирующих вызовов, таких как файловый ввод-вывод, Go использует стратегию компенсации, создавая дополнительные потоки, чтобы сохранить общую производительность.