Конкурентность и многопоточность

Эта глава — жемчужина языка Go. Именно благодаря своей модели многопоточности Go стал стандартом в облачных вычислениях и высоконагруженных системах.

В современном мире программы редко делают одно дело за раз. Веб-сервер обрабатывает тысячи запросов одновременно, а браузер параллельно загружает картинки, текст и видео.

Go был спроектирован с учетом конкурентности (concurrency). Основная идея Go звучит так: «Не общайтесь, делясь памятью; делитесь памятью, общаясь».

Горутины (Goroutines)

Горутина — это функция, которая выполняется конкурентно с другими функциями. Это не поток ОС, это гораздо более легкая сущность. Вы можете запустить сотни тысяч горутин, и они будут потреблять минимум оперативной памяти.

Чтобы запустить функцию в горутине, просто добавьте ключевое слово go перед её вызовом:

package main

import (
    "fmt"
    "time"
)

func f(n int) {
    for i := 0; i < 5; i++ {
        fmt.Println(n, ":", i)
        time.Sleep(time.Millisecond * 100)
    }
}

func main() {
    go f(0) // Запуск горутины
    var input string
    fmt.Scanln(&input) // Ожидаем ввода, чтобы main не завершился раньше времени
}

Почему Scanln?

Когда завершается функция main, все остальные горутины мгновенно уничтожаются. В реальных программах вместо «костыля» с вводом используют sync.WaitGroup, который позволяет дождаться завершения всех задач.


Каналы (Channels)

Каналы — это «трубы», по которым горутины передают данные друг другу. Они позволяют синхронизировать выполнение без сложных блокировок (mutex).

package main

import "fmt"

func pinger(c chan string) {
    for {
        c <- "ping" // Отправка данных в канал
    }
}

func printer(c chan string) {
    for {
        msg := <- c // Получение данных из канала
        fmt.Println(msg)
    }
}

func main() {
    c := make(chan string) // Создание канала

    go pinger(c)
    go printer(c)

    var input string
    fmt.Scanln(&input)
}

Блокировка

Операции с каналами по умолчанию блокирующие:

  • Если вы пытаетесь отправить данные в канал, горутина-отправитель «засыпает», пока кто-то другой не придет их забрать.
  • Если вы пытаетесь прочитать из канала, горутина-получатель ждет, пока в канале что-то появится.

Направленные каналы

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

  • c chan<- string — только для отправки (send-only).
  • c <-chan string — только для получения (receive-only).

Оператор Select

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

select {
case msg1 := <- c1:
    fmt.Println("Получено из c1:", msg1)
case msg2 := <- c2:
    fmt.Println("Получено из c2:", msg2)
case <- time.After(time.Second):
    fmt.Println("Тайм-аут: никто не ответил за 1 секунду")
}

time.After создает канал, который отправит сообщение спустя указанное время. Это стандартный способ реализации тайм-аутов в Go.


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

По умолчанию каналы имеют нулевую емкость (синхронные). Но можно создать канал с «буфером»:

c := make(chan int, 10)

Буферизированный канал не блокирует отправителя, пока буфер не заполнится. Это похоже на почтовый ящик: вы можете положить туда 10 писем, даже если почтальон еще не пришел их забирать.


Задачи

  • Теория: В чем разница между chan int, chan<- int и <-chan int?
  • Практика: Напишите свою функцию Sleep(n int), используя только time.After.
  • Синхронизация: Исследуйте пакет sync и перепишите первый пример с использованием sync.WaitGroup вместо fmt.Scanln.
  • Буфер: Что произойдет, если отправить 11-е сообщение в буферизированный канал емкостью 10, если его никто не читает?

Полезные ссылки

  1. Go by Example: Goroutines
  2. Go by Example: Channels
  3. Visualizing Concurrency in Go — потрясающие 3D визуализации работы горутин.
  4. The Nature of Channels — глубокий разбор механики каналов.