Эта глава — жемчужина языка 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, если его никто не читает?
Полезные ссылки
- Go by Example: Goroutines
- Go by Example: Channels
- Visualizing Concurrency in Go — потрясающие 3D визуализации работы горутин.
- The Nature of Channels — глубокий разбор механики каналов.