Строение каналов в Golang. Часть 2.
В первой части статьи мы рассмотрели общее устройство каналов, процесс их создания, а также запись и чтение для буферизированных каналов.
В этой части мы рассмотрим небуферизированные каналы, функцию 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 следующая:
- Элементы(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
}
В качестве оптимизации, при таком подходе канал не блокируется мьютексом, вместо этого используются атомарные операции.
На этом все. Теперь вы понимаете не только как использовать каналы, но и их внутренюю реализацию, а значит сможете использовать их более эффективно.