Перевод познавательной статьи "Golang: channels implementation" о том, как устроены каналы в Go. Go становится всё популярнее и популярнее, и одна из причин этого — великолепная поддержка конкурентного программирования. Каналы и горутины сильно упрощают разработку конкурентных программ. Есть несколько хороших статей о том, как реализованы различные структуры данных в Go — к примеру, слайсы, карты, интерфейсы — но про внутреннюю реализацию каналов написано довольно мало. В этой статье мы изучим, как работают каналы и как они реализованы изнутри. (Если вы никогда не использовали каналы в Go, рекомендую сначала прочитать эту статью.) Давайте начнём с разбора структуры канала: В общем случае, горутина захватывает мьютекс, когда совершает какое-либо действие с каналом, кроме случаев lock-free проверок при неблокирующих вызовах (я объясню это подробнее чуть ниже). Closed — это флаг, который устанавливается в 1, если канал закрыт, и в 0, если не закрыт. Эти поля далее будут исключены из общей картины, для большей ясности. Канал может быть синхронным (небуферизированным) или асинхронным (буферезированным). Давайте вначале посмотрим, как работают синхронные каналы. Допустим, у нас есть следующий код: Вначале создается новый канал и он выглядит вот так: Go не выделяет буфер для синхронных каналов, поэтому указатель на буфер равен nil и dataqsiz равен нулю. В приведённом коде нет гарантии, что случится первее — чтение из канала или запись, поэтому допустим, что первым действием будет чтение из канала (обратный пример, когда вначале идёт запись, будет рассмотрена ниже в примере с буферизированным каналами). Вначале, текущая горутина произведёт некоторые проверки, такие как: закрыт ли канал, буферизирован он или нет, содержит ли гоуртины в send-очереди. В нашем примере у канала нет ни буфера, ни ожидающих отправки горутин, поэтому горутина добавит сама себя в recvq и заблокируется. На этом шаге наш канал будет выглядеть следующим образом: Теперь у нас осталась только одна работающая горутина, которая пытается записать данные в канал. Все проверки повторяются снова, и когда горутина проверяет recvq очередь, она находит ожидающую чтение горутину, удаляет её из очереди, записывает данные в её стек и снимает блокировку. Это единственное место во всём рантайме Go, когда одна горутина пишет напрямую в стек другой горутины. После этого шага, канал выглядит точно так же, как сразу после инициализации. Обе горутины завершаются и программа выходит. Так устроены синхронные каналы. Сейчас же, давайте посмотрим на буферизированные каналы. Рассмотрим следующий пример: Опять же, порядок исполнения неизвестен, пример с первой читающей горутиной мы разобрали выше, поэтому сейчас допустим, что два значения были записаны в канал, и после этого один из элементов вычитан. И первым шагом идёт создание канала, который будет выглядеть вот так: Разница в сравнении с синхронным каналом в том, что тут Go выделяет буфер и устанавливает значение dataqsiz в единицу. Следующим шагом будет отправка первого значения в канал. Чтобы сделать это, горутина сначала производит несколько проверок: пуста ли очередь recvq, пуст ли буфер, достаточно ли места в буфере. В нашем случае в буфере достаточно места и в очереди ожидания чтения нет горутин, поэтому горутина просто записывает элемент в буфер, увеличивает значение qcount и продолжает исполнение далее. Канал в этот момент выглядит так: На следующем шаге, горутина main отправляет следующее значение в канал. Когда буфер полон, буферизированный канал будет вести себя точно так же, как синхронный (небуферизированный) канал, тоесть горутина добавит себя в очередь ожидания и заблокируется, в результате чего, канал будет выглядеть следующим образом: Сейчас горутина main заблокирована и Go запустил одну анонимную горутину, которая пытается прочесть значение из канала. И вот тут начинается хитрая часть. Go гарантирует, что канал работает по принципу FIFO очереди (спецификация), но горутина не может просто взять значение из буфера и продолжить исполнение. В этом случае горутина main заблокируется навсегда. Для решения этой ситуации, текущая горутина читает данные из буфера, затем добавляет значение из заблокированной горутины в буфер, разблокирует ожидающую горутину и удаляет её из очереди ожидания. (В случае же, если нет ожидающих горутину, она просто читает первое значение из буфера) Но постойте, Go же ещё поддерживает select с дефолтным поведением, и если канал заблокирован, как горутина сможет обработать default? Хороший вопрос, давайте быстро посмотрим на приватное API каналов. Когда вы запускаете следующий кусок кода: Go запускает функцию со следующей сигнатурой: chantype это тип канала (например, bool в случае make(chan bool)), hchan — указатель на структуру канала, ep — указатель на сегмент памяти, куда должны быть записаны данные из канала, и последний, но самый интересный для нас — это аргумент block. Если он установлен в false, то функция будет работать в неблокирующем режиме. В этом режиме горутина проверяет буфер и очередь, возвращает true и пишет данные в ep или возвращает false, если нет данных в буфере или нет отправителей в очереди. Проверки буфера и очереди реализованы как атомарные операции, и не требуют блокировки мьютекса. Также есть функция для записи данных в очередь с аналогичной сигнатурой. Мы разобрались как работают запись и чтение из канала, давайте теперь взглянём, что происходит при закрытии канала. Закрытие канала это простая операция. Go проходит по всем ожидающим на чтение или запись горутинам и разблокирует их. Все получатели получают дефолтные значение переменных того типа данных канала, а все отправители паникуют. В этой статье мы рассмотрели, как каналы реализованы и как работают. Я постарался описать их как можно проще, поэтому упустил некоторые детали. Задача статьи — предоставить базовое понимание внутреннего устройства каналов и подтолкнуть вас к чтениею исходных кодов Go, если вы хотите получить более глубокое понимание. Просто почитайте код реализации каналов. Мне он кажется очень простым, хорошо документированным и довольно коротким, всего около 700 строк кода. Исходный код
Каналы в спецификации Go
Каналы Go на стероидах