Строение каналов в Golang. Часть 2.

Viktor Ugolnikov
3 min readJul 7, 2019

--

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

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

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

func main() {
ch := make(chan bool)

go func() {
time.Sleep(time.Second)
ch <- true
}()
<-ch
}

Для небуферизированных каналов Go не аллоцирует буфер, поэтому в структуре hchan параметр buf будет пустым, как и размерность буфера(dataqsiz):

Когда произойдет попытка чтения из канала ch основной горутиной (main), внутреняя структура канала примет следующий вид:

При этом горутина main заблокируется рантаймом языка golang — выполнение main приостановится до тех пор, пока другая горутина не осуществит запись в канал ch.

Далее, запущенная горутина

go func() {
time.Sleep(time.Second)
ch <- true
}()

при записи в канал ch выполняет следующие шаги:

  • проверяет структуру recv;
  • находит в ней ожидающую горутину (в нашем случае, это функция main);
  • удаляет горутину main из списка recv;
  • пишет напрямую в стек функции main(с целью оптимизации памяти — экономия одной операции копирования)

После этого канал ch имеет состояние такое же, как и сразу после инициализации канала. Обе горутины завершают свое выполнение, и вместе с ними завершает свое выполнение программа.

Закрытие канала

За закрытие канала отвечает функция closechan из файла chan.go.

Набор действий, выполняемых при закрытии канала:

  • выполняется проверка, что канал инициализирован(panic в случае, если канал не инициализирован);
  • захватывается блокировка мьютекса;
  • выполняется проверка, что канал не закрыт (panic в случае, если канал уже закрыт);
  • значение поля closed канала (в структуре hchan) выставляется в true;
  • все структуры, ожидающие чтения, получают default value в зависимости от типа данных в канале;
  • все структуры, ожидающие записи, получают panic;
  • мьютекс канала разблокируется;
  • заблокированные горутины — разблокируются.

Оператор выбора select

Исходный код оператора выбора select доступен в файле select.go

Каждый элемент внутри оператора select представлен структурой scase:

type scase struct {
c *hchan // Канал
elem unsafe.Pointer // Ссылка на данные
kind uint16 // Тип операции (получение, запись или default)
// ...

}

Последовательность действий при обработке оператора select следующая:

  1. Элементы(scase) внутри select сортируются в случайном порядке(перемешиваются):
for i := 1; i < ncases; i++ {
j := fastrandn(uint32(i + 1))
pollorder[i] = pollorder[j]
pollorder[j] = uint16(i)
}

2. Каждый из каналов блокируется мьютексом.

3. Происходит последовательная попытка взаимодействия (запись или чтение) с каналами, перечисленными внутри оператора select. При наличии секции default, чтение и запись происходят в неблокирующем режиме(об этом далее).

4. В случае, если ни один из каналов недоступен для взаимодействия, и секция default отсутствует, то текущая горутина переходит в состояние waiting до тех пор, пока какой то из каналов не станет доступен.

5. С каналов снимается блокировка мьютексом.

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

Ответ кроется в функциях chanrecv и chansend, которые отвечают непосредственно за чтение из канала и отправку данных в канал:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { ... }func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { ... }

Здесь нас интересует параметр block. При наличии секции default в операторе выбора select, функции chansend и chanrecv вызываются с параметром block равным false, в итоге функции осуществляют быстрый возврат в случае, если записать в канал или прочитать из канала без ожидания не удалось:

if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}

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

На этом все. Теперь вы понимаете не только как использовать каналы, но и их внутренюю реализацию, а значит сможете использовать их более эффективно.

--

--