Лучший опыт

Дирижируйте горутинами с помощью каналов.

Go получил известность во многом благодаря своему чистому и эффективному подходу к параллельному выполнению. С применением горутин можно добиться огромного повышения эффективности, выполняя несколько потоков в фоновом режиме во время работы основной программы. Но как обеспечить взаимодействие горутин или наладить совместное использование ими ресурсов? Вы будете удивлены простой, но мощной природой каналов в Go. Будьте к этому гот?
Дирижируйте горутинами с помощью каналов...

Go получил известность во многом благодаря своему чистому и эффективному подходу к параллельному выполнению. С применением горутин можно добиться огромного повышения эффективности, выполняя несколько потоков в фоновом режиме во время работы основной программы. Но как обеспечить взаимодействие горутин или наладить совместное использование ими ресурсов? Вы будете удивлены простой, но мощной природой каналов в Go. Будьте к этому готовы.

Предполагается, что кое-какие знания о горутинах у вас имеются, так что давайте сразу переходить к коду!

Простой пример:

Каналы значительно упростили параллельное программирование. Тем не менее нужно с осторожностью подходить к их пониманию. В приведённой ниже программе выполняется всего четыре операции. Рассмотрим их поэтапно.

Первый этап  —  создание канала в строке 7. Определяем тип входных данных для этого канала. Это будет string (строковый тип данных). Затем в строках с 9 по 11 создаём анонимную функцию: берём строку "ping" и отправляем её в канал messages (сообщения).

При работе с каналами операция по отправке информации обозначается стрелочкой <-, направление которой показывает, куда поступает информация: в канал или из канала.

package main    import "fmt"    func main() {        messages := make(chan string)        go func() {   		messages <- "ping"   	}()        msg := <-messages      fmt.Println(msg)  }

Третий этап  —  это запрос сохранённой строки "ping" из канала сообщений в переменную msg. Последний этап  —  вывод этого сообщения на консоль.

Каналы будут ожидать информацию для отправки

Каналы в Go блокирующие. Например, у нас есть две горутины, выполняющиеся одновременно. Если одна из них запрашивает у другой передачу информации по каналу, канал блокирует выполнение до тех пор, пока другая горутина не передаст информацию в соединяющий их канал.

Продемонстрировать это можно с помощью следующего кода. sync.WaitGroup позаботится о том, чтобы горутины завершились перед выходом из основного потока программы.

Первым делом инициализируем новый канал messages в строке 13.

package main    import (  	"fmt"  	"time"  	"sync"  )    func main() {  	var wg sync.WaitGroup //  инициализируем счётчик  	wg.Add(2) // в ожидании двух горутин    	messages := make(chan string)    	go func() {   		fmt.Println("Starting first anonymous function...")  		fmt.Println("Echo!")    		time.Sleep(time.Millisecond * 1000)    		messages <- "Echo!"   		fmt.Println("Exiting first anonymous function")  		wg.Done()  	}()    	go func() {  		fmt.Println("Starting second anonymous function...")    		msg := <- messages    		fmt.Printf("I hear an %s\n", msg)  		fmt.Println("Exiting second anonymous function")  		wg.Done()  	}()    	wg.Wait() // блокируем, пока все горутины не завершат выполнение  }

После этого первая анонимная функция на секунду засыпает, а затем перед выходом отправляет строку "Echo" в канал messages. В следующей анонимной функции в строке 26 запрашиваем входные данные из канала messages в переменную msg. Это блокирующая операция: в ней эта функция не сможет продолжать работу, пока мы не получим выходные данные из messages.

Если бы мы запустили приведённую выше программу, то получили бы следующий результат:

Starting second anonymous function...  Starting first anonymous function...  Echo!  Exiting first anonymous function  I hear an Echo!  Exiting second anonymous function

Опять же обратите внимание, что горутины недетерминированы. Нельзя сказать, в каком порядке функции будут запускаться или останавливаться. Зато известно, что каналы будут блокировать выполнение в ожидании информации для отправки. Между первой Echo! и заключительной инструкцией на вывод I hear an Echo! есть секундная задержка, потому что принимающий канал ждёт вывод.

Как предотвратить взаимоблокировку в каналах

Итак, каналы блокируют выполнение во время ожидания ресурса. Но что произойдет, если этот ресурс так и не будет отправлен? Эта ситуация приведёт ко взаимоблокировке, которую Go обнаружит не во время компиляции, а уже только во время выполнения. Рассмотрим пример:

package main    import (  	"fmt"  	"time"  	"sync"  )    func main() {  	var wg sync.WaitGroup //  инициализируем счётчик  	wg.Add(2) // в ожидании двух горутин    	messages := make(chan int)    	go func() {   		for i := 0; i < 3; i++ {  			messages <- i  			time.Sleep(time.Millisecond * 500)  		}    		close(messages) // закрываем канал по завершении  		wg.Done()  	}()    	go func() {  		for {  			msg, open := <- messages    			if !open {  				break  			}  			fmt.Println(msg)  		}    		wg.Done()  	}()    	wg.Wait() // блокируем, пока все горутины не завершат выполнение  }

В первой анонимной функции, которая выполняется как горутина, мы трижды добавляем к каналу сообщений любое значение i. Когда мы выходим из этого цикла, то закрываем канал, сигнализируя о том, что с добавлением закончили. Таким образом взаимоблокировка предотвращается.

Во второй горутине из канала messages мы получаем msg и open. Параметр open  —  это логическое значение, которое сигнализирует о том, закрыт канал или нет. Если канал неоткрыт !open, то выходим из цикла и горутины с помощью wg.Done(). Просто, но эффективно!

Можно также воспользоваться преимуществами синтаксиса Go, упростив второй цикл for в строке 16. Тогда даже не придётся проверять, закрыт канал или нет: об этом позаботится Go.

for msg := range messages {      fmt.Println(msg)  }

Кроме того, можно добавить каналам пропускную способность:

package main    import (      "fmt"  )    func main() {      messages := make(chan string, 2)      messages <- "Yer"      messages <- "a"        msg := <- messages      fmt.Println(msg)        msg = <- messages      fmt.Println(msg)        messages <- "Wizard"        msg = <- messages      fmt.Println(msg)  }

В строке 8 мы создаём канал с пропускной способностью 2, а отправлять и получать можем даже три строки, потому что канал работает как очередь. Пока в канале не более 2 сообщений, мы не выходим за пределы пропускной способности и не получаем ошибку.

А что, если имеются каналы, блокирующие друг друга в условиях ограничения по времени? В Go можно использовать выражение select для выполнения любого готового канала:

package main    import (      "fmt"      "time"  )    func main() {      c1 := make(chan string)      c2 := make(chan string)        go func() {          for {              c1 <- "Channel 1 every second"              time.Sleep(time.Millisecond * 1000)          }      }()        go func() {          for {              c1 <- "Channel 2 every 2 seconds"              time.Sleep(time.Millisecond * 2000)          }      }()        for {          select {          case msg1 := <- c1:              fmt.Println(msg1)          case msg2 := <- c2:              fmt.Println(msg2)          }      }  }

Здесь у нас два канала, которые бесконечно добавляются в двух горутинах. Первая горутина добавляется к каналу c1 каждую секунду. Вторая добавляется к каналу c2 каждые две секунды. Если просто попытаться вывести информацию с этих каналов, канал c2 заблокирует выполнение и мы будем получать оба вывода каждые две секунды.

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

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

Надеюсь, статья была полезной и вы узнали что-то новое. Спасибо за внимание!