无锁队列
介绍
一般使用队列涉及并发的时候,会采用加锁的方式。但加锁会带来阻塞的问题,而且阻塞会带来线程切换开销。而无锁队列采用cas不断去尝试获取资源,来保证并发安全。
先看下并发不安全的代码
设置一个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))
}