一个“锁无关”的程序能够确保执行它的所有线程中至少有一个能够继续往下执行。
就是说你的各个线程不会互相阻塞,那么你的程序才能成为lock free的。像我们平常用的互斥锁,当有线程获得锁,其他线程就被阻塞掉了,这里的问题就是如果获得锁的线程挂掉了,而且锁也没有释放,那么整个程序其实就被block在那了,而如果程序是lock free的那么即使有线程挂掉,也不影响整个程序继续向下进行,也就是系统在整体上而言是一直前进的。
那么,不用锁就是lock free的吗,一开始就提到了,不用锁也可能不是lock free的,如果两个线程同时执行,可能同时进入while循环,然后x两次改变值之后,依然是0,那么两个线程就会一直互相在这里阻塞掉了,所以这里虽然没有锁,依然不是lock free的。
Lock-free 算法的基础是 CAS (Compareand-Swap) 原子操作。当某个地址的原始值等于某个比较值时,把值改成新值,无论有否修改,返回这个地址的原始值。目前的cpu 支持最多64位的CAS。并且指针 p 必须对齐。
注:原子操作指一个cpu时钟周期内就可以完成的操作,不会被其他线程干扰。
一般的 CAS 使用方式是:
假设有指针 p, 它指向一个 32 位或者64位数,
复制p 的内容(*p)到比较量 cmp (原子操作)
基于这个比较量计算一个新值 xchg (非原子操作)
调用 CAS 比较当前 *p 和 cmp, 如果相等把 *p 替换成 xchg (原子操作)
如果成功退出,否则回到第一步重新进行
第3步的 CAS 操作保证了写入的同时 p 没有被其他线程更改。如果*p已经被其他线程更改,那么第2步计算新值所使用的值(cmp)已经过期了,因此这个整个过程失败,重新来过。多线程环境下,由于 3 是一个原子操作,那么起码有一个线程(最快执行到3)的CAS 操作可以成功,这样整体上看,就保证了所有的线程上在“前进”,而不需要使用效率低下的锁来协调线程,更不会导致死锁之类的麻烦。
ABA 问题
当 A 线程执行2的时候,被B 线程更改了 *p为x, 而 C 线程又把它改回了原始值,这时回到A 线程,A线程无法监测到原始值已经被更改过了,CAS 操作会成功(实际上应该失败)。ABA 大部分情况下会造成一些问题,因为 p 的内容一般不可能是独立的,其他内容已经更改,而A线程认为它没有更改就会带来不可预知的结果。
ABA 问题的解决一般是扩展 *p 的位数(比如从32扩展到64),使用多余的位来保存一个版本号,每次更改都修改版本号,从而保证这个线程能正确的监测到值的更改。
atomic.value
使用姿势
atomic.Value类型对外暴露的方法就两个:
-
v.Store(c)- 写操作,将原始的变量c存放到一个atomic.Value类型的v里。c = v.Load()- 读操作,从线程安全的v中读取上一步存放的内容。
简洁的接口使得它的使用也很简单,只需将需要作并发保护的变量读取和赋值操作用Load()和Store()代替就行了。
数据结构
atomic.Value被设计用来存储任意类型的数据,所以它内部的字段是一个interface{}类型,非常的简单粗暴。
type Value struct {
v interface{}
}
除了Value外,这个文件里还定义了一个ifaceWords类型,这其实是一个空interface (interface{})的内部表示格式(参见runtime/runtime2.go中eface的定义)。它的作用是将interface{}类型分解,得到其中的两个字段。
写入(Store)操作
在介绍写入之前,我们先来看一下 Go 语言内部的unsafe.Pointer类型。
出于安全考虑,Go 语言并不支持直接操作内存,但它的标准库中又提供一种不安全(不保证向后兼容性) 的指针类型unsafe.Pointer,让程序可以灵活的操作内存。
unsafe.Pointer的特别之处在于,它可以绕过 Go 语言类型系统的检查,与任意的指针类型互相转换。也就是说,如果两种类型具有相同的内存结构(layout),我们可以将unsafe.Pointer当做桥梁,让这两种类型的指针相互转换,从而实现同一份内存拥有两种不同的解读方式。
比如说,[]byte和string其实内部的存储结构都是一样的,但 Go 语言的类型系统禁止他俩互换。如果借助unsafe.Pointer,我们就可以实现在零拷贝的情况下,将[]byte数组直接转换成string类型。
bytes := []byte{104, 101, 108, 108, 111}
p := unsafe.Pointer(&bytes) //强制转换成unsafe.Pointer,编译器不会报错
str := *(*string)(p) //然后强制转换成string类型的指针,再将这个指针的值当做string类型取出来
fmt.Println(str) //输出 "hello"
知道了unsafe.Pointer的作用,我们可以直接来看代码了:
func (v *Value) Store(x interface{}) {
if x == nil {
panic("sync/atomic: store of nil value into Value")
}
vp := (*ifaceWords)(unsafe.Pointer(v)) // Old value
xp := (*ifaceWords)(unsafe.Pointer(&x)) // New value
for {
typ := LoadPointer(&vp.typ)
if typ == nil {
// Attempt to start first store.
// Disable preemption so that other goroutines can use
// active spin wait to wait for completion; and so that
// GC does not see the fake type accidentally.
runtime_procPin()
if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
runtime_procUnpin()
continue
}
// Complete first store.
StorePointer(&vp.data, xp.data)
StorePointer(&vp.typ, xp.typ)
runtime_procUnpin()
return
}
if uintptr(typ) == ^uintptr(0) {
// First store in progress. Wait.
// Since we disable preemption around the first store,
// we can wait with active spinning.
continue
}
// First store completed. Check type and overwrite data.
if typ != xp.typ {
panic("sync/atomic: store of inconsistently typed value into Value")
}
StorePointer(&vp.data, xp.data)
return
}
}
大概的逻辑:
-
第5~6行 - 通过
unsafe.Pointer将现有的和要写入的值分别转成ifaceWords类型,这样我们下一步就可以得到这两个interface{}的原始类型(typ)和真正的值(data)。 -
从第7行开始就是一个无限 for 循环。配合
CompareAndSwap食用,可以达到乐观锁的功效。 -
第8行,我们可以通过
LoadPointer这个原子操作拿到当前Value中存储的类型。下面根据这个类型的不同,分3种情况处理。第一次写入(第9~24行) - 一个Value实例被初始化后,它的typ字段会被设置为指针的零值 nil,所以第9行先判断如果 -
typ是 nil 那就证明这个Value还未被写入过数据。那之后就是一段初始写入的操作: -
runtime_procPin()这是runtime中的一段函数,具体的功能我不是特别清楚,也没有找到相关的文档。这里猜测一下,一方面它禁止了调度器对当前 goroutine 的抢占(preemption),使得它在执行当前逻辑的时候不被打断,以便可以尽快地完成工作,因为别人一直在等待它。另一方面,在禁止抢占期间,GC 线程也无法被启用,这样可以防止 GC 线程看到一个莫名其妙的指向^uintptr(0)的类型(这是赋值过程中的中间状态)。 -
使用
CAS操作,先尝试将typ设置为^uintptr(0)这个中间状态。如果失败,则证明已经有别的线程抢先完成了赋值操作,那它就解除抢占锁,然后重新回到 for 循环第一步。 -
如果设置成功,那证明当前线程抢到了这个"乐观锁”,它可以安全的把
v设为传入的新值了(19~23行)。注意,这里是先写data字段,然后再写typ字段。因为我们是以typ字段的值作为写入完成与否的判断依据的。
atomic.Value Store 流程
-
第一次写入还未完成(第25~30行)- 如果看到
typ字段还是^uintptr(0)这个中间类型,证明刚刚的第一次写入还没有完成,所以它会继续循环,“忙等"到第一次写入完成。 -
第一次写入已完成(第31行及之后) - 首先检查上一次写入的类型与这一次要写入的类型是否一致,如果不一致则抛出异常。反之,则直接把这一次要写入的值写入到
data字段。 -
这个逻辑的主要思想就是,为了完成多个字段的原子性写入,我们可以抓住其中的一个字段,以它的状态来标志整个原子写入的状态。这个想法我在 TiDB 的事务实现中看到过类似的,他们那边叫
Percolator模型,主要思想也是先选出一个primaryRow,然后所有的操作也是以primaryRow的成功与否作为标志。嗯,果然是太阳底下没有新东西。如果没有耐心看代码,没关系,这儿还有个简化版的流程图:

