当前位置: 首页 > news >正文

Go并发编程之原子操作syncatomic

1、sync/atomic定义

Go语言标准库中的sync/atomic包提供了偏底层的原子内存原语(atomic memory primitives),用于实现同步算

法,其本质是将底层CPU提供的原子操作指令封装成了Go函数

使用sync/atomic提供的原子操作可以确保在任意时刻只有一个goroutine对变量进行操作,避免并发冲突。

使用sync/atomic需要特别小心,Go官方建议只有在一些偏底层的应用场景里才去使用sync/atomic,其它场景

建议使用channel或者sync包里的锁。

sync/atomic提供了5种类型的原子操作和1个Value类型。

1.1 5种类型的原子操作

  • swap操作:SwapXXX

  • compare-and-swap操作:CompareAndSwapXXX

  • add操作:AddXXX

  • load操作:LoadXXX

  • store操作:StoreXXX

这几种类型的原子操作只支持几个基本的数据类型。

add操作的Addxxx函数只支持int32, int64, uint32, uint64, uintptr这5种基本数据类型。

其它类型的操作函数只支持int32, int64, uint32, uint64, uintptr, unsafe.Pointer这6种基本数据类

型。

1.2 Value类型

由于上面5种类型的原子操作只支持几种基本的数据类型,因此为了扩大原子操作的使用范围,Go团队在1.4版本

的sync/atomic包中引入了一个新的类型Value。Value类型可以用来读取(Load)和修改(Store)任意类型的值。

Go 1.4版本的Value类型只有LoadStore 2个方法,Go 1.17版本又给Value类型新增了CompareAndSwap和

Swap这2个新方法。

2、sync/atomic实践

2.1 swap操作

swap操作支持int32, int64, uint32, uint64, uintptr, unsafe.Pointer这6种基本数据类型,对应有6个

swap操作函数。

func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)

swap操作实现的功能是把addr 指针指向的内存里的值替换为新值new,然后返回旧值old,是如下伪代码的原子

实现:

old = *addr
*addr = new
return old

我们拿SwapInt32举个例子:

// swap.go
package mainimport ("fmt""sync/atomic"
)func main() {var newValue int32 = 200var dst int32 = 100// 把dst的值替换为newValueold := atomic.SwapInt32(&dst, newValue)// 打印结果fmt.Println("old value: ", old, " new value:", dst)
}

上面程序的执行结果如下:

old value:  100  new value: 200

2.2 compare-and-swap操作

compare-and-swap(CAS)操作支持int32, int64, uint32, uint64, uintptr, unsafe.Pointer这6种基本

数据类型,对应有6个compare-and-swap操作函数。

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (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)

compare-and-swap操作实现的功能是先比较addr 指针指向的内存里的值是否为旧值old相等。

如果相等,就把addr指针指向的内存里的值替换为新值new,并返回true,表示操作成功。

如果不相等,直接返回false,表示操作失败。

compare-and-swap操作是如下伪代码的原子实现:

if *addr == old {*addr = newreturn true
}
return false

我们拿CompareAndSwapInt32举个例子:

// compare-and-swap.go
package mainimport ("fmt""sync/atomic"
)func main() {var dst int32 = 100oldValue := atomic.LoadInt32(&dst)var newValue int32 = 200// 先比较dst的值和oldValue的值,如果相等,就把dst的值替换为newValueswapped := atomic.CompareAndSwapInt32(&dst, oldValue, newValue)// 打印结果fmt.Printf("old value: %d, swapped value: %d, swapped success: %v\n", oldValue, dst, swapped)
}

上面程序的执行结果如下:

old value: 100, swapped value: 200, swapped success: true

2.3 add操作

add操作支持int32, int64, uint32, uint64, uintptr这5种基本数据类型,对应有5个add操作函数。

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)

add操作实现的功能是把addr 指针指向的内存里的值和delta做加法,然后返回新值,是如下伪代码的原子实现:

*addr += delta
return *addr

我们拿AddInt32举个例子:

// add.go
package mainimport ("fmt""sync""sync/atomic"
)var wg sync.WaitGroup// 多个goroutine并发读写sum,有并发冲突,最终计算得到的sum值是不准确的
func test1() {var sum int32 = 0N := 100wg.Add(N)for i := 0; i < N; i++ {go func(i int32) {sum += iwg.Done()}(int32(i))}wg.Wait()fmt.Println("func test1, sum=", sum)
}// 使用原子操作计算sum,没有并发冲突,最终计算得到sum的值是准确的
func test2() {var sum int32 = 0N := 100wg.Add(N)for i := 0; i < N; i++ {go func(i int32) {atomic.AddInt32(&sum, i)wg.Done()}(int32(i))}wg.Wait()fmt.Println("func test2, sum=", sum)
}func main() {test1()test2()
}

上面程序的执行结果如下:

func test1, sum= 4857
func test2, sum= 4950

注意:对于test1函数,你本地运行得到的结果可能和我的不一样,这个值并不是一个固定值。

2.4 load操作

load操作支持int32, int64, uint32, uint64, uintptr, unsafe.Pointer这6种基本数据类型,对应有6个

load操作函数。

func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)

