原子操作
atomic包
atomic 提供的原子操作能够确保任一时刻只有一个 goroutine 对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。
atomic常见操作有:
- 增减
- 载入
- 比较并交换
- 交换
- 存储
增减操作
在 Go 语言的 sync/atomic
包中,提供了一系列原子操作函数,其中包括增减操作。这些函数能够确保在并发环境中对变量进行安全地增减操作,避免了竞态条件和数据竞争问题。下面详细介绍了 sync/atomic
包中的增减操作:
-
Add 函数系列:
-
AddInt32
、AddInt64
、AddUint32
、AddUint64
、AddUintptr
:这些函数用于对对应类型的变量进行增加操作,并返回增加前的值。函数原型如下:1 2 3 4 5
func AddInt32(addr *int32, delta int32) (new int32) func AddInt64(addr *int64, delta int64) (new int64) func AddUint32(addr *uint32, delta uint32) (new uint32) func AddUint64(addr *uint64, delta uint64) (new uint64) func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
-
-
Sub 函数系列:
SubInt32
、SubInt64
:这些函数用于对对应类型的变量进行减少操作,并返回减少前的值。函数原型如下:1 2
func SubInt32(addr *int32, delta int32) (new int32) func SubInt64(addr *int64, delta int64) (new int64)
-
增减操作注意事项:
-
这些增减操作都是原子的,即在执行过程中不会被中断。
-
这些函数都接受指向对应类型的变量的指针作为参数。
第一个参数必须是指针类型的值,通过指针变量可以获取被操作数在内存中的地址,确保同一时间只有一个
goroutine
能够进行操作。 -
参数
delta
是要增加或减少的值。 -
这些函数会返回操作前的值,因此可以用于一些需要获取旧值的场景。
-
-
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
package main import ( "fmt" "sync/atomic" ) func main() { var count int32 = 0 // 增加操作 old := atomic.AddInt32(&count, 5) fmt.Println("Old value:", old) // 输出: Old value: 0 fmt.Println("New value:", count) // 输出: New value: 5 // 减少操作 old = atomic.SubInt32(&count, 2) fmt.Println("Old value:", old) // 输出: Old value: 5 fmt.Println("New value:", count) // 输出: New value: 3 }
多协程变量累加
在Go语言中,可以使用锁和原子操作来实现多个goroutine
对同一个变量进行累加操作。
1.加锁的方式
首先,我们先来看如何使用锁实现:
|
|
2.原子操作
接下来,我们来看如何使用原子操作来实现:
|
|
这两种方式都可以实现多个goroutine对同一个变量进行累加操作,但是使用原子操作会更加高效,因为它不需要加锁,而是通过硬件原语来确保操作的原子性。
载入操作
在 Go 语言的 sync/atomic
包中,载入操作指的是用于原子加载变量值的函数。这些函数可以确保在并发环境中安全地获取变量的当前值,避免了竞态条件和数据竞争问题。下面详细介绍了 sync/atomic
包中的载入操作:
-
Load 函数系列:
LoadInt32
、LoadInt64
、LoadUint32
、LoadUint64
、LoadUintptr
:这些函数用于加载对应类型的变量的值,并返回当前值。函数原型如下:1 2 3 4 5
func LoadInt32(addr *int32) (val int32) func LoadInt64(addr *int64) (val int64) func LoadUint32(addr *uint32) (val uint32) func LoadUint64(addr *uint64) (val uint64) func LoadUintptr(addr *uintptr) (val uintptr)
-
载入操作注意事项:
- 这些载入操作都是原子的,即在执行过程中不会被中断。
- 这些函数都接受指向对应类型的变量的指针作为参数。
- 这些函数会返回当前变量的值,因此可以用于获取变量的当前状态。
- 这些函数仅仅是读取变量的值,并不会修改变量的内容。
-
示例代码:
1 2 3 4 5 6 7
func main() { var count int32 = 10 // 载入操作 val := atomic.LoadInt32(&count) fmt.Println("Current value:", val) // 输出: Current value: 10 }
比较并交换
在 Go 语言的 sync/atomic
包中,比较并交换(CAS,Compare-And-Swap)操作是一种常见的并发原子操作。CAS 操作用于在并发环境中安全地更新变量的值,其原理是**先比较变量的当前值和预期值,如果相等,则将变量的新值写入;如果不相等,则不做任何操作。**CAS 操作能够避免竞态条件和数据竞争问题。下面详细介绍了 sync/atomic
包中的比较并交换操作:
-
CompareAndSwap 函数系列:
CompareAndSwapInt32
、CompareAndSwapInt64
、CompareAndSwapUint32
、CompareAndSwapUint64
、CompareAndSwapUintptr
:这些函数用于对对应类型的变量进行比较并交换操作。函数原型如下:1 2 3 4 5
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
-
比较并交换操作注意事项:
- 这些比较并交换操作都是原子的,即在执行过程中不会被中断。
- 这些函数都接受指向对应类型的变量的指针作为第一个参数。
- 参数
old
是预期值,参数new
是要更新的新值。 - 如果变量的当前值等于预期值
old
,则将新值new
写入,并返回true
;否则不做任何操作,并返回false
。 - 这些函数通常用于实现自旋锁、无锁数据结构等并发算法中。
-
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12
func main() { var count int32 = 10 // 比较并交换操作 swapped := atomic.CompareAndSwapInt32(&count, 10, 20) fmt.Println("Swapped:", swapped) // 输出: Swapped: true fmt.Println("New value:", count) // 输出: New value: 20 swapped = atomic.CompareAndSwapInt32(&count, 15, 30) fmt.Println("Swapped:", swapped) // 输出: Swapped: false fmt.Println("New value:", count) // 输出: New value: 20 (未更新) }
交换
在 Go 语言的 sync/atomic
包中,交换操作是一种原子操作,用于在并发环境中安全地交换变量的值。交换操作可以将变量的新值存储到指定变量中,并返回原来的值,同时保证操作的原子性,避免了竞态条件和数据竞争问题。下面详细介绍了 sync/atomic
包中的交换操作:
-
Swap 函数系列:
-
SwapInt32
、SwapInt64
、SwapUint32
、SwapUint64
、SwapUintptr
:这些函数用于对对应类型的变量进行交换操作。函数原型如下:1 2 3 4 5
func SwapInt32(addr *int32, new int32) (old int32) func SwapInt64(addr *int64, new int64) (old int64) func SwapUint32(addr *uint32, new uint32) (old uint32) func SwapUint64(addr *uint64, new uint64) (old uint64) func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
-
-
交换操作注意事项:
- 这些交换操作都是原子的,即在执行过程中不会被中断。
- 这些函数都接受指向对应类型的变量的指针作为第一个参数。
- 参数
new
是要存储到变量中的新值。 - 这些函数会将变量的新值存储到指定变量中,并返回原来的值。
- 这些函数常用于更新变量值的同时获取变量的旧值。
-
示例代码:
1 2 3 4 5 6 7 8
func main() { var count int32 = 10 // 交换操作 old := atomic.SwapInt32(&count, 20) fmt.Println("Old value:", old) // 输出: Old value: 10 fmt.Println("New value:", count) // 输出: New value: 20 }
存储
在 Go 语言的 sync/atomic
包中,存储操作用于将新值存储到变量中,并确保该操作是原子的,即在执行过程中不会被中断,以避免竞态条件和数据竞争问题。存储操作是一种常见的原子操作,能够帮助开发者在并发环境中安全地更新变量的值。下面详细介绍了 sync/atomic
包中的存储操作:
-
Store 函数系列:
-
StoreInt32
、StoreInt64
、StoreUint32
、StoreUint64
、StoreUintptr
:这些函数用于将对应类型的新值存储到指定变量中。函数原型如下:1 2 3 4 5
func StoreInt32(addr *int32, val int32) func StoreInt64(addr *int64, val int64) func StoreUint32(addr *uint32, val uint32) func StoreUint64(addr *uint64, val uint64) func StoreUintptr(addr *uintptr, val uintptr)
-
-
存储操作注意事项:
- 这些存储操作都是原子的,即在执行过程中不会被中断。
- 这些函数都接受指向对应类型的变量的指针作为第一个参数。
- 参数
val
是要存储到变量中的新值。 - 这些函数会将新值存储到指定变量中,覆盖掉原来的值。
-
示例代码:
1 2 3 4 5 6 7
func main() { var count int32 = 10 // 存储操作 atomic.StoreInt32(&count, 20) fmt.Println("New value:", count) // 输出: New value: 20 }
StoreInt32
仅用于存储新值,不返回旧值;SwapInt32
在存储新值的同时返回旧值。
atomic.Value
atomic.Value
是 Go 语言标准库 sync/atomic
包中的一个数据结构,用于在并发环境中安全地存储和加载任意类型的值。相比于普通的变量,atomic.Value
提供了一种更为灵活和安全的方式来管理共享状态,特别适用于需要在多个 goroutine 之间传递和共享数据的情况。下面是对 atomic.Value
的详细介绍:
-
原子值(Atomic Value):
atomic.Value
是一个原子值,可以存储和加载任意类型的值,而不需要担心并发访问导致的竞态条件和数据竞争问题。- 它提供了
Load
和Store
方法用于原子加载和存储操作,以及Swap
方法用于原子交换操作。
-
零值(Zero Value):
atomic.Value
的零值是一个空的原子值,即初始状态下不包含任何值。- 零值不能通过
Load
方法加载,因为没有存储值。加载操作将会返回nil
。
-
加载操作(Load):
Load
方法用于原子加载操作,返回当前存储的值。- 如果在调用
Load
方法之前没有调用Store
方法存储值,那么Load
方法将返回nil
。 - 加载操作是原子的,即在执行过程中不会被中断。
-
存储操作(Store):
Store
方法用于原子存储操作,存储指定的值到atomic.Value
中。- 存储操作是原子的,即在执行过程中不会被中断。
- 存储操作将替换原有的值,因此后续加载操作将返回最新存储的值。
-
交换操作(Swap):
Swap
方法用于原子交换操作,将新值存储到atomic.Value
中,并返回原来的旧值。- 交换操作是原子的,即在执行过程中不会被中断。
- 这个操作可以用于更新变量的值的同时获取变量的旧值。
-
适用场景:
atomic.Value
适用于需要在多个 goroutine 之间安全地传递和共享数据的场景。- 它特别适用于需要频繁地更新共享状态的情况,而且不需要使用复杂的同步机制来保证并发安全性。
-
注意事项:
- 虽然
atomic.Value
提供了原子操作的支持,但是它并不是万能的。在某些情况下,可能需要考虑更高级的同步机制来保证数据的一致性和正确性。 - 在使用
atomic.Value
时,需要仔细考虑并发访问的场景和操作序列,以确保数据的一致性和正确性。
- 虽然
示例
|
|
在这个示例中,我们首先创建了一个 atomic.Value
,用于存储整数值。然后启动了两个 goroutine,在第一个 goroutine 中存储了整数值 42 到共享值中,在第二个 goroutine 中加载了共享值中的数据。由于 atomic.Value
提供了原子操作,因此无需额外的锁机制来保证并发安全性,两个 goroutine 可以安全地在不同的时间点对共享值进行存取操作。
互斥锁与原子操作
在并发编程里,Go语言sync
包里的同步原语Mutex
是我们经常用来保证并发安全的,但是他跟atomic
包在使用目的和底层实现上都不一样。
使用目的:互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。
底层实现:Mutex
由操作系统的调度器实现,而atomic
包中的原子操作则由底层硬件指令直接提供支持,这些指令在执行的过程中是不允许中断的,因此原子操作可以在lock-free
的情况下保证并发安全,并且它的性能也能做到随CPU
个数的增多而线性扩展。
atomic的底层实现参考:Golang 源码分析系列之 atomic 底层实现。
对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势。