Многопоточность
Очень часто, большие приложения состоят из множества небольших подпрограмм. Например, web-сервер принимает запросы от браузера и отправляет HTML страницы в ответ. Каждый такой запрос выполняется как отдельная небольшая программа.
Такой способ идеально подходит для подобных приложений, так как обеспечивает возможность одновременного запуска множества более мелких компонентов (обработки нескольких запросов одновременно, в случае веб-сервера). Одновременное выполнение более чем одной задачи известно как многопоточность. Go имеет богатую функциональность для работы с многопоточностью, в частности, такие инструменты как горутины и каналы.
Горутины
Горутина — это функция, которая может работать параллельно с другими функциями. Для создания горутины используется ключевое словоgo, за которым следует вызов функции.
package main
import "fmt"
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
}
}
func main() {
go f(0)
var input string
fmt.Scanln(&input)
}
Эта программа состоит из двух горутин. Функцияmain, сама по себе, является горутиной. Вторая горутина создаётся, когда мы вызываемgo f(0). Обычно, при вызове функции, программа выполнит все конструкции внутри вызываемой функции, а только потом перейдет к, следующей после вызова, строке. С горутиной программа немедленно прейдет к следующей строке, не дожидаясь, пока вызываемая функция завершится. Вот почему здесь присутствует вызовScanln, без него программа завершится еще перед тем, как ей удастся вывести числа.
Горутины очень легкие, мы можем создавать их тысячами. Давайте изменим программу так, чтобы она запускала 10 горутин:
func main() {
for i := 0; i < 10; i++ {
go f(i)
}
var input string
fmt.Scanln(&input)
}
При запуске вы наверное заметили, что все горутины выполняются последовательно, а не одновременно, как вы того ожидали. Давайте добавим небольшую задержку функции с помощью функцииtime.Sleepиrand.Inin:
package main
import (
"fmt"
"time"
"math/rand"
)
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
amt := time.Duration(rand.Intn(250))
time.Sleep(time.Millisecond * amt)
}
}
func main() {
for i := 0; i < 10; i++ {
go f(i)
}
var input string
fmt.Scanln(&input)
}
fвыводит числа от 0 до 10, ожидая от 0 до 250 мс после каждой операции вывода. Теперь горутины должны выполняться одновременно.
Каналы
Каналы обеспечивают возможность общения нескольких горутин друг с другом, чтобы синхронизировать их выполнение. Вот пример программы с использованием каналов:
package main
import (
"fmt"
"time"
)
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping"
}
}
func printer(c chan string) {
for {
msg := <- c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}
func main() {
var c chan string = make(chan string)
go pinger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
Программа будет постоянно выводить «ping» (нажмите enter, чтобы её остановить). Тип канала представлен ключевым словомchan, за которым следует тип, который будет передаваться по каналу (в данном случае мы передаем строки). Оператор<-(стрелка влево) используется для отправки и получения сообщений по каналу. Конструкцияc <- "ping"означает отправку"ping", аmsg := <- c— его получение и сохранение в переменнуюmsg. Строка сfmtможет быть записана другим способом:fmt.Println(<-c), тогда можно было бы удалить предыдущую строку.
Данное использование каналов позволяет синхронизировать две горутины. Когдаpingerпытается послать сообщение в канал, он ожидает, покаprinterбудет готов получить сообщение. Такое поведение называется блокирующим. Давайте добавим ещё одного отправителя сообщений в программу и посмотрим, что будет. Добавим эту функцию:
func ponger(c chan string) {
for i := 0; ; i++ {
c <- "pong"
}
}
и изменим функциюmain:
func main() {
var c chan string = make(chan string)
go pinger(c)
go ponger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
Теперь программа будет выводить на экран тоping, тоpongпо очереди.
Направление каналов
Мы можем задать направление передачи сообщений в канале, сделав его только отправляющим или принимающим. Например, мы можем изменить функциюpinger:
func pinger(c chan <- string)
и каналcбудет только отправлять сообщение. Попытка получить сообщение из каналаcвызовет ошибку компилирования. Также мы можем изменить функциюprinter:
func printer(c <-chan string)
Существуют и двунаправленные каналы, которые могут быть переданы в функцию, принимающую только принимающие или отправляющие каналы. Но только отправляющие или принимающие каналы не могут быть переданы в функцию, требующую двунаправленного канала!
Оператор Select
В языке Go есть специальный операторselectкоторый работает какswitch, но для каналов:
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
for {
c1 <- "from 1"
time.Sleep(time.Second * 2)
}
}()
go func() {
for {
c2 <- "from 2"
time.Sleep(time.Second * 3)
}
}()
go func() {
for {
select {
case msg1 := <- c1:
fmt.Println(msg1)
case msg2 := <- c2:
fmt.Println(msg2)
}
}
}()
var input string
fmt.Scanln(&input)
}
Эта программа выводит «from 1» каждые 2 секунды и «from 2» каждые 3 секунды. Операторselectвыбирает первый готовый канал, и получает сообщение из него, или же передает сообщение через него. Когда готовы несколько каналов, получение сообщения происходит из случайно выбранного готового канала. Если же ни один из каналов не готов, оператор блокирует ход программы до тех пор, пока какой-либо из каналов будет готов к отправке или получению.
Обычноselectиспользуется для таймеров:
select {
case msg1 := <- c1:
fmt.Println("Message 1", msg1)
case msg2 := <- c2:
fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
fmt.Println("timeout")
}
time Afterсоздаёт канал, по которому посылаем метки времени с заданным интервалом. В данном случае мы не заинтересованы в значениях временных меток, поэтому мы не сохраняем его в переменные. Также мы можем задать команды, которые выполняются по умолчанию, используя конструкциюdefault:
select {
case msg1 := <- c1:
fmt.Println("Message 1", msg1)
case msg2 := <- c2:
fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
fmt.Println("timeout")
default:
fmt.Println("nothing ready")
}
Выполняемые по умолчанию команды исполняются сразу же, если все каналы заняты.
Буферизированный канал
При инициализации канала можно использовать второй параметр:
c := make(chan int, 1)
и мы получим буферизированный канал с ёмкостью1. Обычно каналы работают синхронно - каждая из сторон ждёт, когда другая сможет получить или передать сообщение. Но буферизованный канал работает асинхронно — получение или отправка сообщения не заставляют стороны останавливаться. Но канал теряет пропускную способность, когда он занят, в данном случае, если мы отправим в канал 1 сообщение, то мы не сможем отправить туда ещё одно до тех пор, пока первое не будет получено.
Задачи
Как задать направление канала?
Напишите собственную функцию
Sleep, используяtime.AfterЧто такое буферизированный канал? Как создать такой канал с ёмкостью в 20 сообщений?