Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

原子操作用法详解 #65

Open
kevinyan815 opened this issue Sep 19, 2021 · 0 comments
Open

原子操作用法详解 #65

kevinyan815 opened this issue Sep 19, 2021 · 0 comments

Comments

@kevinyan815
Copy link
Owner

kevinyan815 commented Sep 19, 2021

啥是原子操作呢?顾名思义,原子操作就是具备原子性的操作... 是不是感觉说了跟没说一样,原子性的解释如下:

一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity) 。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。

Go 语言提供了哪些原子操作

Go语言通过内置包sync/atomic提供了对原子操作的支持,其提供的原子操作有以下几大类:

  • 增减,操作方法的命名方式为AddXXXType
  • 载入,保证了读取到操作数前没有其他任务对它进行变更,操作方法的命名方式为LoadXXXType
  • 存储,有载入了就必然有存储操作,这类操作的方法名以Store开头
  • 比较并交换,操作方法以CompareAndSwap开通,也就是CAS,像Go的很多并发原语实现就是依赖的CAS操作
  • 交换,一组以Store开头的操作方法,这个简单粗暴一些,不比较直接交换,一般不怎么用这个操作

互斥锁跟原子操作的区别

  • 使用目的区别:互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。
  • 底层实现区别:Mutex操作系统的调度器实现,而atomic包中的原子操作则由底层硬件指令直接提供支持,这些指令在执行的过程中是不允许中断的,因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随CPU个数的增多而线性扩展。

原子操作增加、载入

func AtomicAdd() {
	var a int32 =  0
	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < 1000000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			atomic.AddInt32(&a, 1)
		}()
	}
	wg.Wait()
	timeSpends := time.Now().Sub(start).Nanoseconds()
	fmt.Printf("use atomic a is %d, spend time: %v\n", atomic.LoadInt32(&a), timeSpends)
}

完整源码,请访问atomic示例一

需要注意的是,所有原子操作方法的被操作数形参必须是指针类型,通过指针变量可以获取被操作数在内存中的地址,从而施加特殊的CPU指令,确保同一时间只有一个goroutine能够进行操作

比较并交换

该操作简称CAS (Compare And Swap)。 这类操作的前缀为 CompareAndSwap :

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
......

该操作在进行交换前首先确保被操作数的值未被更改,即仍然保存着参数 old 所记录的值,满足此前提条件下才进行交换操作CAS的做法类似操作数据库时常见的乐观锁机制。

atomic.Value保证任意值的读写安全

atomic包里提供了一套Store开头的方法,用来保证各种类型变量的并发写安全,避免其他操作读到了修改变量过程中的脏数据。

func StoreInt32(addr *int32, val int32)

func StoreInt64(addr *int64, val int64)

func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

...

这些操作方法的定义与上面介绍的那些操作的方法类似,就不再演示怎么使用这些方法了。值得一提的是如果你想要并发安全的设置一个结构体的多个字段,除了把结构体转换为指针,通过StorePointer设置外,还可以使用atomic包后来引入的atomic.Value,它在底层为我们完成了从具体指针类型到unsafe.Pointer之间的转换。

有了atomic.Value后,它使得我们可以不依赖于不保证兼容性的unsafe.Pointer类型,同时又能将任意数据类型的读写操作封装成原子性操作(中间状态对外不可见)。

atomic.Value类型对外暴露了两个方法:

  • v.Store(c) - 写操作,将原始的变量c存放到一个atomic.Value类型的v里。
  • c := v.Load() - 读操作,从线程安全的v中读取上一步存放的内容。

1.17 版本我看还增加了SwapCompareAndSwap方法。

下面是一个简单的例子演示atomic.Value的用法。

type Rectangle struct {
	length int
	width  int
}

var rect atomic.Value

func update(width, length int) {
	rectLocal := new(Rectangle)
	rectLocal.width = width
	rectLocal.length = length
	rect.Store(rectLocal)
}

func main() {
	wg := sync.WaitGroup{}
	wg.Add(10)
	// 10 个协程并发更新
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			update(i, i+5)
		}()
	}
	wg.Wait()
	_r := rect.Load().(*Rectangle)
	fmt.Printf("rect.width=%d\nrect.length=%d\n", _r.width, _r.length)
}

完整源代码请访问:atomic示例二

你也可以试试,不用atomic.Value,直接给Rectange类型的指针变量赋值,看看在并发条件下,两个字段的值是不是能跟预期的一样变成10和15。

总结

原子操作由底层硬件支持,而锁则由操作系统的调度器实现。锁应当用来保护一段逻辑,对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用atomic.Value封装好的实现。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant