контур
[toc]
Исходная публичная учетная запись: Qiya Cloud Storage
Почему бы не оценить близкий интерфейс golang?
Я считаю, что каждый должен был столкнуться с этим, когда они впервые узнали голанг тян."send on closed channel"паника. Эта паника срабатывает, когда вы намереваетесь доставить элемент в канал, который был закрыт. Итак, когда вы впервые столкнулись с этой проблемой, подумали ли вы о том, может ли канал предоставить интерфейсный метод для определения того, был ли он закрыт? Я думал об этом, но просмотрел исходный код chan и не смог его найти. Почему?
Позвольте мне сначала задать этот вопрос, давайте посмотрим на вещи, связанные с закрытием канала, и в основном подумаем над 3 вопросами:
- Что именно делает закрытие канала?
- Как избежать проблемы паники, вызванной закрытым каналом?
- Как красиво закрыть канал?
Что именно делает закрытие канала?
Во-первых, пользователь может закрыть канал следующим образом:
c := make(chan int)
// ...
close(c)
Отладка с помощью gdb или delve может обнаружить, что канал закрыт, компилятор преобразует его вclosechanФункция, в этой функции вся реализация закрытия канала, мы можем ее проанализировать.
closechan
Соответствующая функция компиляцииclosechan, функция очень проста и делает примерно 3 вещи:
- позиция флага 1 , то есть
c.closed = 1; - Освободить ресурсы и разбудить все сопрограммы, ожидающие выборки элементов;
- Освободить ресурсы и разбудить все сопрограммы, ожидающие записи элементов;
func closechan(c *hchan) {
// 以下为锁内操作
lock(&c.lock)
// 不能重复 close 一个 channel,否则 panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
// closed 标志位置 1
c.closed = 1
var glist gList
// 释放所有等待取元素的 waiter 资源
for {
// 等待读的 waiter 出队
sg := c.recvq.dequeue()
// 资源一个个销毁
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
gp := sg.g
gp.param = nil
// 相应 goroutine 加到统一队列,下面会统一唤醒
glist.push(gp)
}
// 释放所有等待写元素的 waiter 资源(他们之后将会 panic)
for {
// 等待写的 waiter 出队
sg := c.sendq.dequeue()
// 资源一个个销毁
sg.elem = nil
gp := sg.g
gp.param = nil
// 对应 goroutine 加到统一队列,下面会统一唤醒
glist.push(gp)
}
unlock(&c.lock)
// 唤醒所有的 waiter 对应的 goroutine (这个协程列表是上面 push 进来的)
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
С помощью приведенной выше логики кода мы получаем две важные информации:
- близкая тян отмечена;
- близкий тян разбудит ожидающих людей;
Но очень странно, что наш голанг официал не предоставляет интерфейс для суждения закрыт ли чан? Итак, можем ли мы реализовать метод, чтобы определить, близок ли чан?
Функция для проверки того, близок ли чан
Как этого добиться? первыйisChanCloseК функции предъявляется несколько требований:
- быть в состоянии указать, что это действительно близко;
- Может работать нормально в любое время и вернуться (неблокирующий);
отзыватьСамый подробный разбор канала golangглава, подумайsend, recvДля связанных функций мы можем знать, что существует только два типа жестов использования, которые текущий канал дает пользователю: чтение и запись.isChanCloseЭто можно сделать только на этой основе.
- Напишите:
c <- x - читать:
<-cилиv := <-cилиv, ok := <-c
Метод мышления 1: Реализация путем «написания» тян
«Запись» не должна использоваться как суждение, и я не могу всегда пытаться записать в нее данные, чтобы судить, закрыт ли чан? это приведет кchansendТе, которые паникуют прямо внутри, следующие:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// ...
// 当 channel close 之后的处理逻辑
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// ...
}
Конечно, если ваш подход немного дикий, этого можно добиться технически, потому что панику можно поймать, но это слишком дико и не рекомендуется.
Метод мышления 2: Осознайте, «читая» тян
«Прочитай», чтобы судить. Аналитические функцииchanrecvИзвестно, что при попытке прочитать данные из закрытого канала возвращается (выбрано=истина, получено=ложь), и мы можем узнать, закрыт ли канал, получив = ложь.chanrecvЕсть следующий код:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// ...
// 当 channel close 之后的处理逻辑
if c.closed != 0 && c.qcount == 0 {
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
// ...
}
Итак, теперь мы знаем, можно судить по эффекту «чтение», но мы не можем напрямую быть написано так:
// 错误示例
func isChanClose(ch chan int) bool {
_, ok := <- c
}
выше этоПример ошибки,потому что_, ok := <-cскомпилированоchanrecv2, этому функциональному блоку присваивается значение true и передается, поэтому, когда c является нормальным, он заблокирован здесь, поэтому его нельзя использовать как обычный вызов функции, поскольку он блокирует сопрограмму, как решить эту проблему? использоватьselectа также<-chanкомбинирование может решить эту проблему,selectа также<-chanкомбинировать, чтобы соответствоватьselectnbrecvа такжеselectnbrecv2Эти две неблокирующие функции (block = false).
Правильный пример:
func isChanClose(ch chan int) bool {
select {
case _, received := <- ch:
return !received
default:
}
return false
}
Многие люди в Интернете упоминали одинisChanCloseПлохой пример, плохой пример:
func isChanClose(ch chan int) bool {
select {
case <- ch:
return true
default:
}
return false
}
Подумайте об этом: почему первый пример верен, а второй ложен?
Потому что соответствующая функция, скомпилированная из первого примера,selectnbrecv2, второй скомпилированный пример соответствуетselectnbrecv1, разница между двумя функциями в том, чтоselectnbrecv2Еще один возвращаемый параметрreceived, только эта функция может указать, успешно ли элемент удален из очереди, иselectedПросто рассудите, стоит ли переходить в ветку select case. мы проходимreceivedЭто возвращаемое значение (на самом деле входной параметр, но тип указателя, который может быть изменен внутри функции) используется для обратного вывода, закрыт ли chan.
резюме:
- Код дела должен быть
_, received := <- chформа, если только<- chЭто неправильная логика, чтобы судить, потому что мы обеспокоеныreceivedзначение; - select должен иметь ветку по умолчанию, иначе функция будет заблокирована, и наша функция должна гарантированно нормально возвращаться;
принцип закрытия чан
- Никогда не пытайтесь закрыть канал на стороне чтения, сторона записи не может знать, был ли канал закрыт, и запись данных в закрытый канал вызовет панику;
- Конец записи, где вы можете безопасно закрыть канал;
- Когда есть несколько писателей, не закрывайте канал на стороне пишущего.Другие писатели не могут знать, закрыт ли канал.При закрытии закрытого канала возникнет паника (вы должны найти способ сделать так, чтобы только один человек вызывал close );
- Когда канал используется в качестве параметра функции, лучше всего указать направление;
На самом деле в этих принципах есть только один пункт: закрытие канала должно быть безопасным.
На самом деле не нужноisChanCloseфункция !!!
реализовано вышеisChanCloseМожно определить, закрыт ли канал, но применимый сценарий предпочтительнее, потому что он может ждать васisChanCloseПри оценке возвращаемое значение ложно, вы думаете, что канал еще нормальный, но в следующий момент канал закрывается, в это время "запись" данных в него снова паникует, следующим образом:
if isChanClose( c ) {
// 关闭的场景,exit
return
}
// 未关闭的场景,继续执行(可能还是会 panic)
c <- x
Потому что после суда еще есть временное окно, так чтоisChanCloseПриложение все еще ограничено, так что есть ли лучший способ?
Давайте изменим образ мышления. На самом деле вам не нужно судить, близок ли канал. Настоящая цель такова:Используйте каналы безопасно и избегайте использования закрытых каналов, которые уже были закрыты, что может привести к панике..
Суть этой проблемы заключается в обеспечении тайминга события, официальная рекомендация - пройтиcontextДля совместного использования мы можем указать событие закрытия через переменную ctx вместо того, чтобы напрямую оценивать состояние канала. Возьмите каштан:
select {
case <-ctx.Done():
// ... exit
return
case v, ok := <-c:
// do something....
default:
// do default ....
}
ctx.Done()После того, как событие произошло, мы даем понять, что не будем читать данные канала.
или
select {
case <-ctx.Done():
// ... exit
return
default:
// push
c <- x
}
ctx.Done()После того, как событие произошло, мы даем понять, что не записываем данные в канал, либо не читаем данные из канала, поэтому можно гарантировать это время. Проблем не будет.
Нам просто нужно убедиться, что:
- Гарантия времени срабатывания: обязательно сначала инициируйте событие ctx.Done(), а затем выполните операцию закрытия канала, чтобы гарантировать, что это время может гарантировать отсутствие проблем при выборе решения;
- Только это время может гарантировать, что все будет в безопасности, когда событие Done будет изучено;
- Условный порядок оценки: select case сначала оценивает событие ctx.Done(), это очень важно, иначе очень вероятно, что операция chan будет выполнена первой и вызовет проблему паники;
Как красиво закрыть чан?
Способ 1: паническое восстановление
Чтобы закрыть канал, просто вызовите close напрямую, но закрытие уже закрытого канала вызовет панику, что мне делать? Его можно использовать в сочетании с паническим восстановлением.
func SafeClose(ch chan int) (closed bool) {
defer func() {
if recover() != nil {
closed = false
}
}()
// 如果 ch 是一个已经关闭的,会 panic 的,然后被 recover 捕捉到;
close(ch)
return true
}
Это не элегантно.
Способ 2: sync.Once
можно использоватьsync.Onceдля обеспеченияcloseВыполнить только один раз.
type ChanMgr struct {
C chan int
once sync.Once
}
func NewChanMgr() *ChanMgr {
return &ChanMgr{C: make(chan int)}
}
func (cm *ChanMgr) SafeClose() {
cm.once.Do(func() { close(cm.C) })
}
Это выглядит нормально.
Способ 3: синхронизация событий для решения
У нас есть два кратких принципа закрытия канала:
- Никогда не пытайтесь закрыть канал на стороне чтения;
- Только одна горутина (например, одна горутина, которая используется только для выполнения операции выключения) всегда может выполнять операцию выключения;
можно использоватьsync.WaitGroupЧтобы синхронизировать это событие выключения, следуйте приведенным выше принципам, чтобы привести несколько примеров:
Первый пример: отправитель
package main
import "sync"
func main() {
// channel 初始化
c := make(chan int, 10)
// 用来 recevivers 同步事件的
wg := sync.WaitGroup{}
// sender(写端)
go func() {
// 入队
c <- 1
// ...
// 满足某些情况,则 close channel
close(c)
}()
// receivers (读端)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ... 处理 channel 里的数据
for v := range c {
_ = v
}
}()
}
// 等待所有的 receivers 完成;
wg.Wait()
}
В этом примере мы закрываем канал в горутине отправителя.Поскольку есть только один отправитель, закрытие, естественно, безопасно. использование приемникаWaitGroupДля синхронизации событий цикл for получателя завершится только после закрытия канала, а основная сопрограммаwg.Wait()Оператор не вернется, пока все получатели не завершат работу. Итак, последовательность событий такова:
- Сторона записи ставит в очередь целочисленный элемент
- закрыть канал
- Все окончания чтения завершаются безопасно
- основная сопрограмма возвращается
Все безопасно.
Второй пример: несколько отправителей
package main
import (
"context"
"sync"
"time"
)
func main() {
// channel 初始化
c := make(chan int, 10)
// 用来 recevivers 同步事件的
wg := sync.WaitGroup{}
// 上下文
ctx, cancel := context.WithCancel(context.TODO())
// 专门关闭的协程
go func() {
time.Sleep(2 * time.Second)
cancel()
// ... 某种条件下,关闭 channel
close(c)
}()
// senders(写端)
for i := 0; i < 10; i++ {
go func(ctx context.Context, id int) {
select {
case <-ctx.Done():
return
case c <- id: // 入队
// ...
}
}(ctx, i)
}
// receivers(读端)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ... 处理 channel 里的数据
for v := range c {
_ = v
}
}()
}
// 等待所有的 receivers 完成;
wg.Wait()
}
В этом примере мы видим, что есть несколько отправителей и получателей. В этом случае нам все еще нужно убедиться, что только один человек может выполнить операцию close(ch). Для этого мы извлекаем горутину и используем контекст, чтобы сделать синхронизация событий. , последовательность событий такова:
- Запускается 10 сопрограмм на стороне записи (отправитель), доставляющих элементы;
- Запуск 10 сопрограмм на стороне чтения (получатель), чтение элементов;
- После двухминутного тайм-аута выполняется отдельная сопрограмма.
close(channel)действовать; - Основная сопрограмма возвращается;
Все безопасно.
Суммировать
- Канал напрямую не предоставляет интерфейс для оценки того, закрыт он или нет.Официально рекомендуется использовать контекст и синтаксис выбора в сочетании с уведомлением о событии, чтобы добиться эффекта элегантного суждения о закрытии канала;
- Положение закрытия канала также очень специфично: никогда не пытайтесь закрыть его на стороне чтения, всегда сохраняйте закрывающую запись и используйте sync.WaitGroup и контекст для достижения синхронизации событий для достижения плавного эффекта закрытия;