读取(Load)操作
先上代码:
func (v *Value) Load() (x interface{}) { vp := (*ifaceWords)(unsafe.Pointer(v)) typ := LoadPointer(&vp.typ) if typ == nil || uintptr(typ) == ^uintptr(0) { // First store not yet completed. return nil } data := LoadPointer(&vp.data) xp := (*ifaceWords)(unsafe.Pointer(&x)) xp.typ = typ xp.data = data return }读取相对就简单很多了,它有两个分支:
1.如果当前的typ是 nil 或者^uintptr(0),那就证明第一次写入还没有开始,或者还没完成,那就直接返回 nil (不对外暴露中间状态)。
2. 否则,根据当前看到的typ和data构造出一个新的interface{}返回出去sync.pool
直接上例子
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// 用来统计实例真正创建的次数
var numCalcsCreated int32
// 创建实例的函数
func createBuffer() interface{} {
// 这里要注意下,非常重要的一点。这里必须使用原子加,不然有并发问题;
atomic.AddInt32(&numCalcsCreated, 1)
buffer := make([]byte, 1024)
return &buffer
}
func main() {
// 创建实例
bufferPool := &sync.Pool{
New: createBuffer,
}
// 多 goroutine 并发测试
numWorkers := 1024 * 1024
var wg sync.WaitGroup
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func() {
defer wg.Done()
// 申请一个 buffer 实例
buffer := bufferPool.Get()
_ = buffer.(*[]byte)
// 释放一个 buffer 实例
defer bufferPool.Put(buffer)
}()
}
wg.Wait()
fmt.Printf("%d buffer objects were created.\n", numCalcsCreated)
}
sync.Pool 不适合用于像 socket 长连接或数据库连接池?
因为,我们不能对 sync.Pool 中保存的元素做任何假设,以下事情是都可以发生的:
Pool 池里的元素随时可能释放掉,释放策略完全由 runtime 内部管理;
Get 获取到的元素对象可能是刚创建的,也可能是之前创建好 cache 住的。使用者无法区分;
Pool 池里面的元素个数你无法知道;
所以,只有的你的场景满足以上的假定,才能正确的使用 Pool 。sync.Pool 本质用途是
本文介绍了Go语言中的锁无关编程概念,强调了Lock-free算法的重要性,指出即使不使用锁,程序也可能不是lockfree的。文章详细解析了基于CAS(Compare and Swap)的原子操作,并通过示例解释了atomic.Value的写入(Store)和读取(Load)操作。同时,文章通过一个sync.Pool的例子说明了其在并发编程中的应用,以及使用限制和注意事项。
897

被折叠的 条评论
为什么被折叠?



