Многие разработчики C или Unix слышатselect
Подумайте обо всех системных вызовах, и когда вы говорите о модели ввода-вывода, вы в конечном итоге упомяну об этом.select
,poll
а такжеepoll
Модель мультиплексирования ввода-вывода, построенная с помощью таких функций, как язык Go, который мы представим в этом разделе.select
Ключевое слово на самом деле такое же, как в языке Cselect
Есть относительно похожие функции.
Этот раздел познакомит вас с языком Go.select
принципы реализации, в том числеselect
структура и общие проблемы, различные оптимизации во время компиляции и выполнения во время выполнения.
Обзор
на языке Сselect
Ключевое слово может одновременно отслеживать доступное для чтения или записи состояние нескольких файловых дескрипторов. Прежде чем состояние файлового дескриптора изменится,select
Всегда будет блокировать текущий поток на языке Goselect
Это ключевое слово чем-то похоже на ключевое слово в языке C, за исключением того, что оно позволяет горутине ожидать одновременной готовности нескольких каналов.
select
этоswitch
очень похожая структура управления, сswitch
разница в том,select
Хотя есть несколькоcase
, но этиcase
Выражения в должны быть такими же, какChannelотносится к работе канала, то есть операциям чтения и записи канала.Следующая функция показывает
Чтение данных в канале и отправка данных в каналselect
структура:
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
этоselect
Структура управления будет ждатьc <- x
или<-quit
Возврат любого из двух выражений, в зависимости от того, какое из них будет возвращено, будет выполнено немедленно.case
код в , но еслиselect
два изcase
В то же время срабатывает, случайно выберутcase
воплощать в жизнь.
структура
select
На самом деле в исходном коде языка Go нет структурного представления, ноselect
в структуре управленияcase
но используетсяscase
структура для представления:
type scase struct {
c *hchan
elem unsafe.Pointer
kind uint16
pc uintptr
releasetime int64
}
из-за не-default
изcase
Это связано с отправкой и получением данных канала, поэтому вscase
В состав также входитc
поле для храненияcase
Канал, используемый в ,elem
переменный адрес, используемый для получения или отправки данных,kind
Указывает текущийcase
Всего четыре типа:
const (
caseNil = iota
caseRecv
caseSend
caseDefault
)
Эти четыре константы представляют различные типыcase
, Я считаю, что их имена помогли нам в полной мере понять их роль, поэтому мы не будем вводить их здесь.
Феномен
Когда мы используем язык Goselect
При управлении структурой на самом деле есть две очень интересные проблемы, одна из нихselect
Можно сделать на каналенеблокирующийоперации отправки и получения, другойselect
Может случайным образом выбирать, когда несколько каналов отвечают одновременноcase
воплощать в жизнь.
неблокирующая отправка и получение
еслиselect
Структура управления содержитdefault
выражение, то этоselect
Вместо того, чтобы ждать готовности других каналов, он будет считывать или записывать данные без блокировки:
func main() {
ch := make(chan int)
select {
case i := <-ch:
println(i)
default:
println("default")
}
}
$ go run main.go
default
Когда мы запускаем приведенный выше код, он фактически не блокирует текущую горутину, а выполняет ее напрямую.default
содержание условия и возврата.
случайное исполнение
другое использованиеselect
Что происходит, так это то, что есть несколькоcase
Когда готово,select
Как выбрать проблему, мы можем просто понять через следующий код:
func main() {
ch := make(chan int)
go func() {
for range time.Tick(1 * time.Second) {
ch <- 0
}
}()
for {
select {
case <-ch:
println("case1")
case <-ch:
println("case2")
}
}
}
$ go run main.go
case1
case2
case1
case2
case2
case1
...
Из вывода приведенного выше кода мы видим, чтоselect
встреча с двумя<-ch
На самом деле, при одновременном ответе один будет выбран случайным образом.case
Выполните в нем выражение, и в этом разделе мы опишем, как работает это явление.
во время компиляции
select
Операторы преобразуются во время компиляции вOSELECT
узел, каждыйOSELECT
Узлы будут содержать сериюOCASE
узел, еслиOCASE
Все узлы пусты, что означает, что этоdefault
узел:
Собственно, на картинке выше показаноselect
структура во время компиляции, каждыйOCASE
Он содержит как условия выполнения, так и код, который выполняется после выполнения условий, которые мы представим в этом разделе.select
Оптимизации и преобразования, выполняемые оператором во время компиляции.
Во время генерации промежуточного кода компиляторselect
серединаcase
Оператор управления оптимизируется в соответствии с разницей, и этот процесс фактически происходит вwalkselectcases
В функции мы представим процесс оптимизации и результаты в четырех случаях:
-
select
Нет необходимостиcase
; -
select
здесь только одинcase
; -
select
есть дваcase
,один изcase
даdefault
утверждение; - универсальный
select
условие;
Мы разделимся в соответствии с этими четырьмя различными ситуациямиwalkselectcases
функции и представить результаты оптимизации в различных сценариях.
прямая блокировка
Первым вводится фактически простейший случай, т. е. когдаselect
структура не содержитcase
, как компилятор обрабатывает:
func walkselectcases(cases *Nodes) []*Node {
n := cases.Len()
if n == 0 {
return []*Node{mkcall("block", nil, nil)}
}
// ...
}
Этот код очень прост и понятен, он будет выглядеть так:select {}
пустой оператор, преобразованный в паруblock
вызов функции:
func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1)
}
block
Реализация функции очень проста, она будет работатьgopark
Откажитесь от права текущей горутины на использование процессора, горутина также войдет в постоянное состояние сна, и другие горутины не смогут ее разбудить, мы можем видеть, что вызовgopark
Причина ожидания, переданная в методе,waitReasonSelectNoCases
, что на самом деле говорит нам о пустомselect
оператор будет напрямую блокировать текущий
Горутины.
независимая ситуация
Если текущийselect
условие содержит только одинcase
, то для преобразования исходногоselect
Предложение переписывается какif
условное утверждение, следующее находится вselect
В случае перезаписи при получении данных от Канала:
select {
case v, ok <-ch:
// ...
}
if ch == nil {
block()
}
v, ok := <-ch
// ...
существуетwalkselectcases
функция, если только один отправленcase
, то он не будет содержатьv, ok := <- ch
Это выражение, потому что отправка данных в канал не имеет возвращаемого значения.
Мы видим, что если вselect
здесь только одинcase
, тогда, когдаcase
происходит, когда обработанный канал является нулевым указателем и неcase
изselect
Та же ситуация, что и в операторе, то есть текущая горутина напрямую приостановлена и никогда не будет разбужена.
неблокирующая операция
Перед выполнением следующей стратегии оптимизацииwalkselectcases
Функция будет сначалаcase
Все Каналы в канале преобразуются в адреса, указывающие на Каналы для следующей оптимизации и выполнения общей логики.После перезаписи будет выполнена последняя оптимизация кода, и условие срабатывания —select
содержит дваcase
, но один изdefault
, мы можем разделить его на два случая отправки и получения, чтобы представить процесс обработки.
Отправить
Первый — это процесс отправки канала, т. е.case
ВыражениеOSEND
Тип, в этом случае будет использоватьсяif/else
Код перезаписи оператора:
select {
case ch <- i:
// ...
default:
// ...
}
if selectnbsend(ch, i) {
// ...
} else {
// ...
}
Самая важная функция здесь на самом делеselectnbsend
, его основная функция — отправлять данные в Канал без блокировки, мы находимся вChannelВ разделе упоминалось, что отправка данныхchansend
функция содержитblock
параметр, этот параметр будет определять, заблокирована ли эта передача или нет:
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}
Здесь нам нужно только знать, что текущий процесс-отправитель не блокируется, и он немедленно вернется, даже если получателя нет или недостаточно места в буфере приведет к сбою.
перенимать
Так как получение данных из Channel может вернуть одно или два значения, здесь ситуация немного сложнее, чем при отправке, но процедура перезаписи и логика аналогичны:
select {
case v <- ch: // case v, received <- ch:
// ...
default:
// ...
}
if selectnbrecv(&v, ch) { // if selectnbrecv2(&v, &received, ch) {
// ...
} else {
// ...
}
Разница в количестве возвращаемых значений приведет к разным функциям в конечном использовании, две функции для неблокирующего приема сообщенийselectnbrecv
а такжеselectnbrecv2
На самом деле в самый разchanrecv
Возвращаемое значение обрабатывается немного по-другому:
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
selected, *received = chanrecv(c, elem, false)
return
}
Потому что получателю это не нужно, поэтомуselectnbrecv
просто проигнорирует возвращенное логическое значение иselectnbrecv2
передаст логическое значение обратно на верхний уровень; иchansend
Такой же,chanrecv
также обеспечиваетblock
Параметр используется для контроля того, блокируется ли этот прием или нет.
Общая ситуация
По умолчанию,select
Операторы обрабатываются на этапе компиляции следующим образом:
- положить все
case
Преобразуется в канал, содержащий такую информацию, как канал и типscase
структура; - вызов функции времени выполнения
selectgo
быть выбраннымscase
структурный индекс, если текущийscase
Это операция, которая получает данные и возвращает текущую индикациюcase
получено логическое значение; - пройти через
for
Генерация набора петельif
Заявление, в заявлении, чтобы определить, выбраны ли выcase
один содержит триcase
обычныйselect
Оператор фактически расширяется до следующей логики, где мы можем видеть три части обработки:
selv := [3]scase{}
order := [6]uint16
for i, cas := range cases {
c := scase{}
c.kind = ...
c.elem = ...
c.c = ...
}
chosen, revcOK := selectgo(selv, order, 3)
if chosen == 0 {
// ...
break
}
if chosen == 1 {
// ...
break
}
if chosen == 2 {
// ...
break
}
расширенныйselect
Фактически он состоит из трех частей: изначально массив инициализируется и преобразуется.scase
структура, использованиеselectgo
выбрать выполнениеcase
и, наконец, черезif
Определить выбранную ситуацию и выполнитьcase
Выражение в , следует отметить, что это фактически только расширенноеselect
структура управления,select
Самый важный процесс выполнения операторов — это собственно выборcase
Процесс выполнения, на котором мы сосредоточимся в следующем разделе, Runtime.
Время выполнения
мы полностью понялиselect
Далее можно представить процесс обработки во время компиляции.selectgo
Принцип реализации функции.
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
}
selectgo
— это функция, которая будет выполняться во время выполнения. Основная функция этой функции — начать сselect
Несколько в структуре управленияcase
Выберите один для выполненияcase
, за которым следует несколькоif
Условные операторы будут основаны наselectgo
Возвращаемое значение соответствующего утверждения выполняется.
инициализация
selectgo
Функция сначала выполнит некоторые необходимые операции инициализации, то есть решит обработатьcase
двух последовательностей, одна из которыхpollOrder
другойlockOrder
:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]
for i := range scases {
cas := &scases[i]
if cas.c == nil && cas.kind != caseDefault {
*cas = scase{}
}
}
for i := 1; i < ncases; i++ {
j := fastrandn(uint32(i + 1))
pollorder[i] = pollorder[j]
pollorder[j] = uint16(i)
}
// sort the cases by Hchan address to get the locking order.
// ...
sellock(scases, lockorder)
// ...
}
Порядок опроса канала - черезfastrandn
Генерируется случайным образом, что собственно и приводит к тому, что если несколько Каналов «отвечают» одновременно,select
Случайным образом выбирает его выполнение; другоеlockOrder
Он определяется по адресу Канала. Блокировка Канала в том же порядке может избежать возникновения взаимоблокировки. Последний вызовsellock
Все каналы будут заблокированы в том порядке, в котором они были созданы ранее.
цикл
когда мыselect
Оператор определяет порядок опроса и блокировки и блокирует все каналы перед входом.select
Основной цикл находит или ожидает готовности канала, цикл проходит всеcase
и найти то, что нужно возбудитьsudog
Структура, в коде этого цикла мы будем иметь дело с четырьмя разными случаямиselect
кратноеcase
:
-
caseNil
- токcase
Если он не содержит ни одного канала, он будет пропущен напрямую; -
caseRecv
- токcase
Будет получать данные от Канала;- Если текущий канал
sendq
Горутины, ожидающие этого, перейдут прямо кrecv
Сегмент кода, в котором находится тег, получает последние отправленные данные из горутины; - Если буфер текущего канала не пуст, он перейдет к
bufrecv
Получить данные из буфера по метке; - Если текущий канал был закрыт, он перейдет к
rclose
Сделайте некоторые последние штрихи очистки;
- Если текущий канал
-
caseSend
- токcase
Отправит данные в канал;- Если текущий канал был закрыт, он сразу перейдет к
rclose
сегмент кода; - Если текущий канал
recvq
Горутины, ожидающие этого, перейдут кsend
Сегмент кода отправляет данные непосредственно в канал;
- Если текущий канал был закрыт, он сразу перейдет к
-
caseDefault
- токcase
Указывает ситуацию по умолчанию, если цикл выполняется до этой ситуации, это означает, что все предыдущиеcase
не выполняются, поэтому здесь будут сразу разблокированы все каналы и выходselectgo
функция, что означает, что текущийselect
Другие операторы отправки и получения в структуре являются неблокирующими.
Соответствующий код на самом деле довольно длинный. Он не показан здесь для удобства чтения, но конкретный процесс выполнения также может быть четко показан с помощью описания и блок-схемы. Читатели, которые все еще хотят знать соответствующий код, могут проверить его.select.goдокумент.
Фактически это первый обход выполнения цикла, основная функция которого состоит в том, чтобы найти всеcase
Есть ли в Канале ситуация, которую можно обработать немедленно, есть ли данные в ожидающей горутине или буфере, пока условия соблюдены, она будет обработана немедленно.Если активный Канал не может быть найден сразу, он войдет в следующий процесс цикла в соответствии с необходимостью добавить текущую Горутин ко всем каналамsendq
илиrecvq
В очереди:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
// ...
gp = getg()
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
cas = &scases[casi]
if cas.kind == caseNil {
continue
}
c = cas.c
sg := acquireSudog()
sg.g = gp
sg.isSelect = true
sg.elem = cas.elem
sg.c = c
*nextp = sg
nextp = &sg.waitlink
switch cas.kind {
case caseRecv:
c.recvq.enqueue(sg)
case caseSend:
c.sendq.enqueue(sg)
}
}
gp.param = nil
gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
// ...
}
Создано здесьsudog
Процесс вступления в команду на самом делеChannelПроцесс практически идентичен при отправке и получении непосредственно вsudog
Структура будет объединена в связанный список и прикреплена к текущей Горутине, и будет вызываться после постановки в очередь.gopark
Функция приостанавливает текущую Goroutine и ждет, пока планировщик проснется.
Подожди покаselect
После того, как соответствующие каналы будут готовы, текущая горутина будет разбужена планировщиком и продолжит выполнение.selectgo
Оставшаяся логика в функции, которая ставится в очередь сверхуsudog
Получить данные из структуры:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
// ...
gp.selectDone = 0
sg = (*sudog)(gp.param)
gp.param = nil
casi = -1
cas = nil
sglist = gp.waiting
gp.waiting = nil
for _, casei := range lockorder {
k = &scases[casei]
if sg == sglist {
casi = int(casei)
cas = k
} else {
if k.kind == caseSend {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist)
sglist = sgnext
}
c = cas.c
if cas.kind == caseRecv {
recvOK = true
}
selunlock(scases, lockorder)
goto retc
// ...
}
в третьемlockOrder
пройти всеcase
В процессе мы сначала получим параметры, полученные Горутинойparam
, этот параметр фактически пробуждаетсяsudog
структура, мы сравним всеcase
соответствующийsudog
структура находит проснувшийсяcase
и бесплатно другие неиспользуемыеsudog
структура.
Из-за текущегоselect
Структура выбрала один из этихcase
выполнить, то остальныеcase
не используется вsudog
На самом деле он будет проигнорирован и выпущен напрямую.Чтобы не влиять на нормальное использование Канала, нам все равно нужно отбросить этиsudog
Выйти из очереди из Канала; события, отличные от этого, заставляют нас просыпатьсяsudog
Структура уже в Канале
При отправке и получении он уже вышел из команды, и нам не нужно с ним снова разбираться, код и сопутствующий анализ команды фактически находится вChannelРазделы Отправка и Получение в одном разделе.
Передается, когда мы обнаруживаем в цикле, что в буфере есть элементы или буфер не заполненgoto
Ключевое слово переходит к следующим двум сегментам кода.Процесс выполнения этих двух сегментов кода на самом деле очень прост.Они просто отправляют новые данные в канал или напрямую получают новые данные из буфера:
bufrecv:
recvOK = true
qp = chanbuf(c, c.recvx)
if cas.elem != nil {
typedmemmove(c.elemtype, cas.elem, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
selunlock(scases, lockorder)
goto retc
bufsend:
typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
selunlock(scases, lockorder)
goto retc
Здесь операции в буфере и прямые обращения к каналуchansend
а такжеchanrecv
Процесс отправки и получения почти одинаков, и он сразу перейдет кretc
поле.
Два случая прямой отправки и получения на самом деле являются двумя методами вызова среды выполнения канала.send
а такжеrecv
, эти два метода будут напрямую управлять соответствующим каналом:
recv:
recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
recvOK = true
goto retc
send:
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
goto retc
Однако при отправке или получении ситуация немного сложнее: получение данных из закрытого канала приведет к прямой очистке соответствующего контента в канале, в то время как отправка данных в закрытый канал будет напрямую удалена.panic
Вызывает сбой программы:
rclose:
selunlock(scases, lockorder)
recvOK = false
if cas.elem != nil {
typedmemclr(c.elemtype, cas.elem)
}
goto retc
sclose:
selunlock(scases, lockorder)
panic(plainError("send on closed channel"))
В целом операции отправки и получения, связанные с каналом, такие же, как и в предыдущем разделе.ChannelВ принципе реализации не так много раз разницы, только потому, чтоselect
дополнительныйdefault
ключевое слово, поэтому будет ситуация неблокирующей отправки и получения.
Суммировать
В конце этого раздела нам необходимо подвести итоги,select
Процесс выполнения и принцип реализации структуры, в первую очередь, при компиляции, язык Go будетselect
Постановка оптимизирована, нижеследующее основано наselect
Различные пути оптимизации выбираются для разных операторов в:
- пустой
select
оператор напрямую преобразуется вblock
Вызов функции напрямую приостанавливает текущую Goroutine; - если
select
заявление содержит только одинcase
, будет преобразовано вif ch == nil { block }; n;
выражение;- Во-первых, определите, пуст ли канал операции;
- затем выполнить
case
содержание структуры;
- если
select
В заявлении всего дваcase
и один из нихdefault
, то и канал, и операции приема и отправки используютselectnbrecv
а такжеselectnbsend
Выполнять операции приема и отправки без блокировки; - по умолчанию
selectgo
Выбор функции должен быть выполненcase
и через несколькоif
выполнение оператораcase
выражение в;
Компилятор имеетselect
После оптимизации оператора язык Go выполнит расширение компиляции во время выполнения.selectgo
функция, эта функция будет выполняться в соответствии со следующим процессом:
- Случайным образом сгенерируйте порядок опроса обхода
pollOrder
И создайте порядок блокировки для обхода на основе адреса канала.lockOrder
; - согласно с
pollOrder
перебрать всеcase
Проверьте, есть ли сообщения Канала, которые можно обработать немедленно;- Если есть новости, получайте их напрямую
case
Соответствующий индекс и возврат;
- Если есть новости, получайте их напрямую
- будет создан, если нет сообщения
sudog
Структура, которая добавляет текущую горутину ко всем связанным каналам.sendq
а такжеrecvq
очередь и звонитеgopark
Запуск планирования планировщика; - Когда планировщик пробуждает текущую горутину, она будет следовать
lockOrder
перебрать всеcase
, из которого найти тот, который нужно обработатьsudog
структурировать и вернуть соответствующий индекс;
Тем не менее, не всеselect
структуры управления перейдут кselectgo
Во многих случаях будет оптимизировано прямое выключение, нет возможности позвонитьselectgo
функция.
на языке Гоselect
ключевые слова и мультиплексирование ввода-вывода вselect
,epoll
и другие функции очень похожи, не только операции отправки и получения канала, а также ожидание чтения и записи ввода-вывода могут найти это однозначное соответствие, но роли этих двух также очень похожи; в целом,select
Принцип реализации ключевого слова немного сложнее.ChannelОтношения очень тесные, многие детали операций Канала здесь опущены, а в главе о структурах данных они представлены.ChannelОтправляйте и получайте детали.
Статьи по Теме
- Принцип составления
- структура данных
- ключевые слова
Reference
О картинках и репринтах
В этой работе используетсяМеждународная лицензия Creative Commons Attribution 4.0Лицензия. При перепечатке просьба указывать ссылку на оригинал.При использовании рисунка просьба сохранять все содержание на рисунке.Его можно соответствующим образом увеличить и ссылку на статью,где находится рисунок,прикрепить к ссылке.Картинка нарисована с помощью Скетча.
Публичный аккаунт WeChat
О комментариях и сообщениях
Если эта статьяГоворя о принципе реализации выбора языка GoЕсли у вас есть какие-либо вопросы по содержанию, пожалуйста, оставьте сообщение в системе комментариев ниже, спасибо.Оригинальная ссылка:Рассказываем о принципе реализации select в языке Go
Follow: Draveness · GitHub