Может ли горутина заблокировать выполнение других горутин на том же потоке ОС?

Ответ

Да, такое возможно, особенно в старых версиях Go. Это связано с моделью кооперативной многозадачности, которую использует планировщик Go.

Планировщик Go не может прервать горутину в любой момент времени. Он ожидает, пока горутина сама уступит управление, достигнув точки вытеснения (preemption point). Такими точками являются:

  • Вызовы функций (с Go 1.14 это стало надежнее).
  • Операции с каналами.
  • Системные вызовы (например, чтение файла или сетевое взаимодействие).
  • Явный вызов runtime.Gosched().

Проблема в старых версиях Go (до 1.14)

Если горутина выполняла долгий цикл без точек вытеснения, она могла монополизировать поток ОС (P), не давая другим горутинам из очереди этого потока выполняться.

// В Go < 1.14 этот код заблокирует поток ОС
func main() {
    runtime.GOMAXPROCS(1) // Ограничимся одним потоком для наглядности

    go func() {
        // Этот цикл не содержит точек вытеснения
        for {}
    }()

    // Эта горутина, скорее всего, никогда не получит управление
    go func() {
        println("Hello from another goroutine!")
    }()

    time.Sleep(time.Second)
    println("Done")
}

Решение в современных версиях Go (1.14+)

Начиная с версии Go 1.14, была введена асинхронная вытесняющая многозадачность (asynchronous preemption). Теперь компилятор вставляет специальные проверки в начало функций и в циклы. Если горутина выполняется слишком долго (десятки миллисекунд), планировщик может принудительно приостановить её и передать управление другой горутине.

Это делает проблему "зависания" из-за долгих циклов гораздо менее вероятной, но не устраняет её полностью. Например, в циклах без вызовов функций на некоторых архитектурах проблема может сохраняться.

Вывод

  • Да, горутина может заблокировать поток, если выполняет долгую, непрерывную вычислительную работу без точек вытеснения.
  • В Go 1.14+ эта проблема в значительной степени решена благодаря асинхронному вытеснению, но понимание механизма остается важным.
  • Для ручного управления можно использовать runtime.Gosched(), чтобы явно указать планировщику, что горутина готова уступить процессор.