Как планировщик Go обрабатывает блокирующие системные вызовы, такие как сетевые операции и чтение файла?

Ответ

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

1. Сетевой ввод-вывод (неблокирующий для потока ОС)

Когда горутина выполняет сетевой вызов (например, чтение из TCP-сокета), Go runtime использует интегрированный сетевой поллер (network poller), который под капотом работает с системными вызовами epoll (Linux), kqueue (macOS) или IOCP (Windows).

Процесс выглядит так:

  1. Горутина инициирует сетевую операцию.
  2. Runtime передает файловый дескриптор сокета сетевому поллеру.
  3. Горутина переводится в состояние ожидания, а поток ОС (M) освобождается и может выполнять другие горутины.
  4. Когда сетевая операция завершена (например, пришли данные), поллер уведомляет рантайм.
  5. Рантайм снова ставит исходную горутину в очередь на выполнение.

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

// Этот вызов заблокирует только горутину, но не поток ОС.
// Планировщик сможет выполнять другие горутины в это время.
resp, err := http.Get("https://example.com")

2. Файловый ввод-вывод (потенциально блокирующий для потока ОС)

Стандартные операции чтения/записи файлов в большинстве ОС являются блокирующими на уровне ядра. Сетевой поллер здесь не поможет.

Когда горутина выполняет такой блокирующий системный вызов:

  1. Рантайм Go обнаруживает, что вызов может заблокировать текущий поток ОС (M) надолго.
  2. Чтобы не блокировать выполнение других горутин, которые могли бы работать на этом потоке, планировщик может создать новый поток ОС и передать ему на выполнение другие готовые горутины из очереди.
  3. Исходный поток ОС остается заблокированным до завершения файловой операции.

Это предотвращает "зависание" всего пула потоков из-за одной медленной дисковой операции.

// Этот вызов может заблокировать и горутину, и поток ОС, на котором она выполняется.
// Рантайм может запустить новый поток, чтобы продолжить выполнение других горутин.
data, err := os.ReadFile("file.txt")

Итог: Ключевое различие в том, что Go оптимизировал работу с сетью через асинхронный API (сетевой поллер), что позволяет избежать блокировки потоков ОС. Для других блокирующих вызовов, таких как файловый ввод-вывод, Go использует стратегию компенсации, создавая дополнительные потоки, чтобы сохранить общую производительность.