Расскажите о модели конкурентности в Go и как она связана с системными потоками.

Ответ

В Go управление конкурентностью реализовано через горутины (goroutines) и каналы, а не через прямое управление системными потоками (тредами). Модель, которую использует Go, называется M:N планированием.

Это означает, что M горутин выполняются поверх N системных потоков. Обычно N равно количеству доступных ядер процессора (GOMAXPROCS).

Ключевые компоненты планировщика Go (GMP model):

  • G (Goroutine): Легковесный поток выполнения. Имеет свой небольшой стек (~2 КБ), который может расти и сжиматься. Создание горутины намного дешевле создания системного потока.
  • M (Machine): Системный поток (OS thread), управляемый операционной системой.
  • P (Processor): Контекст для выполнения Go-кода. У каждого P есть локальная очередь готовых к выполнению горутин. Количество P по умолчанию равно runtime.NumCPU().

Как это работает:
Планировщик Go распределяет горутины (G) по процессорам (P). Системный поток (M) "берёт" себе P и начинает выполнять горутины из его очереди. Если горутина блокируется (например, на системном вызове или ожидании данных из канала), M отсоединяется от P, а планировщик может прикрепить к этому P другой M (или создать новый), чтобы продолжить выполнение других горутин из очереди. Это позволяет эффективно утилизировать процессорные ядра, не блокируя потоки.

Пример с Worker Pool:

// worker — горутина, которая обрабатывает задачи из канала jobs
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        // какая-то работа
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Запуск 3 горутин-воркеров
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Отправка 5 задач в канал
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // Закрываем канал, чтобы воркеры завершились

    // Сбор результатов
    for r := 1; r <= 5; r++ {
        <-results
    }
}

Синхронизация: Для координации горутин используются каналы (идиоматический способ) и примитивы из пакета sync, такие как sync.Mutex для защиты общих данных и sync.WaitGroup для ожидания завершения группы горутин.