多个goroutine访问同一个变量,需要考虑Data Race的问题。可以使用下面的命令来进行检测:
go test -race 或者 go run -race
解决方案:使用atomic库或者sync.Mutex,对访问顺序进行限制。
- atomic,一般在单个变量一写多读的场景使用, Compare And Swap虽然可以用于多写多读,但是应用乐观锁,效率不高
- sync.Mutex,一般在多个变量的多写多读场景使用
针对当前问题,实现并发安全的有序单链表,最简单的思路是,对给单链表加锁。如果是读操作远多于写操作,使用读写锁。
这种方式,可以解决Data Race的问题,但是只有并发读可以利用CPU多核能力,并发写操作时,只能使用一个CPU的能力。
如果想要使得并发写时,可以充分发挥多核CPU的能力,那么链表的写操作就需要更精细的处理。可以把锁的粒度,缩小到节点级别。
以并发插入为例:
如果想在A节点后边插入X节点,那么就需要新创建一个X节点,并把X的下一个节点指向B,再把A的下一个节点从指向B改为指向X。
在这个过程中,需要考虑的Data Race环节是:
- 如果一个节点在被修改,那么它不允许被其他goroutine读取或者写入。这个需要加锁保证
- 在A加锁之前,A.next指向的内容是可能会变更的,比如同时还有插入Y节点的操作,在A加锁之前,A.next实际就已经变为Y了,所以在A加锁之后,需要判定A.next是否依旧等于B,如果不再等于B了,那么需要重新查找A.next,以确保X.next的值无误
- 由于节点的写入操作加了锁,所以这是一个一写多读的场景,为了提升读性能,需要在这种场景下,使用原子读写
其他有序单链表的操作,可以参考插入操作的思路,最终代码实现如下:
// Package clist
package clist
import (
"sync"
"sync/atomic"
"unsafe"
)
type IntList struct {
head *intNode
length int64
}
type intNode struct {
value int
next *intNode
mu sync.Mutex
marked int32
}
func newIntNode(value int) *intNode {
return &intNode{
value: value}
}
func NewInt() *IntList {
return &IntList{
head: newIntNode(-1)}
}
func (l *IntList) Insert(value int) bool {
var a *intNode
var b *intNode
for {
// Step 1, 寻找a b
a = l.head
b = a.atomicNext()