Как происходит исполнение горутин?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Принципы исполнения горутин в Go
Горутины (goroutines) — это легковесные потоки выполнения, реализованные на уровне среды выполнения Go (runtime). Их исполнение кардинально отличается от классических потоков ОС и строится на трех ключевых компонентах: модели многопоточности M:N, планировщике (scheduler) и концепции разбиения на сегменты (work-stealing).
Модель M:N и компоненты рантайма
Go использует гибридную модель M:N, где:
- M — потоки ОС (machine threads), управляемые рантаймом
- N — горутины (goroutines), логические потоки приложения
- G — структура данных, представляющая горутину (хранит стек, статус, контекст)
- P — процессоры (processors), виртуальные контексты выполнения (максимум
GOMAXPROCS) - Scheduler — планировщик, распределяющий G по M и P
Такая модель позволяет запускать миллионы горутин при небольшом количестве системных потоков, минимизируя накладные расходы на создание и переключение контекста.
// Пример создания горутин
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Printf("Горутина %d начала работу\n", id)
time.Sleep(500 * time.Millisecond)
fmt.Printf("Горутина %d завершила работу\n", id)
}
func main() {
// Запускаем 5 горутин
for i := 1; i <= 5; i++ {
go worker(i)
}
// Ждем завершения горутин
time.Sleep(2 * time.Second)
}
Работа планировщика (scheduler)
Планировщик Go — это кооперативный вытесняющий планировщик (cooperative preemptive scheduler), который управляет выполнением горутин следующим образом:
Триггеры перепланирования:
- Системные вызовы (I/O операции, системные запросы)
- Операции с каналами (чтение/запись, если операция блокируется)
- Вызов
runtime.Gosched()— явная уступка процессорного времени - Сетевые операции через netpoller (асинхронный ввод-вывод)
- Сборка мусора (stop-the-world фазы)
- Истечение кванта времени (после 10ms непрерывного выполнения)
Алгоритм work-stealing:
Каждый P (процессор) поддерживает локальную очередь горутин (runqueue). Когда очередь пуста, процессор "ворует" работу из:
- Глобальной очереди планировщика
- Очередей других процессоров
- Сетевого планировщика (netpoller)
// Демонстрация перепланирования
package main
import (
"fmt"
"runtime"
"time"
)
func cpuIntensive() {
sum := 0
for i := 0; i < 1000000000; i++ {
sum += i
}
fmt.Println("Завершена CPU-intensive горутина")
}
func ioIntensive() {
time.Sleep(100 * time.Millisecond)
fmt.Println("Завершена I/O-intensive горутина")
}
func main() {
// Устанавливаем 2 процессора
runtime.GOMAXPROCS(2)
go cpuIntensive()
go ioIntensive()
time.Sleep(1 * time.Second)
}
Жизненный цикл горутины
- Создание — аллокация начального стека (2KB, растет динамически)
- Постановка в очередь — попадает в локальную очередь P
- Исполнение — выполняется на потоке M, привязанном к P
- Блокировка — при системных вызовах, операциях с каналами
- Пробуждение — перемещается в очередь исполнения
- Завершение — освобождает ресурсы, может вызвать панику
Стек горутин и управление памятью
Каждая горутина начинается с маленького стека (обычно 2KB), который динамически растет и сокращается:
- Segmented stacks (ранние версии) — прерывание при переполнении
- Stack copying (современные версии) — копирование в больший сегмент
Отличия от потоков ОС
| Аспект | Горутины | Потоки ОС |
|---|---|---|
| Создание | ~2-4 µs, несколько KB | ~100 µs, 1-8 MB |
| Переключение | ~100 ns (в пространстве пользователя) | ~1-10 µs (системный вызов) |
| Планирование | Кооперативное + вытеснение по времени | Вытесняющее (ОС) |
| Параллелизм | M:N модель, тысячи легко | 1:1 модель, сотни сложно |
Практические рекомендации
- Не блокируйте планировщик — избегайте долгих вычислений без точек вытеснения
- Используйте
runtime.Gosched()для CPU-bound операций - Контролируйте утечки горутин — всегда предусматривайте условия выхода
- Используйте пулы воркеров для задач с известным количеством параллелизма
- Мониторьте использование через
runtime.NumGoroutine()
// Пример с пулом воркеров
package main
import (
"fmt"
"sync"
)
func workerPool(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
results <- job * 2 // Обработка задачи
}
}
func main() {
const numWorkers = 5
const numJobs = 20
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// Запускаем пул воркеров
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go workerPool(i, jobs, results, &wg)
}
// Отправляем задания
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Собираем результаты
go func() {
wg.Wait()
close(results)
}()
// Выводим результаты
for result := range results {
fmt.Println("Результат:", result)
}
}
Итог: Горутины предоставляют исключительно эффективную абстракцию для параллельного программирования, сочетающую низкие накладные расходы с гибким управлением параллелизмом. Понимание их внутреннего устройства позволяет писать более производительные и предсказуемые concurrent-приложения на Go.