Как записать значение в канал в go
Перейти к содержимому

Как записать значение в канал в go

  • автор:

Go
каналы

Канал содержит значения заданного типа. Значения могут быть записаны на канал и считаны из него, и они распространяются внутри канала в порядке «первым в первом порядке». Существует различие между буферными каналами, которые могут содержать несколько сообщений и небуферизованные каналы, что не может. Каналы обычно используются для связи между goroutines, но также полезны в других обстоятельствах.

Синтаксис

  • make (chan int) // создание небуферизованного канала
  • make (chan int, 5) // создать буферный канал с емкостью 5
  • close (ch) // закрывает канал "ch"
  • ch <- 1 // записываем значение 1 в канал "ch"
  • val: = <-ch // считывает значение из канала "ch"
  • val, ok: = <-ch // альтернативный синтаксис; ok — это bool, указывающий, закрыт ли канал

замечания

Канал, содержащий пустую структуру struct make(chan struct<>) является ясным сообщением пользователю о том, что никакая информация не передается по каналу и что она используется исключительно для синхронизации.

Что касается небуферизованных каналов, запись канала будет блокироваться до тех пор, пока соответствующее прочтение не поступит от другого горутина. То же самое верно для блокировки чтения канала во время ожидания записи.

Использование диапазона

При чтении нескольких значений из канала использование range — это общий шаблон:

Таймауты

Каналы часто используются для реализации тайм-аутов.

Координация горти

Представьте себе горутин с двухэтапным процессом, где основной поток должен выполнять некоторую работу между каждым шагом:

Буферизованный и небуферизованный

Блокирование и разблокирование каналов

По умолчанию связь по каналам синхронизируется; когда вы отправляете какое-то значение, должен быть приемник. В противном случае вы получите fatal error: all goroutines are asleep — deadlock! следующее:

Go: Каналы

В Go существует постулат: "Do not communicate by sharing memory; instead, share memory by communicating" (Не общайтесь разделением памяти. Разделяйте память через общение). Для безопасной коммуникации между горутинами используется специальный тип данных: chan (канал).

Как слайсы и мапы, каналы инициализируются с помощью функции make :

Чтение и запись в канал происходит через конструкцию <- . Стрелка ставится перед, если канал читается и после, если записывается:

Чтение из канала блокирует текущую горутину, пока не вернется значение:

Запись в канал так же блокирует текущую горутину, пока кто-то не прочтет значение.

Каналы также можно использовать для задачи из прошлого урока:

Иногда требуется запустить обработчика в отдельной горутине, который будет выполнять работу на протяжении всего цикла жизни программы. С помощью конструкции for range можно читать из канала до того момента, пока он не будет закрыт:

Задание

Реализуйте функцию-воркера SumWorker(numsCh chan []int, sumCh chan int) , которая суммирует переданные числа из канала numsCh и передает результат в канал sumCh:

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

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

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Это нормально ��, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

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

Под капотом Golang — как работают каналы. Часть 1.

Поразительное удобство написания конкурентного кода в эпоху высоконагруженных приложений и широкой распространнености микросервисов позволило приобрести языку Golang столь высокую популярность. И для написания конкурентного кода в Golang можно выделить два основных примитива:

  • Горутины, предназначенные для выполнения задач независимо друг от друга, в конкурентной или параллельной манере;
  • Каналы, о которых и пойдет речь в этой статье, предназначенные для обмена сообщениями и синхронизации между горутинами.

Я думаю каждый, кто уже знаком с языком Golang, так или иначе использовал каналы. И о том, как использовать каналы, есть предостаточно информации, так что сегодня мы заглянем “под капот”, и рассмотрим, как они работают изнутри. Ведь зная, как та или иная технология работает изнутри, мы можем использовать эту технологию более эффективно.

Структура каналов

Исходный код каналов досутпен на github в файле chan.go, и центральной структурой данных для канала является hchan:

Давайте теперь создадим канал, запишем в него два значения, и проанализируем его внутренюю структуру(структуру hchan):

  • qcount определяет количество элементов в буфере(мы видим 1 т.к. записали одно значение в наш канал)
  • dataqsiz определяет размерность буфера для буферизированного канала, в нашем случае это 2;
  • buf — определяет буфер с данными, записанными в канал, реализованный с помощью структуры данных “кольцевой буфер”
  • elemsize — размер одного элемента в канале, в нашем случае это 8 байт
  • closed — определяет закрыт или открыт канал в данный момент;
  • elemype — содержит указатель на тип данных в канале;
  • sendx и recvx содержат индексы (смещения) в буфере, по которым должна производиться запись и чтение из буфера соответственно;
  • sendq и recvq — односвязные списки, содержащие заблокированные горутины, ожидающие чтения или записи;
  • lock — мьютекс, используемый для операций, изменяющих состояние канала

Создание каналов

Создание канала происходит при помощи функции makechan, которая описана в том же исходном файле chan.go:

Функция makechan выделяет память под структуру hchan в куче, инициализирует эту структуру, и возвращает указатель.

И несмотря на то, что в go все передается по значению, передавать канал по ссылке бессмысленно, потому что под капотом канал — это и есть указатель.

Запись и чтение

При записи в канал, происходит следующее:

  • структура приобретает блокировку мьютексом;
  • в буфер копируется значение, которое мы положили в канал;
  • структура освобождается от блокировки мьютексом;

И стоит обратить внимание на то, что при добавлении в буфер, элемент копируется.

При чтении из канала, последовательность действий аналогичная:

  • структура приобретает блокировку мьютексом;
  • из слота буфера копируется значение с последующим удалением;
  • структура освобождается от блокировки мьютексом.

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

Ожидание записи

