在日常开发中, 经常会使用chan
来进行协程之间的通信. 对chan
的操作也无外乎读写关. 而本次, 就是从chan
的关闭而来.
假设我们对外提供的方法如下:
type Chan struct {ch chan int
}func (c *Chan) Close() {close(c.ch)
}func (c *Chan) Send(v int) {c.ch <- v
}
那么, 简单写段逻辑测试一下:
func main() {for {c := &Chan{ch: make(chan int, 100)}w := sync.WaitGroup{}w.Add(2)go func() {defer w.Done()c.Send(1)}()go func() {defer w.Done()c.Close()}()w.Wait()}
}
很快, 你就会看到报错了:
报错的原因也很简单, 就是因为向已关闭的管道中写数据.
我们如何能实现安全的管道关闭操作呢?
注意, 本次不讨论重复调用Close
方法的情况, 这种只要加锁保证单次执行就可以了, 情况简单.
有的小伙伴想到了, 加个状态判断不就行了, 管道关闭之后就不再往里写咯. 并很快给出了代码:
type Chan struct {ch chan intisClosed bool
}
func (c *Chan) Close() {c.isClosed = trueclose(c.ch)
}
func (c *Chan) Send(v int) bool {if c.isClosed {return false}c.ch <- vreturn true
}
但是, 我相信在运行之后就不会这么想了.
问题: 如果在发送时的 isClosed
判断与chan
通信之间发生了管道的关闭, 还是会出错.
既然发生错误的原因是操作的原则性, 那很自然的就会想到加锁.
type Chan struct {ch chan intisClosed boollock sync.Mutex
}func (c *Chan) Close() {c.lock.Lock()defer c.lock.Unlock()c.isClosed = trueclose(c.ch)
}func (c *Chan) Send(v int) bool {c.lock.Lock()defer c.lock.Unlock()if c.isClosed {return false}c.ch <- vreturn true
}
将isClosed
变量的访问与赋值通过加锁来实现原子操作, 那么我们直接让Close
和Send
函数全体加锁, 将其强行改为串行, 不就解决了嘛?
开心的告诉你, 没错, 这样确实解决了.而且, 加锁的粒度不能再小了,
但是, 不得不遗憾的告诉你, 这么写只能说看起来对.
问题: 因为向管道写数据是阻塞的, 当chan
满了再写数据, 就会阻塞在这里, 持有的锁就不会释放. 导致关闭函数一直无法执行.
有的小机灵鬼想到了, 使用select-case
将阻塞写改为非阻塞写不就行了嘛? 确实可以, 但这样无疑会增加调用者的成本.
有的小机灵想到了, 我再Send
的时候把panic
抓住处理掉不就行了么.
type Chan struct {ch chan int
}func (c *Chan) Close() {close(c.ch)
}func (c *Chan) Send(v int) (ret bool) {defer func() {if r := recover(); r != nil {ret = false}}()c.ch <- vreturn true
}
开心的告诉你, 确实可以. 而且不存在锁的竞争问题, 很好.
但是, 在Go
的哲学中, 流程中的异常是通过error
返回, 通过捕捉panic
来实现总觉得怪怪的.
Sender
关闭既然总会遇到风险, 那么我在Send
汉中中进行close
可以么?
type Chan struct {ch chan intneedClose boolonce sync.Once
}func (c *Chan) Close() {c.needClose = true
}func (c *Chan) Send(v int) (ret bool) {if c.needClose {c.once.Do(func() {close(c.ch)})return false}c.ch <- vreturn true
}
这样确实也是可以的, 但是别高兴的太早.
问题: 如果在Close
之后没有调用Send
方法, 那么此chan
就不会关闭, 也就无法通知到接收方了. 因此, 十分不推荐.
包括使用额外的chan
来实现, 也不推荐, 因为没有将chan
关闭, 无法通知接收方(如下):
type Chan struct {ch chan intdone chan int
}func (c *Chan) Close() {close(c.done)
}func (c *Chan) Send(v int) (ret bool) {select {case <-c.done:return falsecase c.ch <- v:return true}
}
等等吧, 其实有很多方式来实现, 在这里就不一一赘述了.
回过头来再想.
panic
, 却可以从已关闭的管道读取数据?从官方 API 的设计中, 我们可以猜到其希望调用方做的事情:
而我们关闭管道的目的是什么呢? 无非是:
那么, 我们就目的而言来分析:
close
就行综上, 我得出如下结论, 而这想必也是官方的意思吧:
那么, 如果真的真的就是要在发送完之前关闭管道怎么办呢?
recover
都要被加锁更好.以上, 如果你有什么其他意见, 烦请不吝赐教
原文地址: https://hujingnb.com/archives/888