- PVSM.RU - https://www.pvsm.ru -
Перевод познавательной статьи "Golang: channels implementation" [1] о том, как устроены каналы в Go.
Go становится всё популярнее и популярнее, и одна из причин этого — великолепная поддержка конкурентного программирования. Каналы и горутины сильно упрощают разработку конкурентных программ. Есть несколько хороших статей о том, как реализованы различные структуры данных в Go — к примеру, слайсы [2], карты [3], интерфейсы [4] — но про внутреннюю реализацию каналов написано довольно мало. В этой статье мы изучим, как работают каналы и как они реализованы изнутри. (Если вы никогда не использовали каналы в Go, рекомендую сначала прочитать эту статью [5].)
Давайте начнём с разбора структуры канала:
В общем случае, горутина захватывает мьютекс, когда совершает какое-либо действие с каналом, кроме случаев lock-free проверок при неблокирующих вызовах (я объясню это подробнее чуть ниже). Closed — это флаг, который устанавливается в 1, если канал закрыт, и в 0, если не закрыт. Эти поля далее будут исключены из общей картины, для большей ясности.
Канал может быть синхронным (небуферизированным) или асинхронным (буферезированным). Давайте вначале посмотрим, как работают синхронные каналы.
Допустим, у нас есть следующий код:
package main
func main() {
ch := make(chan bool)
go func() {
ch <- true
}()
<-ch
}
Вначале создается новый канал и он выглядит вот так:
Go не выделяет буфер для синхронных каналов, поэтому указатель на буфер равен nil и dataqsiz
равен нулю. В приведённом коде нет гарантии, что случится первее — чтение из канала или запись, поэтому допустим, что первым действием будет чтение из канала (обратный пример, когда вначале идёт запись, будет рассмотрена ниже в примере с буферизированным каналами). Вначале, текущая горутина произведёт некоторые проверки, такие как: закрыт ли канал, буферизирован он или нет, содержит ли гоуртины в send-очереди. В нашем примере у канала нет ни буфера, ни ожидающих отправки горутин, поэтому горутина добавит сама себя в recvq
и заблокируется. На этом шаге наш канал будет выглядеть следующим образом:
Теперь у нас осталась только одна работающая горутина, которая пытается записать данные в канал. Все проверки повторяются снова, и когда горутина проверяет recvq
очередь, она находит ожидающую чтение горутину, удаляет её из очереди, записывает данные в её стек и снимает блокировку. Это единственное место во всём рантайме Go, когда одна горутина пишет напрямую в стек другой горутины. После этого шага, канал выглядит точно так же, как сразу после инициализации. Обе горутины завершаются и программа выходит.
Так устроены синхронные каналы. Сейчас же, давайте посмотрим на буферизированные каналы.
Рассмотрим следующий пример:
package main
func main() {
ch := make(chan bool, 1)
ch <- true
go func() {
<-ch
}()
ch <- true
}
Опять же, порядок исполнения неизвестен, пример с первой читающей горутиной мы разобрали выше, поэтому сейчас допустим, что два значения белы записаны в канал, и после этого один из элементов вычитан. И первым шагом идёт создание канала, который будет выглядеть вот так:
Разница в сравнении с синхронным каналом в том, что тут Go выделяет буфер и устанавливает значение dataqsiz
в единицу.
Следующим шагом будет отправка первого значения в канал. Чтобы сделать это, горутина сначала производит несколько проверок: пуста ли очередь recvq
, пуст ли буфер, достаточно ли места в буфере.
В нашем случае в буфере достаточно места и в очереди ожидания чтения нет горутин, поэтому горутина просто записывает элемент в буфер, увеличивает значение qcount
и продолжает исполнение далее. Канал в этот момент выглядит так:
На следующем шаге, горутина main отправляет следующее значение в канал. Когда буфер полон, буферизированный канал будет вести себя точно так же, как сихронный (буферизированный) канал, тоесть горутина добавит себя в очередь ожидания и заблокируется, в результате чего, канал будет выглядеть следующим образом:
Сейчас горутина main заблокирована и Go запустил одну анонимную горутину, которая пытается прочесть значение из канала. И вот тут начинается хитрая часть. Go гарантирует, что канал работает по принципу FIFO очереди (спецификация [6]), но горутина не может просто взять значение из буфера и продолжить исполнение. В этом случае горутина main заблокируется навсегда. Для решения этой ситуации, текущая горутина читает данные из буфера, затем добавляет значение из заблокированной горутины в буфер, разблокирует ожидающую горутину и удаляет её из очереди ожидания. (В случае же, если нет ожидающих горутину, она просто читает первое значение из буфера)
Но постойте, Go же ещё поддерживает select с дефолтным поведением, и если канал заблокирован, как горутина сможет обработать default? Хороший вопрос, давайте быстро посмотрим на приватное API каналов. Когда вы запускаете следующий кусок кода:
select {
case <-ch:
foo()
default:
bar()
}
Go запускает функцию со следующей сигнатурой:
func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool)
chantype
это тип канала (например, bool в случае make(chan bool)), hchan
— указатель на структуру канала, ep
— указатель на сегмент памяти, куда должны быть записаны данные из канала, и последний, но самый интересный для нас — это аргумент block
. Если он установлен в false
, то функция будет работать в неблокирующем режиме. В этом режиме горутина проверяет буфер и очередь, возвращает true
и пишет данные в ep
или возвращает false
, если нет данных в буфере или нет отправителей в очереди. Проверки буфера и очереди реализованы как атомарные операции, и не требуют блокировки мьютекса.
Также есть функция для записи данных в очередь с аналогичной сигнатурой.
Мы разобрались как работают запись и чтение из канала, давайте теперь взглянём, что происходит при закрытии канала.
Закрытие канала это простая операция. Go проходит по всем ожидающим на чтение или запись горутинам и разблокирует их. Все получатели получают дефолтные значение переменных того типа данных канала, а все отправители паникуют.
В этой статье мы рассмотрели, как каналы реализованы и как работают. Я постарался описать их как можно проще, поэтому упустил некоторые детали. Задача статьи — предоставить базовое понимание внутреннего устройства каналов и подтолкнуть вас к чтениею исходных кодов Go, если вы хотите получить более глубокое понимание. Просто почитайте код реализации каналов [7]. Мне он кажется очень простым, хорошо документированным и довольно коротким, всего около 700 строк кода.
Исходный код [7]
Каналы в спецификации Go [6]
Каналы Go на стероидах [8]
Автор: divan0
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/go/175098
Ссылки в тексте:
[1] "Golang: channels implementation": http://dmitryvorobev.blogspot.com.es/2016/08/golang-channels-implementation.html
[2] слайсы: https://blog.golang.org/go-slices-usage-and-internals
[3] карты: https://www.goinggo.net/2013/12/macro-view-of-map-internals-in-go.html
[4] интерфейсы: http://research.swtch.com/interfaces
[5] эту статью: https://golang.org/doc/effective_go.html#channels
[6] спецификация: https://golang.org/ref/spec#Channel_types
[7] код реализации каналов: https://golang.org/src/runtime/chan.go
[8] Каналы Go на стероидах: https://docs.google.com/document/d/1yIAYmbvL3JxOKOjuCyon7JhW4cSv1wy5hC0ApeGMV9s/pub
[9] Источник: https://habrahabr.ru/post/308070/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.