Почему при заполнении буфера, последующая попытка записи в канал приведет к приостановке потока выполнения пишущей горутины?

Поскольку создание и управление горутинами — это зона ответственности runtime самого Go, то runtime может без труда приостанавливать выполнение одной горутины, и запускать выполнение другой.

Итак, когда горутина осуществляет попытку записи в канал, буфер которого полон, рантайм переводит эту горутину в состояние “waiting”.

Предположим, что теперь запись возможна, так как другая горутина прочитала данные из канала. Как рантайм узнает, что нужно продолжить выполнение нашей горутины?

Для дальнейшего понимания процесса ожидания, нам нужно рассмотреть вспомогательную структуру sudog:

Мы видим, что канал содержит в себе ссылку на ожидающую горутину, представленную структурой sudog. Эта структура помещается в односвязный список waitq. И когда буфер становится доступным для заполнения, происходит следующее:

  • очередная структура, представляющая ожидающую горутину sudog извлекается из списка waitq;
  • данные из поля elem добавляются в буфер канала;
  • горутина из sudog переходит из состояния “waiting” в состояние “runnable” (готова к выполнению).

Ожидание чтения

Что происходит при попытке чтения из канала, буфер которого пуст?

  • Для горутины, осуществляющей попытку чтения из канала, создается структура sudog;
  • Горутина переходит в состояние “waiting”
  • Односвязный список recv пополняется структурой sudog;

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

Когда другая горутина записывает данные в канал, последовательность действий отличается:

  • Данные не пишутся в буфер, а пишутся напрямую в стек читающей горутины;
  • Читающая горутина переходит из состояния “waiting” в состояние “runnable” (готова к выполнению).

Такой подход позволяет сократить накладные расходы на блокировку буфера мьютексом. Сам буфер также остается нетронутым, и не проиходит дополнительных операций копирования.

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

Во второй части статьи мы рассмотрим небуферизированные каналы, функцию close(), а также оператор выбора select.

Как записать значение в канал в go

Каналы (channels) представляют инструменты коммуникации между горутинами. Для определения канала применяется ключевое слово chan :

После слова chan указывается тип данных, которые будут передаться с помощью канала. Например:

Здесь переменная intCh представляет канал, который передает данные типа int.

Для передачи данных в канал или, наоборот, из канала применяется операция <- (направленная влево стрелка). Например, передача данных в канал:

В данном случае в канал посылается число 5. Получение данных из канала в переменную:

Если ранее в канал было отправлено число 5, то при выполнении операции <- intCh мы можем получить это число в переменную val.

Стоит учитывать, что мы можем отправить в канал и получить из канала данные только того типа, который представляет канал. Так, в примере с каналом intCh это данные типа int.

Как правило, отправителем данных является одна горутина, а получателем — другая горутина.

При простом определении переменной канала она имеет значение nil , то есть по сути канал неинициализирован. Для инициализации применяется функция make() . В зависимости от определения емкости канала он может быть буферизированным или небуферизированным.

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

Для создания небуферизированного канала вызывается функция make() без указания емкости канала:

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

Горутина-отправитель может отправлять данные только в пустой канал. Горутина-отправитель блокируется до тех пор, пока данные из канала не будут получены. Например:

Через небуферизированный канал intCh горутина, представленная анонимной функцией, передает число 5:

А функция main получает это число:

Общий ход выполнения программы выглядит следующим образом:

Запускается функция main. Она создает канал intCh и запускает горутину в виде анонимной функции.

Функция main продолжает выполняться и блокируется на строке fmt.Println(<-intCh) , пока не будут получены данные.

Параллельно выполняется запущенная горутина в виде анонимной функции. В конце своего выполнения она отправляет даные через канал: intCh <- 5 . Горутина блокируется, пока функция main не получит данные.

Функция main получает отправленные данные, деблокируется и продолжает свою работу.

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

Обратите внимание, как определяется параметр, который представляет канал данных типа int: ch chan int . Консольный вывод данной программы:

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

Стоит отметить, что отправителем данных должна быть отдельно запускаемая горутина. Например, если мы определим отправление и получение данных через канал в самой функции main, то мы столкнемся с взаимоблокировкой:

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

Буферизированные каналы также создаются с помощью функции make() , только в качестве второго аргумента в функцию передается емкость канала. Если канал пуст, то получатель ждет, пока в канале появится хотя бы один элемент.

При отправке данных горутина-отправитель ожидает, пока в канале не освободится место для еще одного элемента и отправляет элемент, только тогда, когда в канале освобождается для него место.

В данном случае отправителем и получателем данных является функция main. В ней создается канал из трех элементов, и последовательно отправляются три значения типа int.

В то же время в данном случае должно быть соответствие между количеством отправляемых и получаемых данных. Если в функции main будет одновременно отправлено значений больще, чем вмещает канал, то функция заблокируется:

С помощью встроенных функций cap() и len() можно получить соответственно емкость и количество элементов в канале:

Однонаправленные каналы

В Go можно определить канал, как доступный только для отправки данных или только для получения данных.

Определение канала только для отправки данных:

Определение канала только для получения данных:

Здесь второй параметр функции factorial определен как канал, доступный только для отправки данных: ch chan<- int . Соответственно внутри функции factorial мы можем только отправлять данные в канал, но не получать их.

Возвращение канала

Канал может быть возвращаемым значением функции. Однако следует внимательно подходить к операциям записи и чтения в возвращаемом канале. Например:

Функция createChan возвращает канал. Однако при выполнении операции ch <- n мы столкнемся с блокировкой, поскольку происходит ожидание получения данных из канала. Поэтому следующее выражение return ch не будет выполняться.

И если все таки необходимо определить функцию, которая возвращает канал, то все операции чтения-записи в канал следует вынести в отдельную горутину:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *