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

Что управляет горутинами?

2.0 Middle🔥 211 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Что управляет горутинами в Go?

В языке Go горутины (goroutines) управляются не напрямую программистом или операционной системой, а через встроенный механизм под названием планировщик Go (Go scheduler). Этот планировщик является частью runtime Go и работает на уровне пользовательского пространства (user-space), координируя выполнение горутин на ограниченном количестве потоков ОС (OS threads).

Ключевые компоненты управления

1. Планировщик Go (M:N Scheduler)

Планировщик реализует модель M:N, где:

  • M — горутины (легкие потоки, тысячи или миллионы)
  • N — потоки ОС (обычно равны количеству логических процессоров, GOMAXPROCS)
// Пример запуска горутин
func main() {
    // Эти горутины будут управляться планировщиком
    go task1()
    go task2()
    
    time.Sleep(time.Second) // Даем время на выполнение
}

func task1() { fmt.Println("Task 1") }
func task2() { fmt.Println("Task 2") }

Планировщик решает:

  • Когда запускать горутину (при вызове go)
  • Когда переключать между горутинами (при блокировках или через cooperative scheduling)
  • На каком потоке ОС выполнять горутину

2. Каналы (Channels) и синхронизация

Каналы — основной механизм коммуникации и синхронизации горутин. Они позволяют безопасно передавать данные и координировать выполнение.

func worker(ch chan int) {
    result := compute()
    ch <- result // Отправка результата (может блокировать горутину)
}

func main() {
    ch := make(chan int)
    go worker(ch)
    value := <-ch // Получение (может блокировать основную горутину)
}

Когда горутина пытается читать из пустого канала или писать в заполненный, планировщик блокирует ее и переключает на другую готовую горутину.

3. Примитивы синхронизации из пакета sync

  • sync.Mutex, sync.RWMutex — для исключительного доступа к данным
  • sync.WaitGroup — для ожидания завершения группы горутин
  • sync.Cond — для условной синхронизации
var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()   // Блокировка — планировщик может переключить горутину
    counter++
    mu.Unlock() // Освобождение
}

4. Контексты (Context)

Пакет context позволяет управлять временем жизни горутин, особенно полезно для:

  • Распространения сигналов завершения
  • Установки deadlines и timeoutов
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

go process(ctx) // Горутина должна отслеживать ctx.Done()

5. Runtime и системные события

Планировщик реагирует на определенные точки в коде (scheduling points):

  • Вызовы каналов (операции отправки/получения)
  • Системные вызовы (например, файловые операции)
  • Сетевые операции
  • Операции с примитивами синхронизации
  • Вызов runtime.Gosched() (явная уступка планировщику)
func cooperative() {
    for i := 0; i < 10; i++ {
        runtime.Gosched() // Явно уступаем другим горутинам
    }
}

Как работает планировщик

  1. Инициация: При вызове go функция() runtime создает горутину (начальный размер стека ~2KB).
  2. Очереди: Горутины помещаются в локальные (per-thread) или глобальные очереди планировщика.
  3. Выполнение: Планировщик назначает горутины на потоки ОС (обычно 1 поток на ядро CPU).
  4. Блокировка: Если горутина блокируется (канал, системный вызов), поток ОС может выполнять другие горутины.
  5. Прерывание: Планировщик периодически прерывает горутины для балансировки нагрузки (через системные вызовы или асинхронные сигналы).

Важные особенности

  • Кооперативная многозадачность: Горутины уступают контроль планировщику в точках блокировки, а не через вытеснение (preemption) как потоки ОС. Однако современные версии Go (с 1.14) имеют асинхронное вытеснение для предотвращения зависания одной горутины.
  • Малый оверхед: Переключение между горутинами происходит в runtime Go, без дорогостоящего переключения контекста ОС.
  • Не гарантируется порядок: Планировщик не гарантирует порядок выполнения горутин — это деталь реализации.

Пример демонстрации управления

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func main() {
    // Управление количеством потоков ОС
    runtime.GOMAXPROCS(2) // Используем 2 потока ОС
    
    var wg sync.WaitGroup
    ch := make(chan string, 3)
    
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            // Точка планирования: отправка в канал
            ch <- fmt.Sprintf("Worker %d started", id)
            
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
            
            ch <- fmt.Sprintf("Worker %d finished", id)
        }(i)
    }
    
    go func() {
        wg.Wait()
        close(ch)
    }()
    
    // Получаем сообщения — горутины блокируются при отправке
    for msg := <-ch; msg != ""; msg = <-ch {
        fmt.Println(msg)
    }
}

В этом примере:

  1. runtime.GOMAXPROCS управляет количеством потоков ОС
  2. sync.WaitGroup координирует завершение горутин
  3. Канал ch синхронизирует отправку сообщений
  4. Планировщик автоматически переключает горутины при операциях с каналом и time.Sleep

Таким образом, горутины управляются совместно:

  • Планировщиком runtime Go (техническая координация выполнения)
  • Механизмами синхронизации (каналы, mutexes) — логическая координация
  • Программистом через использование этих механизмов и понимание модели выполнения

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

Что управляет горутинами? | PrepBro