Конкурентность и многопоточность в Go: goroutines и каналы

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

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

Начать курс

Введение в конкурентность в Go

Язык Go (Golang) был разработан в Google с учётом современных требований к производительности и масштабируемности. Одной из ключевых особенностей Go является встроенная поддержка конкурентности (concurrency) — способности программы выполнять несколько задач одновременно. В отличие от традиционной многопоточности, где потоки управляются операционной системой, Go предлагает легковесные goroutines и удобные примитивы синхронизации — каналы.

В этой статье мы разберём:

  • Что такое goroutines и как их запускать
  • Каналы для обмена данными между goroutines
  • Мьютексы для безопасного доступа к общим ресурсам
  • Практические примеры и шаблоны конкурентного кода

1. Goroutines: лёгкие потоки Go

Goroutine — это функция, выполняющаяся конкурентно с другими goroutines. Она занимает всего несколько килобайт стека (в отличие от потоков ОС, где стек может быть мегабайтным). Запустить goroutine можно просто добавив ключевое слово go перед вызовом функции.

Пример: Запуск goroutines

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("Привет, %s! (итерация %d)\n", name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go sayHello("Алиса")  // Запуск goroutine
    go sayHello("Боб")    // Ещё одна goroutine
    time.Sleep(1 * time.Second) // Ждём завершения
    fmt.Println("Главная функция завершена")
}

Важно: Программа завершается, когда завершается функция main, даже если другие goroutines ещё работают. Поэтому мы используем time.Sleep для ожидания. В реальном коде лучше использовать sync.WaitGroup.

Использование sync.WaitGroup

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Уменьшаем счётчик при завершении
    fmt.Printf("Работник %d начал работу\n", id)
    // ... какая-то работа ...
    fmt.Printf("Работник %d закончил работу\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1) // Увеличиваем счётчик перед запуском goroutine
        go worker(i, &wg)
    }

    wg.Wait() // Блокируем main до завершения всех goroutines
    fmt.Println("Все работники завершили работу")
}

2. Каналы: обмен данными между goroutines

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

Создание и использование каналов

package main

import "fmt"

func main() {
    // Создаём канал для целых чисел
    ch := make(chan int)

    // Запускаем goroutine, которая отправляет данные
    go func() {
        ch <- 42 // Отправляем значение в канал
    }()

    // Получаем значение из канала
    value := <-ch
    fmt.Println("Получено:", value)
}

По умолчанию каналы небуферизированные: отправка блокируется до тех пор, пока получатель не будет готов принять данные, и наоборот. Это обеспечивает синхронизацию.

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

package main

import "fmt"

func main() {
    // Канал с буфером на 3 элемента
    ch := make(chan string, 3)

    // Отправляем без блокировки (пока буфер не заполнен)
    ch <- "один"
    ch <- "два"
    ch <- "три"

    // Читаем
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Закрытие каналов и range

package main

import "fmt"

func fibonacci(n int, ch chan int) {
    a, b := 0, 1
    for i := 0; i < n; i++ {
        ch <- a
        a, b = b, a+b
    }
    close(ch) // Закрываем канал после отправки всех данных
}

func main() {
    ch := make(chan int, 10)
    go fibonacci(10, ch)

    // Читаем из канала, пока он не будет закрыт
    for val := range ch {
        fmt.Println(val)
    }
}

3. Select: работа с несколькими каналами

Оператор select позволяет goroutine ожидать несколько операций с каналами одновременно. Он выбирает первый готовый канал.

package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "данные из канала 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "данные из канала 2"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Получено из ch1:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Получено из ch2:", msg2)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("Таймаут, никто не ответил")
    }
}

В этом примере select сработает по таймауту, так как оба канала будут готовы только через 1 и 2 секунды соответственно. Это полезно для реализации тайм-аутов и неблокирующих операций.

4. Мьютексы: защита общих данных

Хотя каналы — предпочтительный способ синхронизации в Go, иногда требуется защитить общую память. Для этого используется sync.Mutex.

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 <                

Блоги

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