Говоря о принципе реализации выбора языка Go

Go

Многие разработчики C или Unix слышатselectПодумайте обо всех системных вызовах, и когда вы говорите о модели ввода-вывода, вы в конечном итоге упомяну об этом.select,pollа такжеepollМодель мультиплексирования ввода-вывода, построенная с помощью таких функций, как язык Go, который мы представим в этом разделе.selectКлючевое слово на самом деле такое же, как в языке CselectЕсть относительно похожие функции.

Этот раздел познакомит вас с языком Go.selectпринципы реализации, в том числеselectструктура и общие проблемы, различные оптимизации во время компиляции и выполнения во время выполнения.

Обзор

на языке СselectКлючевое слово может одновременно отслеживать доступное для чтения или записи состояние нескольких файловых дескрипторов. Прежде чем состояние файлового дескриптора изменится,selectВсегда будет блокировать текущий поток на языке GoselectЭто ключевое слово чем-то похоже на ключевое слово в языке C, за исключением того, что оно позволяет горутине ожидать одновременной готовности нескольких каналов.

Golang-Select-Channels

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узел:

Golang-Select-Case-Struct

Собственно, на картинке выше показаноselectструктура во время компиляции, каждыйOCASEОн содержит как условия выполнения, так и код, который выполняется после выполнения условий, которые мы представим в этом разделе.selectОптимизации и преобразования, выполняемые оператором во время компиляции.

Во время генерации промежуточного кода компиляторselectсерединаcaseОператор управления оптимизируется в соответствии с разницей, и этот процесс фактически происходит вwalkselectcasesВ функции мы представим процесс оптимизации и результаты в четырех случаях:

  1. selectНет необходимостиcase;
  2. selectздесь только одинcase;
  3. selectесть дваcase,один изcaseдаdefaultутверждение;
  4. универсальный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Операторы обрабатываются на этапе компиляции следующим образом:

  1. положить всеcaseПреобразуется в канал, содержащий такую ​​информацию, как канал и типscaseструктура;
  2. вызов функции времени выполненияselectgoбыть выбраннымscaseструктурный индекс, если текущийscaseЭто операция, которая получает данные и возвращает текущую индикациюcaseполучено логическое значение;
  3. пройти через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:

  1. caseNil- токcaseЕсли он не содержит ни одного канала, он будет пропущен напрямую;
  2. caseRecv- токcaseБудет получать данные от Канала;
    • Если текущий каналsendqГорутины, ожидающие этого, перейдут прямо кrecvСегмент кода, в котором находится тег, получает последние отправленные данные из горутины;
    • Если буфер текущего канала не пуст, он перейдет кbufrecvПолучить данные из буфера по метке;
    • Если текущий канал был закрыт, он перейдет кrcloseСделайте некоторые последние штрихи очистки;
  3. caseSend- токcaseОтправит данные в канал;
    • Если текущий канал был закрыт, он сразу перейдет кrcloseсегмент кода;
    • Если текущий каналrecvqГорутины, ожидающие этого, перейдут кsendСегмент кода отправляет данные непосредственно в канал;
  4. caseDefault- токcaseУказывает ситуацию по умолчанию, если цикл выполняется до этой ситуации, это означает, что все предыдущиеcaseне выполняются, поэтому здесь будут сразу разблокированы все каналы и выходselectgoфункция, что означает, что текущийselectДругие операторы отправки и получения в структуре являются неблокирующими.

Golang-Select-Go-Loop

Соответствующий код на самом деле довольно длинный. Он не показан здесь для удобства чтения, но конкретный процесс выполнения также может быть четко показан с помощью описания и блок-схемы. Читатели, которые все еще хотят знать соответствующий код, могут проверить его.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 и ждет, пока планировщик проснется.

Golang-Select-Waiting

Подожди пока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Различные пути оптимизации выбираются для разных операторов в:

  1. пустойselectоператор напрямую преобразуется вblockВызов функции напрямую приостанавливает текущую Goroutine;
  2. еслиselectзаявление содержит только одинcase, будет преобразовано вif ch == nil { block }; n;выражение;
    • Во-первых, определите, пуст ли канал операции;
    • затем выполнитьcaseсодержание структуры;
  3. еслиselectВ заявлении всего дваcaseи один из нихdefault, то и канал, и операции приема и отправки используютselectnbrecvа такжеselectnbsendВыполнять операции приема и отправки без блокировки;
  4. по умолчаниюselectgoВыбор функции должен быть выполненcaseи через несколькоifвыполнение оператораcaseвыражение в;

Компилятор имеетselectПосле оптимизации оператора язык Go выполнит расширение компиляции во время выполнения.selectgoфункция, эта функция будет выполняться в соответствии со следующим процессом:

  1. Случайным образом сгенерируйте порядок опроса обходаpollOrderИ создайте порядок блокировки для обхода на основе адреса канала.lockOrder;
  2. согласно сpollOrderперебрать всеcaseПроверьте, есть ли сообщения Канала, которые можно обработать немедленно;
    1. Если есть новости, получайте их напрямуюcaseСоответствующий индекс и возврат;
  3. будет создан, если нет сообщенияsudogСтруктура, которая добавляет текущую горутину ко всем связанным каналам.sendqа такжеrecvqочередь и звонитеgoparkЗапуск планирования планировщика;
  4. Когда планировщик пробуждает текущую горутину, она будет следоватьlockOrderперебрать всеcase, из которого найти тот, который нужно обработатьsudogструктурировать и вернуть соответствующий индекс;

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

на языке Гоselectключевые слова и мультиплексирование ввода-вывода вselect,epollи другие функции очень похожи, не только операции отправки и получения канала, а также ожидание чтения и записи ввода-вывода могут найти это однозначное соответствие, но роли этих двух также очень похожи; в целом,selectПринцип реализации ключевого слова немного сложнее.ChannelОтношения очень тесные, многие детали операций Канала здесь опущены, а в главе о структурах данных они представлены.ChannelОтправляйте и получайте детали.

Статьи по Теме

Reference

О картинках и репринтах

知识共享许可协议
В этой работе используетсяМеждународная лицензия Creative Commons Attribution 4.0Лицензия. При перепечатке просьба указывать ссылку на оригинал.При использовании рисунка просьба сохранять все содержание на рисунке.Его можно соответствующим образом увеличить и ссылку на статью,где находится рисунок,прикрепить к ссылке.Картинка нарисована с помощью Скетча.

Публичный аккаунт WeChat

wechat-account-qrcode

О комментариях и сообщениях

Если эта статьяГоворя о принципе реализации выбора языка GoЕсли у вас есть какие-либо вопросы по содержанию, пожалуйста, оставьте сообщение в системе комментариев ниже, спасибо.

Оригинальная ссылка:Рассказываем о принципе реализации select в языке Go

Follow: Draveness · GitHub