1 关于atomic包
atomic包位于sync目录下。它提供了低级原子内存原语,用于实现同步算法。
这些函数需要非常小心地使用才能正确地工作。
除非是特殊的、低级的应用程序,否则最好使用通道或[sync]包来进行同步:通过通信共享内存;不要通过共享内存进行通信。
上面这段注释是源码中的注释,尽管推荐使用通道或[sync]包来进行同步,但atomic包仍然具有一些优势和适用场景,包括:
- 原子性操作:atomic包提供了一些原子性操作,如Swap、Add和CompareAndSwap等,能够在不需要锁的情况下进行原子性操作,避免了竞态条件。
- 内存模型:atomic操作遵循Go内存模型的规范,保证了在不同goroutines之间的内存同步和一致性。
- 性能:相比使用锁和通道进行同步,atomic操作通常具有更低的开销和更高的性能,特别是在需要频繁进行原子性操作的情况下。
- 低级控制:atomic包提供了对内存的低级原子操作,允许开发者更精细地控制并发程序的同步和状态管理。
- 特定场景:在特定的性能要求和同步需求下,atomic操作可以是更合适的选择,特别是在需要对共享内存进行细粒度的原子操作时。
2 atomic包中的类型
atomic包中定义了如下几个类型:
- Bool:表示原子的布尔类型,零值为false
- Int32:表示原子的int32类型,零值为0
- Int64:表示原子的int64类型,零值为0
- Uint32:表示原子的uint32类型,零值为0
- Uint64:表示原子的uint64类型,零值为0
- Pointer:表示T (泛型)类型的原子指针。零值是 nil T。
- Uintptr:表示原子的uintptr类型,零值为0
- Value:Value 提供一致类型值的原子加载和存储。 Value 的零值从 Load 返回 nil。一旦 Store 被调用,Value 就不能被复制。
3 atomic包中的方法
atomic包提供了一系列原子操作方法:
- Load : 原子地加载并返回存储的值
- Store:原子地存储值
- Add:原子地加上一个增量,并返回新的值。
- Swap:原子地存储新值,并返回旧值。
- CompareAndSwap:原子地进行比较和交换操作:
- 如果原值与指定值(第一个参数)相等,就存储新值(第二个参数),并返回true。
- 如果原值与指定值(第一个参数)不相等,直接返回false。
上述的方法,除了Add方法只支持Int32、Int64、Uint32、Uint64、Uintptr外,其他四个方法支持章节2中的所有类型。
4 使用示例
4.1 使用Int64实现线程安全的计算
先看一个线程不安全的实现:
func main() {
startTime := time.Now()
waitGroup := sync.WaitGroup{}
for i := 0; i < 10; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
Add(int64(i))
}()
}
waitGroup.Wait()
fmt.Println(num)
fmt.Printf("use time:%dms", time.Now().Sub(startTime)/time.Millisecond)
}
func Add(x int64) {
for i := 0; i < 1000000; i++ {
num = num + x
}
}
多次执行这段代码会发现,执行结果不唯一,且大部分时候是小于期望的结果:45000000的。将上述的代码改造成使用atomic.Int64:
func main() {
startTime := time.Now()
waitGroup := sync.WaitGroup{}
for i := 0; i < 10; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
Add(int64(i))
}()
}
waitGroup.Wait()
fmt.Println(num.Load())
fmt.Printf("use time:%dms", time.Now().Sub(startTime)/time.Millisecond)
}
func Add(x int64) {
for i := 0; i < 1000000; i++ {
num.Add(x)
}
}
执行改造后的代码可以发现,每次都能得到期望结果:45000000.