有序链表并发安全的实现

431 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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 可以同时执行写操作

问题

  1. 谁来规定每个 CPU 负责哪个区域?
  1. 如果插入一个新元素,属于哪个区域呢?
  1. 负载均衡问题,某些区域删除过多,某些区域插入过少导致各 CPU 控制的区域不均匀

启示:所以这个区域最好是动态抢占的,并且区域间有明显的分割

方案三

让节点及其后继指针作为有明显界限,可以动态组合的区域。如果某个 CPU 锁定了某个节点,它就控制这个节点及其后继指针这段区域。此时只有此 CPU 有权限在区域内进行写操作

Insert

  1. 找到节点 A 和 B,不不存在则直接返回
  1. 锁定节点 A ,检查 A.next == B,如果为假,则解锁 A 然后返回 step 1
  1. 创建新节点 X
  1. X.next = B; A.next = X
  1. 解锁节点 A

三种情况

  1. 多个插⼊入操作在 Step [2,5] 之间,因为都需要争抢 A 节点的锁,所以不不会冲突
  1. 两个插⼊入的 B1 和 A2 重合,由于有序链表的结构特点,不不会冲突
  1. 多个插⼊入操作在 Step [1,2] 之间,由于我们锁定 A 节点后进⾏行行检查,所以不不会冲突

delete

  1. 找到节点 A 和 B,不不存在则直接返回
  1. 锁定节点 B,检查 b.marked == true,如果为真,则解锁 B 然后返回 step 1
  1. 锁定节点 A,检查 A.next != B OR a.marked,如果为真,则解锁 A 和 B 然后返回 step 1
  1. b.marked = true;A.next = B.next
  1. 解锁节点 A 和 B

三种情况

  1. 多个删除前后执⾏行行,由于删除标记的存在,所以都可以正常删除
  1. 插⼊入或删除操作发⽣生在 Step [1,2] 之间,由于我们锁定 A、B 节点后进⾏行行检查,所以不不会冲突
  1. 插⼊入或删除操作发⽣生在 Step [2,5] 之间,由于我们同时锁定 A 和 B,A.next 和 B.next 都不不会改变

read

  1. 找到节点 A 和 B (atmoic.Load),不存在则直接返回
  1. 锁定节点 A ,检查A.next == B,如果为假,则解锁 A 然后返回 step 1
  1. 创建新节点 X
  1. X.next = B; A.next(atomic.Store) = X
  1. 解锁节点 A