Lock-Free Queue 实现

722 阅读2分钟

无锁队列

介绍

一般使用队列涉及并发的时候,会采用加锁的方式。但加锁会带来阻塞的问题,而且阻塞会带来线程切换开销。而无锁队列采用cas不断去尝试获取资源,来保证并发安全。

先看下并发不安全的代码

未命名文件.png

设置一个head 和tail 分别代表头尾,通过head和tail进行入队和出队

package gp_queue

type Node struct {
   value interface{}
   next  *Node
}

type Queue struct {
   head *Node
   tail *Node
}

func NewQueue() *Queue {
   n := &Node{}
   return &Queue{
      head: n,
      tail: n,
   }
}

func (q *Queue) Dequeue() interface{} {
   if q.head == q.tail {
      return nil
   }
   v := q.head.next.value
   q.head = q.head.next
   return v
}

func (q *Queue) Enqueue(v interface{}) {
   n := &Node{
      value: v,
      next:  nil,
   }
   q.tail.next = n
   q.tail = n
}

而无锁队列就是在此基础上对有并发问题的地方,采用load和cas操作进行不断重试来达到并发安全的目的

package gp_queue

import (
   "sync/atomic"
   "unsafe"
)

type LKQueue struct {
   head unsafe.Pointer
   tail unsafe.Pointer
}

type node struct {
   value interface{}
   next  unsafe.Pointer
}

func NewNode(v interface{}) *node {
   return &node{
      value: v,
      next:  nil,
   }
}

func NewLKqueue() *LKQueue {
   uPointer := unsafe.Pointer(NewNode(nil))
   return &LKQueue{
      head: uPointer,
      tail: uPointer,
   }
}

func (l *LKQueue) Enqueue(v interface{}) {
   n := NewNode(v)
   tail := load(&l.tail)
   next := load(&tail.next)
   for {
      if tail == load(&l.tail) { //判断 tail 还是原来的tai
         if next == nil { //判断 没有新数据插入队尾
            if cas(&tail.next, next, n) { //向队尾添加数据
               cas(&l.tail, tail, n) //将tail指向队尾
               return
            }
         } else {
            cas(&l.tail, tail, n) //已有数据加入队尾,移动tail至队尾
         }
      }
   }
}

func (l *LKQueue) Dequeue() interface{} {
   head := load(&l.head)
   tail := load(&l.tail)
   next := load(&head.next)
   for {
      if head == load(&l.head) { //判断 head 还是原来的head
         if head == tail { //判断是否为空
            if next == nil { //有数据插入
               return nil
            }
            cas(&l.tail, tail, next) //将tail指向队尾
         } else {
            v := next.value //读取出队数据
            //头指针移动到下一个,如果next的v已经读出去,那么head肯定以及变更,cas失败重新循环
            if cas(&l.head, head, next) {
               return v
            }
         }
      }
   }
}
//Load 方法会取出 addr 地址中的值,即使在多处理器、多核、有 CPU cache 的情况下,这个操作也能保证 Load 是一个原子操作。
func load(p *unsafe.Pointer) *node {
   return (*node)(atomic.LoadPointer(p))
}

func cas(p *unsafe.Pointer, old, new *node) bool {
   return atomic.CompareAndSwapPointer(p, unsafe.Pointer(old), unsafe.Pointer(new))
}

参考: 12 | atomic:要保证原子操作,一定要使用这几种方法 (geekbang.org)