← Назад к вопросам

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

2.2 Middle🔥 192 комментариев
#Конкурентность и горутины#Сетевые протоколы и API

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Обработка сетевых операций в Go — это ключевой аспект, демонстрирующий эффективность планировщика (scheduler) и его интеграцию с сетевой поллинг-системой (netpoller). Вот пошаговый механизм этого процесса:

1. Блокировка на сетевом системном вызове

Когда горутина выполняет блокирующий сетевой запрос (например, через net.Dial, http.Get или чтение из net.Conn), она делает системный вызов (например, recvfrom). В традиционной модели это привело бы к блокировке потока ОС, но в Go этого не происходит благодаря асинхронному вводу-выводу.

resp, err := http.Get("https://api.example.com/data")
// На этом этапе горутина будет "приостановлена"

2. Делегирование netpoller'у

Вместо непосредственной блокировки потока ОС, рантайм Go регистрирует файловый дескриптор (сокет) в netpoller — внутреннем компоненте, который использует наиболее эффективный механизм поллинга, доступный в ОС (epoll в Linux, kqueue в BSD, IOCP в Windows).

// Внутренняя реализация net.Conn.Read (упрощенно)
func (c *conn) Read(b []byte) (int, error) {
    // 1. Регистрация дескриптора в netpoller
    // 2. Приостановка текущей горутины
    // 3. Возврат управления планировщику
}

3. Приостановка горутины и перепланирование

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

  • Переводит горутину в состояние waiting (ожидание).
  • Отсоединяет горутину от текущего потока ОС (M).
  • Освобожденный поток ОС может выполнять другие готовые горутины из локальной или глобальной очереди.
Горутина G1 (сетевой запрос) → состояние Waiting
Поток M1 освобождается → берет горутину G2 из очереди

4. Асинхронное ожидание и callback

Netpoller мониторит зарегистрированные дескрипторы в фоновом режиме. При появлении данных (или возможности записи) он:

  1. Получает уведомление от ядра ОС через механизм поллинга.
  2. Помечает соответствующую горутину как runnable (готовую к выполнению).
  3. Добавляет горутину в очередь выполнения.

5. Возобновление выполнения

Когда горутина помечается как runnable:

  • Она попадает в локальную очередь потока ОС или глобальную очередь планировщика.
  • При наличии свободных потоков ОС (или при следующем планировании) горутина возобновляет выполнение с точки блокировки.
  • Результаты сетевой операции (данные или ошибка) уже доступны.

Ключевые преимущества этой модели

  1. Высокая масштабируемость: Один поток ОС может обслуживать десятки тысяч одновременных сетевых соединений.
  2. Минимальные накладные расходы: Нет необходимости создавать по потоку на соединение.
  3. Прозрачность для разработчика: Синхронный API (conn.Read/Write) с асинхронной реализацией под капотом.

Пример практического следствия

func handleConnection(conn net.Conn) {
    buf := make([]byte, 1024)
    // Блокирующее чтение, но без блокировки потока ОС
    n, err := conn.Read(buf)
    // Планировщик переключился на другие горутины
    // во время ожидания данных
    fmt.Printf("Received %d bytes\n", n)
}

В этом примере, если 10 000 горутин одновременно выполнят conn.Read, это не потребует 10 000 потоков ОС — достаточно будет небольшого пула потоков, которые будут эффективно переиспользоваться планировщиком.

Интеграция с остальными компонентами планировщика

Важно отметить, что netpoller работает в отдельном потоке ОС, но тесно интегрирован с планировщиком через runtime-механизмы уведомления. Когда netpoller получает события, он вызывает goready(g) — внутреннюю функцию, которая переносит горутину g в состояние готовности.

Таким образом, обработка сетевых запросов в Go представляет собой идеальную модель M:N (много горутин на немного потоков ОС), где блокирующие операции ввода-вывода не снижают общую производительность приложения, сохраняя при этом простоту синхронного программирования.