最近在读 gRPC 的源码,以下代码截取自 [email protected]/balancer_conn_wrappers.go 文件第 118 行。
// watcher balancer functions sequentially, so the balancer can be implemented
// lock-free.
func (ccb *ccBalancerWrapper) watcher() {
for {
select {
case t := <-ccb.stateChangeQueue.get():
ccb.stateChangeQueue.load()
select {
case <-ccb.done:
ccb.balancer.Close()
return
default:
}
ccb.balancer.HandleSubConnStateChange(t.sc, t.state)
case t := <-ccb.resolverUpdateCh:
select {
case <-ccb.done:
ccb.balancer.Close()
return
default:
}
ccb.balancer.HandleResolvedAddrs(t.addrs, t.err)
case <-ccb.done:
}
select {
case <-ccb.done:
ccb.balancer.Close()
ccb.mu.Lock()
scs := ccb.subConns
ccb.subConns = nil
ccb.mu.Unlock()
for acbw := range scs {
ccb.cc.removeAddrConn(acbw.getAddrConn(), errConnDrain)
}
return
default:
}
}
}
关于这段代码,我理解是,隐藏 ccb.balancer 的方法调用,转为以 channel 信号触发。其目的是使得 balancer 的方法在实现时不需要考虑并发问题。
我的问题是,这里如果给 balancer 的方法调用都加互斥锁,也能实现同样的目的,那么用 channel 还有什么好处呢?有哪些地方我没有考虑到的,恳请大神们指正。
1
xrlin 2019-03-27 17:34:55 +08:00
使用 select 实现同时监听多个 channel 信号,而且使用锁在尝试加锁 Lock 时可能会阻塞,在尝试解锁 Unlock 时可能会触发异常,使用 channel 免去了各种情况都处理,而且可阅读性更强。
channel 的那边实现也是有锁的,channel 还可以实现数据的分发,这些在需要通信的场景下比锁好用多了。 |
2
owenliang 2019-03-27 18:02:00 +08:00
收敛逻辑在一个协程,这无疑是可读性最高的,最好维护的。
你说的玩锁的方法,一般存在于 c++中,shared_ptr+lock 保证单个资源的同步,但带来的坏处就是并发很复杂,很难看懂。 |
3
index90 OP @xrlin 如果其中一个 channel 在处理中,其他 channel 也会被阻塞,这个和 Lock 类似。
不过也可以实现队列:ccb.stateChangeQueue,来避免阻塞。 Unlock 会触发异常,这个会产生什么异常?使用 channel 可读性更强,目前还没习惯,可能思维还没转换过来。 就为实现“顺序调用对象的 function ”这个问题上,我只是觉得用锁实现的话,代码量会少一点。 上面示例之所以非使用 channel 不可,我觉得是因为 ccb.stateChangeQueue 需要非阻塞,所以要用 Channel,不知道我理解是否正确? |
4
xrlin 2019-03-27 19:11:48 +08:00 via Android
@index90 Lock 方法在取到锁前会一直阻塞,如果要监听多把锁需要在不同的协程中进行了,Unlock 一把未锁定的锁会导致 panic,虽然这情况属于逻辑错误,但是使用 channel 就可以减轻思维负担了,channel 本身提供了并发保证,好维护。
|
5
hilbertz 2019-03-27 19:14:55 +08:00
锁能提供更细的控制粒度,延迟更低,但也更复杂
|