1、尽量使用函数参数的方式传递信息。
协程间少使用共享数据结构(尤其是会变化的全局变量)
2、使用支持并发的go数据结构
比如sync.Map,sync.Once,sync.Map使用要注意几点:
添加不要先查找再添加(查找和添加间数据可能已经并发修改),如下操作是有问题的
val, ok := eMap.Find(key)
if ok {
//旧数据
return
}
//处理newNode初始化
eMap.Store(newNode)
//........
//处理新数据
判断是否是新添加数据使用
//处理newNode初始化
val, loaded :=val, loaded := eMap.LoadOrStore(newNode)
if loaded {
//处理旧数据
} else {
//新添加数据
}
另外需要注意,即使按照上面的方法添加,如果新添加数据中涉及必须的初始化,也是有问题的。
因为并发另一个协程认为是旧数据,而添加数据的协程还没初始化完毕。所以初始化必须放在store之前。
删除同理,也不能先Find再决定是不是Delete
3、使用锁
( 无法避免协程间共享数据的情况下)
锁很容易死锁或者拖累效率,使用应遵循以下几个原则:
原则一 锁在数据初始化时赋值,中间不能重新赋值
原则二 锁粒度要小
包括锁的数据粒度小,比如能锁单个node就不锁local区。
粒度小排查方便,性能影响有限,也方便defer解锁
原则三 使用defer解锁
不使用defer,后面增加return流程很容易漏掉解锁步骤。如:
func (node *Node) LinkFindByKey(linkKey LinkKey) *Link {
node.linkLocker.RLock()
defer node.linkLocker.RUnlock()
val, _ := node.LinkTree.Get(linkKey)
if val == nil {
return nil
}
return val.(*Link)
}
原则四 把加锁内容单独封装函数,且该函数中不得增加不需要加锁的流程
这是由原则三导致的,不然锁的范围大,很容易死锁,排查也麻烦
原则五 使用for循环访问数据的读锁,尽量保护读取后用局部变量
(以避免锁范围过大,影响效率或难以排查)
如
for _, v, next := local.GTree.Iterate()(); next != nil; _, v, next = next() {
//do something
}
应该保护为
local.GTreeLoker.RLock()
gTree := local.GTree
local.GTreeLoker.RUlock()
for _, v, next := gTree.Iterate()(); next != nil; _, v, next = next() {
//do something
}
而不是
local.GTreeLoker.RLock()
for _, v, next := local.GTree.Iterate()(); next != nil; _, v, next = next() {
//do something
}
local.GTreeLoker.RUlock()
原则五 尽量不要嵌套
一般只要遵循锁最小化,就能做到不嵌套
4、使用channel并发改串行