持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情
并发安全问题
data race 实质:多个 CPU 争抢同一个内存且没有同步机制规定顺序,引发数据竞争问题
认定条件:类比 java,如果是读操作,多少个线程同时读都不要紧,但是如果包含写操作,读写操作的顺序就会对结果有影响,所以认定条件是两个及以上的 goroutine 同时接触一个变量,其中一个 goroutine 为写操作
检测方案: go run -race 或者 go test -race
- 解决方案:
-
- atomic 库:大多⽤用于解决单个变量的⼀写多读问题,Compare And Swap 等⽅法也可以解决多写多读,但是可能需要不断尝试,效率不⾼
- sync.Mutex:大多⽤用于解决多个变量的多写多读问题,实际是是CAS + semaphore + linux futex 的组合。(sync.RWMutex 同理理)
- 总体方案:
- 单个变量,一写多读 -> atomic
- 单个变量,多写多读 -> sync.Mutex
- 多个变量,多写多读 -> sync.Mutex
有序链表并发安全的实现
目标:设计一个并发安全的并且性能随着 CPU 核数增加而增加的有序链表
方案一
针对读操作远多于写操作,使用 sync.RWMutex 代替 sync.Mutex
问题:该方法能解决 data race,但是只能在并发操作下利用多核性能,并发写操作(删除和插入节点)只能利用一个 CPU(因为多个 CPU 必须要争抢这一把锁)
方案二
让那个不同 CPU 负责不同的区域,任何的写操作都由该区域的 CPU 来负责,使得多个 CPU 可以同时执行写操作
问题:
- 谁来规定每个 CPU 负责哪个区域?
- 如果插入一个新元素,属于哪个区域呢?
- 负载均衡问题,某些区域删除过多,某些区域插入过少导致各 CPU 控制的区域不均匀
启示:所以这个区域最好是动态抢占的,并且区域间有明显的分割
方案三
让节点及其后继指针作为有明显界限,可以动态组合的区域。如果某个 CPU 锁定了某个节点,它就控制这个节点及其后继指针这段区域。此时只有此 CPU 有权限在区域内进行写操作
Insert
- 找到节点 A 和 B,不不存在则直接返回
- 锁定节点 A ,检查 A.next == B,如果为假,则解锁 A 然后返回 step 1
- 创建新节点 X
- X.next = B; A.next = X
- 解锁节点 A
三种情况
- 多个插⼊入操作在 Step [2,5] 之间,因为都需要争抢 A 节点的锁,所以不不会冲突
- 两个插⼊入的 B1 和 A2 重合,由于有序链表的结构特点,不不会冲突
- 多个插⼊入操作在 Step [1,2] 之间,由于我们锁定 A 节点后进⾏行行检查,所以不不会冲突
delete
- 找到节点 A 和 B,不不存在则直接返回
- 锁定节点 B,检查 b.marked == true,如果为真,则解锁 B 然后返回 step 1
- 锁定节点 A,检查 A.next != B OR a.marked,如果为真,则解锁 A 和 B 然后返回 step 1
- b.marked = true;A.next = B.next
- 解锁节点 A 和 B
三种情况
- 多个删除前后执⾏行行,由于删除标记的存在,所以都可以正常删除
- 插⼊入或删除操作发⽣生在 Step [1,2] 之间,由于我们锁定 A、B 节点后进⾏行行检查,所以不不会冲突
- 插⼊入或删除操作发⽣生在 Step [2,5] 之间,由于我们同时锁定 A 和 B,A.next 和 B.next 都不不会改变
read
- 找到节点 A 和 B (atmoic.Load),不存在则直接返回
- 锁定节点 A ,检查A.next == B,如果为假,则解锁 A 然后返回 step 1
- 创建新节点 X
- X.next = B; A.next(atomic.Store) = X
- 解锁节点 A