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

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

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

Начать курс

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

Конкурентность (concurrency) — одна из ключевых особенностей языка Go, заложенная в его архитектуру на уровне рантайма и синтаксиса. В отличие от многих других языков, где многопоточность реализуется через сложные библиотеки и требует ручного управления потоками, Go предлагает лёгкие горутины (goroutines) и каналы (channels). На продвинутом уровне важно не просто запустить несколько задач, а грамотно синхронизировать их, избежать гонок данных и эффективно использовать системные ресурсы. В этой статье мы разберём продвинутые техники работы с конкурентностью в Go.

1. Горутины: управление жизненным циклом и приоритеты

Горутины — это легковесные потоки, которые управляются рантаймом Go. Они запускаются с помощью ключевого слова go. Однако на продвинутом уровне нужно понимать, как контролировать их выполнение и завершение.

Базовый запуск горутины:

package main

import (
    "fmt"
    "time"
)

func worker(id int) {
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    for i := 1; i <= 5; i++ {
        go worker(i)
    }
    time.Sleep(2 * time.Second) // Костыль: ждём завершения
}

Проблема: в этом примере мы используем time.Sleep для ожидания, что ненадёжно. На продвинутом уровне применяются каналы и sync.WaitGroup.

2. Синхронизация с помощью sync.WaitGroup

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

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Уменьшаем счётчик при завершении
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Add(1) // Увеличиваем счётчик перед запуском
        go worker(i, &wg)
    }
    wg.Wait() // Блокируем до обнуления счётчика
    fmt.Println("All workers completed")
}

Важно: wg.Add() должен вызываться до запуска горутины, иначе возможна гонка данных. Используйте defer wg.Done() внутри горутины для гарантии уменьшения счётчика даже при панике.

3. Каналы: буферизация, направление и select

Каналы — основной механизм обмена данными между горутинами. На продвинутом уровне используются буферизированные каналы, однонаправленные каналы и оператор select.

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

Буферизированные каналы позволяют отправлять данные без немедленного получения. Это уменьшает блокировки и повышает пропускную способность.

package main

import "fmt"

func main() {
    ch := make(chan int, 3) // канал с буфером на 3 элемента
    ch <- 1
    ch <- 2
    ch <- 3
    // ch <- 4 // блокировка, так как буфер полон
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

3.2. Однонаправленные каналы

Для безопасности типов и явного указания роли канала используются однонаправленные каналы: chan<- (только отправка) и <-chan (только получение).

package main

import "fmt"

func sender(out chan<- int) {
    for i := 0; i < 5; i++ {
        out <- i
    }
    close(out)
}

func receiver(in <-chan int) {
    for val := range in {
        fmt.Println(val)
    }
}

func main() {
    ch := make(chan int)
    go sender(ch)
    receiver(ch)
}

3.3. Оператор select

select позволяет горутине ожидать несколько каналов одновременно. Это основа для таймаутов, неблокирующих операций и мультиплексирования.

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "two"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Received from ch1:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Received from ch2:", msg2)
    case <-time.After(3 * time.Second):
        fmt.Println("Timeout")
    // default: // неблокирующий выбор
    //     fmt.Println("No message ready")
    }
}

4. Мьютексы и атомарные операции

Каналы — не единственный способ синхронизации. Для защиты общих данных от гонок используются мьютексы (sync.Mutex) и атомарные операции (sync/atomic).

4.1. sync.Mutex

Мьютекс блокирует доступ к критической секции. Важно помнить о дедлоках: всегда используйте defer mu.Unlock().

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

func main() {
    var wg sync.WaitGroup
    counter := Counter{}

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }
    wg.Wait()
    fmt.Println("Final counter:", counter.Value())
}

4.2. Атомарные операции

Для простых счётчиков и флагов атомарные операции быстрее мьютексов

Блоги

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