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

Что происходит с горутиной, работающей с синхронными системными вызовами?

2.8 Senior🔥 111 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Влияние синхронных системных вызовов на горутины в Go

Когда горутина выполняет синхронный системный вызов (например, операции с файлами, сетевые запросы без использования netpoll, некоторые операции с мьютексом), она блокируется на уровне операционной системы. Это означает, что горутина переходит в состояние ожидания до завершения системного вызова, и планировщик Go (scheduler) не может ее перепланировать в этот период.

Механизм блокировки горутины

Синхронные вызовы блокируют всю поток выполнения на уровне ОС. В отличие от асинхронных вызовов или операций с каналами, которые могут быть перепланированы планировщиком Go, синхронные операции "захватывают" горутину до их завершения. Это происходит потому, что системный вызов выполняется в контексте текущего потока ОС (OS thread), и поток не может быть использован для выполнения других горутин пока вызов не завершится.

package main

import (
    "fmt"
    "time"
    "syscall"
)

func main() {
    go func() {
        // Пример синхронного системного вызова - sleep на уровне ОС
        fmt.Println("Горутина начинает системный вызов")
        time.Sleep(2 * time.Second) // Внутри использует системный вызов sleep
        fmt.Println("Горутина завершила системный вызов")
    }()
    
    time.Sleep(1 * time.Second)
    fmt.Println("Основная горутина продолжает работу")
}

Как планировщик Go обрабатывает эту ситуацию

Планировщик Go использует механизм вытеснения потоков (thread preemption) для минимизации влияния таких блокировок:

  1. Когда горутина блокируется на системном вызове, планировщик отсоединяет ее от текущего потока ОС.
  2. Создается новый поток ОС (или используется существующий из пула), чтобы продолжить выполнение других горутин.
  3. После завершения системного вызова, горутина пытается вернуться в выполнение:
    • Она присоединяется к существующему потоку ОС
    • Если нет свободных потоков, может быть создан новый
    • Горутина помещается в очередь готовых к выполнению

Проблемы и решения

Основная проблема: системные вызовы могут создавать большое количество потоков ОС, если много горутин одновременно выполняют такие операции. Это увеличивает нагрузку на память и планировщик ОС.

// Плохой пример: много синхронных файловых операций
func processFiles(filePaths []string) {
    for _, path := range filePaths {
        go func(p string) {
            data, err := os.ReadFile(p) // Синхронный системный вызов
            // Обработка данных...
        }(path)
    }
}

Решение: использование асинхронных интерфейсов или ограничение количества одновременных системных вызовов:

// Улучшенный пример с ограничением через каналы
func processFilesLimited(filePaths []string, limit int) {
    sem := make(chan bool, limit)
    
    for _, path := range filePaths {
        go func(p string) {
            sem <- true // Ограничиваем количество одновременных операций
            data, err := os.ReadFile(p)
            <-sem
            // Обработка данных...
        }(path)
    }
}

Сравнение с асинхронными операциями

Сетевые операции в Go обычно используют асинхронный механизм через netpoll (в Linux - epoll, в Windows - IOCP). Когда горутина выполняет сетевую операцию через net/http или net, она не блокируется на системном вызове, а регистрируется в сетевом планировщике и может быть перепланирована сразу.

Ключевые выводы

  1. Синхронные системные вызовы блокируют горутину на уровне ОС, делая ее недоступной для перепланирования.
  2. Планировщик Go создает новые потоки ОС для продолжения выполнения других горутин во время блокировки.
  3. Массовые синхронные вызовы могут деградировать производительность через создание множества потоков ОС.
  4. Асинхронные интерфейсы и пулы помогают минимизировать негативное влияние таких операций.
  5. Для сетевых операций Go использует эффективный асинхронный механизм netpoll, который избегает этой проблемы.

Таким образом, разработчикам следует осознанно использовать синхронные системные вызовы и предпочитать асинхронные паттерны или ограничивать параллельное выполнение таких операций для сохранения эффективности планировщика Go.

Что происходит с горутиной, работающей с синхронными системными вызовами? | PrepBro