Как работает вытеснение горутин в Go? Является ли планировщик полностью вытесняющим?

Ответ

Нет, планировщик Go не является полностью вытесняющим (preemptive), он использует гибридную модель.

До Go 1.14 планировщик был в основном кооперативным. Это означало, что горутина уступала процессорное время другой горутине только в определенных точках (так называемых "safe points"):

  • Вызовы функций (не для всех вызовов)
  • Операции с каналами
  • Системные вызовы
  • Ожидание блокировок

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

// Этот цикл без вызовов функций мог "подвесить" поток в версиях Go < 1.14
for {
    // Никаких вызовов функций или каналов
}

Начиная с Go 1.14, было введено асинхронное вытеснение. Теперь планировщик может принудительно прервать горутину, если она выполняется слишком долго (более 10 мс) без кооперативного уступания. Это решает проблему "застрявших" циклов.

Итог:

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

Несмотря на улучшения, для написания отзывчивого кода все еще рекомендуется избегать очень длинных вычислений в циклах без точек уступания или при необходимости явно вызывать runtime.Gosched().