Как реализована модель конкурентности в Go?

Ответ

Модель конкурентности в Go основана на концепции "Communicating Sequential Processes" (CSP) Тони Хоара. Основной принцип: "Не обменивайтесь данными через общую память; обменивайтесь памятью через каналы".

Реализация построена на двух ключевых элементах:

  1. Горутины (Goroutines)

    • Это легковесные потоки, управляемые планировщиком Go (Go Scheduler), а не напрямую операционной системой.
    • Они намного дешевле системных потоков. Начальный размер стека горутины всего ~2 КБ, в то время как у системного потока — мегабайты. Это позволяет создавать сотни тысяч и даже миллионы горутин одновременно.
    • Запускаются с помощью ключевого слова go перед вызовом функции: go myFunction().
  2. Каналы (Channels)

    • Это типизированные "трубы", через которые горутины могут безопасно обмениваться данными, не прибегая к мьютексам и другим примитивам синхронизации.
    • Обеспечивают синхронизацию: отправка в канал блокируется, пока кто-то не будет готов принять данные, и наоборот (для небуферизированных каналов).
    • Бывают небуферизированные (make(chan int)) и буферизированные (make(chan int, 10)).

Пример: Worker Pool

В этом примере мы создаем пул воркеров (горутин), которые обрабатывают задачи из канала jobs и отправляют результаты в канал results. sync.WaitGroup используется для ожидания завершения всех горутин.

package main

import (
    "fmt"
    "sync"
)

// worker - это горутина, которая читает из канала jobs и пишет в results
func worker(id int, wg *sync.WaitGroup, jobs <-chan int, results chan<- int) {
    defer wg.Done() // Сообщаем WaitGroup, что горутина завершила работу
    for j := range jobs {
        fmt.Printf("worker %d started job %dn", id, j)
        results <- j * 2
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    var wg sync.WaitGroup

    // Запускаем 3 воркера
    for w := 1; w <= 3; w++ {
        wg.Add(1) // Увеличиваем счетчик WaitGroup
        go worker(w, &wg, jobs, results)
    }

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

    // Ждем, пока все воркеры завершат свою работу
    wg.Wait()

    // Собираем все результаты
    // Важно: этот код должен выполняться после wg.Wait(),
    // чтобы гарантировать, что все результаты уже отправлены.
    close(results)
    for r := range results {
        fmt.Println("Result:", r)
    }
}

Дополнительные инструменты:

  • select: Позволяет горутине ждать операции сразу в нескольких каналах.
  • Пакет sync: Предоставляет традиционные примитивы синхронизации, такие как Mutex, RWMutex, WaitGroup, Cond и Once, когда модель каналов не подходит.