load操作实现的功能是返回addr 指针指向的内存里的值,是如下伪代码的原子实现:

return *addr

我们拿LoadInt32举个例子:

// load.go
package mainimport ("fmt""sync/atomic"
)func main() {var sum int32 = 100result := atomic.LoadInt32(&sum)fmt.Println("result=", result)
}

上面程序的执行结果如下:

result= 100

2.5 store操作

store操作支持int32, int64, uint32, uint64, uintptr, unsafe.Pointer这6种基本数据类型,对应有6个

store操作函数。

func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)

store操作实现的功能是把addr 指针指向的内存里的值修改为val,是如下伪代码的原子实现:

*addr = val

我们拿StoreInt32举个例子:

// store.go
package mainimport ("fmt""sync/atomic"
)func main() {var sum int32 = 100var newValue int32 = 200// 将sum的值修改为newValueatomic.StoreInt32(&sum, newValue)// 读取修改后的sum值result := atomic.LoadInt32(&sum)// 打印结果fmt.Println("result=", result)
}

上面程序的执行结果如下:

result= 200

2.6 Value类型

Go标准库里的sync/atomic包提供了Value类型,可以用来并发读取和修改任何类型的值。

Value类型的定义如下:

// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {v any
}

Value类型有4个方法:CompareAndSwap, Load, Store, Swap,定义如下:

func (v *Value) CompareAndSwap(old, new any) (swapped bool)
func (v *Value) Load() (val any)
func (v *Value) Store(val any)
func (v *Value) Swap(new any) (old any)

下面是一个具体的示例:对map[string][string]类型做并发读写,为了避免加锁,使用value类型来读取和修改map[string][string]。

package mainimport ("sync/atomic""time"
)func loadConfig() map[string]string {// 从数据库或者文件系统中读取配置信息,然后以map的形式存放在内存里return make(map[string]string)
}func requests() chan int {// 将从外界中接收到的请求放入到channel里return make(chan int)
}func main() {// config变量用来存放该服务的配置信息var config atomic.Value// 初始化时从别的地方加载配置文件,并存到config变量里config.Store(loadConfig())go func() {// 每10秒钟定时拉取最新的配置信息,并且更新到config变量里for {time.Sleep(10 * time.Second)// 对应于赋值操作 config = loadConfig()config.Store(loadConfig())}}()// 创建协程,每个工作协程都会根据它所读取到的最新的配置信息来处理请求for i := 0; i < 10; i++ {go func() {for r := range requests() {// 对应于取值操作 c := config// 由于Load()返回的是一个interface{}类型,所以我们要先强制转换一下c := config.Load().(map[string]string)// 这里是根据配置信息处理请求的逻辑..._, _ = r, c}}()}
}

2.7 总结和注意事项

原子操作由底层CPU的原子操作指令支持。

2.8 CAS操作会有ABA问题

对于386处理器架构,64-bit原子操作函数使用了奔腾MMX或更新处理器型号才支持的CPU指令。对于非Linux的

ARM处理器架构,64-bit原子操作函数使用了ARMv6k core或更新处理器型号才支持的CPU指令。对于ARM, 386

和32-bit MIPS处理器架构,原子操作的调用者要对进行原子访问的64bit字(word)按照64-bit进行内存对齐。变量

或者分配的结构体、数组和切片的第1个字可以认为是64-bit对齐的。


http://www.mrgr.cn/news/65188.html

相关文章:

  • [ vulnhub靶机通关篇 ] 渗透测试综合靶场 Corrosion1 通关详解 (附靶机搭建教程)
  • 深度学习相关资料
  • 解决CentOS 7环境下VNC出现乱码问题
  • Python酷库之旅-第三方库Pandas(193)
  • 【django】django RESTFramework前后端分离框架快速入门
  • 2-143 基于matlab-GUI的脉冲响应不变法实现音频滤波功能
  • YOLO11论文 | 重要性能衡量指标、训练结果评价及分析及影响mAP的因素【发论文关注的指标】
  • 一文搞懂Apk的各种类型
  • Verilog HDL基础
  • 基于web的中小学成绩管理系统的设计与实现
  • Web-高校教务考试管理系统
  • 用户程序发出磁盘IO请求后,系统的处理流程
  • 供应SW1102集成氮化镓直驱的准谐振模式反激控制IC
  • Go:struct结构体和继承
  • 华为独家揭秘:AI时代产品经理成长宝典——《人工智能产品经理》168页首发
  • 遗传算法与深度学习实战(21)——使用差分搜索自动超参数优化
  • 供应SW1108P集成氮化镓直驱的高频准谐振IC
  • mac 打开访达快捷键
  • 【Linux内核设计思想】三、Linux内核的启动过程
  • ORACLE 删除archivelog日志
  • Nginx 的基础架构解析(上)
  • 正向代理模块
  • 从0开始搭建一个生产级SpringBoot2.0.X项目(八)SpringBoot 使用Redis
  • 【C++动态规划】2435. 矩阵中和能被 K 整除的路径|1951
  • 【天线&空中农业】草莓果实检测系统源码&数据集全套:改进yolo11-HSFPN
  • 不得不会的 VSCode 快捷键!让你的工作效率快速提升