Почему горутина переключается быстрее, чем тред?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему горутина переключается быстрее, чем тред?
Горутины в Go переключаются быстрее, чем традиционные потоки операционной системы, из-за кооперативной многозадачности, легковесной реализации и интеграции с планировщиком Go.
1. Кооперативная многозадачность против вытесняющей
Горутины используют кооперативную модель: они сами уступают управление в ключевых точках (вызовы runtime.Gosched(), блокировки на I/O, каналы, системные вызовы). Потоки ОС используют вытесняющую многозадачность: планировщик ОС силой переключает потоки по таймеру, независимо от их состояния.
Контекст переключения горутин включает только регистры и указатель стека, а контекст потока ОС требует сохранения полного состояния (регистры, стек, сигналы, состояние файловых дескрипторов). Пример переключения горутины:
package main
func main() {
go func() {
// Горутина уступит управление при вызове Gosched
runtime.Gosched()
}()
}
Вытеснение потоков требует вмешательства ОС и полного сохранения контекста, что тяжелее.
2. Легковесность горутин
Стек горутин динамический и маленький (начально 2KB, растёт/сжимается). Стек потока ОС фиксированный и большой (обычно 1-8MB).
Планировщик Go управляет горутинами в пользовательском пространстве, избегая перехода в ядро ОС. Потоки управляются ядром ОС, что требует системных вызовов.
Пример создания тысяч горутин:
package main
import "time"
func worker(id int) {
time.Sleep(time.Second)
}
func main() {
for i := 0; i < 10000; i++ {
go worker(i) // Легковесные горутины
}
}
Создание 10000 потоков ОС перегружает систему.
3. Планировщик Go: модель M:N
Планировщик Go мультиплексирует M горутин на N потоков ОС (обычно N = число ядер). Это даёт:
- Меньше переключений потоков ОС: горутины переключаются внутри одного потока ОС.
- Оптимизированное распределение: планировщик Go балансирует горутины между потоками ОС.
// Планировщик эффективно распределяет горутины на 4 потока ОС (4 ядра)
runtime.GOMAXPROCS(4)
Переключение потоков ОС требует системного вызова и полного сохранения контекста.
4. Эффективная работа с блокирующими операциями
При блокирующих операциях (I/O, системные вызовы) планировщик Go отсоединяет горутину от потока ОС, позволяя другим горутинам использовать поток.
Пример работы с сетью:
package main
import "net/http"
func fetch(url string) {
resp, _ := http.Get(url) // Блокирующий вызов
// Планировщик переключит горутину, пока ждём ответ
}
Поток ОС при блокировании просто ждёт, переключение требует вытеснения.
5. Статистика и профилирование
Переключение горутин занимает десятки-сотни наносекунд, переключение потоков ОС — микросекунды (включая переход в ядро). Планировщик Go оптимизирует:
- Локальные очереди для каждого потока ОС.
- Глобальную очередь для балансировки.
- Работа в пространстве пользователя.
Пример профилирования:
import "runtime"
func main() {
// Профилирование планировщика
runtime.SetBlockProfileRate(1)
}
Сравнение в таблице
| Критерий | Горутины | Потоки ОС |
|---|---|---|
| Модель многозадачности | Кооперативная | Вытесняющая |
| Размер стека | 2KB (динамический) | 1-8MB (фиксированный) |
| Контекст переключения | Регистры + указатель стека | Полный контекст ОС |
| Планировщик | Пользовательский (Go) | Ядро ОС |
| Создание/управление | Десятки микросекунд | Миллисекунды |
| Переключение | Наносекунды | Микросекунды |
Заключение
Горутины переключаются быстрее, благодаря:
- Кооперативной модели без накладных вытеснений.
- Легковесным стекам с динамическим размером.
- Пользовательскому планировщику без перехода в ядро.
- Модели M:N с минимальным использованием потоков ОС.
Это делает Go эффективным для высоконагруженных параллельных систем с тысячами одновременных задач, где потоки ОС непрактичны.