Каналы крайне полезная штука и используются для передачи данных между рутинами, а также для синхронизации. Альтернативный способ — локи. Но каналы — идейно правильный. 😉
Каналы могут быть синхронными и асинхронными, двунаправленными и однонаправленными.
Синхронный канал приводит к остановке кода при чтении, пока в канал не отправят данные, и при записи, пока из канала не прочтут уже имеющееся в нём.
1 2 3 |
var names chan string = make(chan string) names <- "Иван Васильевич" fmt.Println("Здравствуйте, ", <-names) |
Асинхронный канал — канал с буфером, который блокирует код рутины только если весь его буфер занят. Отличие от синхронного в том, что при создании мы указываем размер буфера.
1 |
var names chan string = make(chan string, 10) |
То есть, синхронный канал — это канал с размером буфера = 0.
Разберём подробнее принцип работы.
а) программа встречает инструкцию чтения из канала. Если в канал ещё никто ничего не отправлял или всё отправленное из него уже забрали, программа блокируется.
б) программа встречает инструкцию записи в канал. Если буфер канала исчерпан, программа блокируется.
1 2 3 4 5 6 |
chan1 := make(chan bool) chan1 <- 1 // блокировка и как результат дедлок // мы заблокированы и нет никакой рутины, которая могла бы // записать в канал и разблокировать нас x <- chan1 // тоже блокировка с дедлоком, т.к. нет рутины, // которая могла бы нас разблокировать |
Почему тут происходят блокировки в обоих случаях? Потому что канал синхронный. Буфер нулевой — некуда временно сохранить значение. В зависимости от наших целей, можно либо сделать канал с ненулевым буфером, либо (а обычно «и») запустить рутины, которые будут читать/отправлять данные.
Как вы понимаете, в примере выше буферизированный канал нас не спасёт, если мы сразу же в цикле в него попробуем отправить данных больше, чем размер буфера. Мы просто отправим столько, сколько вместится в буфер и затем дедлок. Поначалу это кажется не очень понятным, но когда будете писать параллельный код, довольно быстро набьёте пару шишек и всё усвоите. 😉
В примере выше канал двунаправленный. В него можно отправить данные и из него можно прочесть данные.
1 2 3 4 5 |
var readChan <-chan string // отсюда можно будет только читать var sendChan chan<- string // сюда можно будет только отправлять readChan = names readChan <- "bla-bla-bla" // не компилируется name <- sendChan // не компилируется |
Простенький пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package main import ( "fmt" "time" ) func main() { persons := []string{ "Антон", "Борис", "Виктор", "Геннадий", "Дмитрий", "Егор", "Ждан", "Захар", "Игорь", "Константин", } greetings := make(chan string, 3) checkins := make(chan int) go func() { cnt := 0 for person := range greetings { fmt.Printf("%s checked in\n", person) cnt++ checkins <- cnt } close(checkins) }() go func() { for checkins := range checkins { fmt.Printf("Checkins: %d\n", checkins) } }() i := 1 for _, person := range persons { fmt.Printf("Hi! I'm %s (%d)\n", person, i) greetings <- person i++ } close(greetings) time.Sleep(time.Second) } |
Пример вывода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
Hi! I'm Антон (1) Hi! I'm Борис (2) Hi! I'm Виктор (3) Hi! I'm Геннадий (4) Hi! I'm Дмитрий (5) Антон checked in Борис checked in Hi! I'm Егор (6) Checkins: 1 Checkins: 2 Виктор checked in Геннадий checked in Hi! I'm Ждан (7) Hi! I'm Захар (8) Checkins: 3 Checkins: 4 Hi! I'm Игорь (9) Дмитрий checked in Егор checked in Hi! I'm Константин (10) Checkins: 5 Checkins: 6 Ждан checked in Захар checked in Checkins: 7 Checkins: 8 Игорь checked in Константин checked in Checkins: 9 Checkins: 10 |
Сон в конце добавлен только для того, чтобы упростить код и наверняка увидеть весь вывод. Правильно было не спать в надежде, что рутины все успеют завершиться, а непосредственно ждать их завершения. Для этого можно было бы использовать sync.WaitGroup или те же каналы (ещё один создать, в который рутины по завершении бы писали; или даже два 😉 ).
В целом пример крайне прост (и крайне надуман). Мы создаём два канала. Один для отправки имён в рутину, которая сообщает в консоль, что человек зачекинился. Второй канал используется для подсчёта чекинов. Естественно, необходимости в нём нет, так можно чуть лучше представить общение между рутинами. Не для всех очевидно, что main-функция программы — это тоже, по сути, рутина.
Итак, создали два канала. Запустили две рутины. И стали пихать в канал имён эти самые имена. И радоваться тому, как всё завораживающе работает.
Да, кстати. Не забывайте выносить мусор закрывать каналы. 😉
for … range по каналу бегает до тех пор, пока канал открыт.
А что делать, если у нас не один канал и мы желаем одновременно их проверять на доступность данных?
В этом случае нам помогает select.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func DoSome(in <-chan int, out chan<- int) { i := 0 for { select { case x := <-in: // приняли из входного канала case out <- i: // смогли отправить в выходной канал i++ } if i > 100 { break } } } |
Первая функция иллюстрирует возможность работы одновременно с несколькими каналами. Если оба кейса доступны для выполнения, какой будет выполнен, выбирается случайно.
Спасибо, одно из самых понятных объяснений по каналам, которые читал)