Конкурентность и многопоточность в Go: от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Конкурентность и многопоточность в Go: от А до Я

Go (Golang) был спроектирован с учетом современных вычислительных нагрузок. В эпоху многоядерных процессоров и облачных сервисов умение эффективно работать с конкурентностью стало обязательным навыком для бэкенд-разработчика. В этой статье мы разберем, как Go решает задачи параллельного выполнения кода, чем горутины отличаются от потоков ОС, и как правильно использовать каналы и мьютексы.

1. Горутины: легковесные потоки

Горутина (goroutine) — это функция или метод, выполняющийся конкурентно с другими горутинами. В отличие от потоков операционной системы, горутины имеют минимальные накладные расходы: их стек начинается с 2-4 КБ и может динамически расти. Запустить горутину невероятно просто — достаточно добавить ключевое слово go перед вызовом функции.

package main

import (
    "fmt"
    "time"
)

func say(text string) {
    for i := 0; i < 3; i++ {
        fmt.Println(text)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go say("Hello from goroutine")
    say("Hello from main")
    // Даем время горутине завершиться
    time.Sleep(500 * time.Millisecond)
}

Обратите внимание: программа завершается сразу после выполнения функции main(), не дожидаясь завершения запущенных горутин. Для синхронизации мы используем time.Sleep (в реальных проектах — sync.WaitGroup или каналы).

2. Каналы: безопасная передача данных

Каналы (channels) — это типизированные "трубы", через которые горутины обмениваются данными. Они решают две ключевые задачи: синхронизацию выполнения и передачу значений. Каналы бывают буферизированные и небуферизированные.

Небуферизированный канал

Операция отправки блокирует горутину до тех пор, пока другая горутина не выполнит чтение. Это обеспечивает синхронизацию "на лету".

package main

import "fmt"

func main() {
    ch := make(chan string)

    go func() {
        ch <- "Привет из горутины!"
    }()

    msg := <-ch
    fmt.Println(msg)
}

Буферизированный канал

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

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 10
    ch <- 20
    // ch <- 30 // deadlock! буфер переполнен

    fmt.Println(<-ch) // 10
    fmt.Println(<-ch) // 20
}

Range и close

Закрытие канала сигнализирует получателям, что данных больше не будет. Цикл for range автоматически завершается при закрытии канала.

package main

import "fmt"

func producer(ch chan int) {
    for i := 1; i <= 5; i++ {
        ch <- i
    }
    close(ch)
}

func main() {
    ch := make(chan int)
    go producer(ch)

    for val := range ch {
        fmt.Println("Получено:", val)
    }
}

3. Синхронизация и мьютексы

Несмотря на мощь каналов, иногда требуется прямой доступ к общим данным. Пакет sync предоставляет мьютексы (sync.Mutex) и ридер-мьютексы (sync.RWMutex) для защиты критических секций.

package main

import (
    "fmt"
    "sync"
)

var counter = 0
var mu sync.Mutex

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    counter++
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment(&wg)
    }
    wg.Wait()
    fmt.Println("Итоговый счетчик:", counter)
}

Здесь sync.WaitGroup ожидает завершения всех горутин, а мьютекс предотвращает состояние гонки (data race). Без мьютекса результат был бы непредсказуем.

4. Паттерны конкурентности

В Go сложились устоявшиеся шаблоны работы с горутинами и каналами. Рассмотрим три самых популярных.

Worker Pool (пул воркеров)

Ограничивает количество одновременно работающих горутин, что важно для контроля нагрузки.

package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Воркер %d обрабатывает задачу %d\n", id, job)
        results <- job * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    var wg sync.WaitGroup

    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    wg.Wait()
    close(results)

    for r := range results {
        fmt.Println("Результат:", r)
    }
}

Fan-Out / Fan-In

Fan-Out — распределение задач между несколькими горутинами. Fan-In — сбор результатов в один канал.

package main

import (
    "fmt"
    "sync"
)

func producer(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func merge(cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    output := func(c <-chan int) {
        for n := range c {
            out <- n
        }
        wg.Done()
    }

    wg.Add(len(cs))                

Блоги

Рекомендаций по книгам