atomic.AddInt64
介绍
原理
源码
看不到源码解释个勾八原理
源码里只有函数doc, 但是没有函数实现, 但是有一段注释
// AddInt64 atomically adds delta to *addr and returns the new value.
// Consider using the more ergonomic and less error-prone [Int64.Add] instead
// (particularly if you target 32-bit platforms; see the bugs section).
介绍了他的功能是原子性的对地址所指的数字 + delta, 需要注意一个问题, 在32位的平台上不应该使用, 会存在bug
在全局搜索过后, 一段特别的注释引起了我的注意
//go:linkname abigen_sync_atomic_AddInt64 sync/atomic.AddInt64
这条指令告诉编译器,虽然sync/atomic.AddInt64
函数定义在sync/atomic
包中,但是可以通过abigen_sync_atomic_AddInt64
这个别名在其他包中被直接调用,就好像它定义在那个包内一样。
好了, 我们已经找到了实际对应的源码位置, 但是奇怪的事情出现了, 此处依然没有实现
//go:linkname abigen_sync_atomic_AddInt64 sync/atomic.AddInt64
func abigen_sync_atomic_AddInt64(addr *int64, delta int64) (new int64)
在同级目录下, 存在这么一个文件
这就是他的实现源码了, 为了不同的平台的适配, 底层的实现使用了汇编, 在最后编译时在链接起来.
分析
函数签名
func abigen_sync_atomic_AddInt64(addr *int64, delta int64) (new int64)
栈帧布局
+----------------+
| addr |
+----------------+
| delta |
+----------------+
| 返回值 (new) |
+----------------+
变量对应
-
addr
: +0(FP) -
delta
: +8(FP) -
new
: +16(FP)
代码解释
TEXT sync∕atomic·AddInt64(SB), NOSPLIT|NOFRAME, $0-24
GO_ARGS
MOVQ $__tsan_go_atomic64_fetch_add(SB), AX
CALL racecallatomic<>(SB)
MOVQ add+8(FP), AX // convert fetch_add to add_fetch
ADDQ AX, ret+16(FP)
RET
-
载入函数
__tsan_go_atomic64_fetch_add
到寄存器AX中 -
执行函数
__tsan_go_atomic64_fetch_add
, 这一步执行的是fetch_add
在并发编程中,
fetch_add
和add_fetch
是两种常见的原子操作,用于实现对共享变量的原子加操作。它们的区别在于操作的顺序不同。-
fetch_add
:fetch_add
操作首先读取共享变量的当前值,然后将指定的值加到该变量上,并返回变量之前的值。换句话说,fetch_add
的顺序是先读取再相加。 -
add_fetch
:与fetch_add
相反,add_fetch
操作首先将指定的值加到共享变量上,然后返回变量的新值。换句话说,add_fetch
的顺序是先相加再返回。
举个简单例子,假设共享变量的初始值为0,执行以下操作:
-
fetch_add(3)
:首先读取变量的当前值为0,然后将3加到变量上,最后返回之前的值0。 -
add_fetch(3)
:首先将3加到变量上,变量的新值为3,然后返回新值3。
-
-
载入delta, 存放进AX寄存器, 需要注意的是此时的
ret+16(FP)
存放的是__tsan_go_atomic64_fetch_add的结果, 是未执行加操作前的数值, 在外面在执行一遍加法, 保证一致, 函数结束.
汇编分析
我们需要注意看绿色部分的上半边内容
- 将 0x3f (63) 载入CX寄存器
- XADDQ 进行原子性的加法, 并将结果存入CX中
- 将CX结果移入
0x10SP
, SP是调用栈, 偏移16个byte表示返回结果 (参见上方的栈帧布局) - 返回.
注意
为什么要有Lock?
参考 https://stackoverflow.com/questions/30130752/assembly-does-xadd-instruction-need-lock
如果没有Lock, XADDQ依然可以保证原子性, 但是只能保证在单个core上的原子性, 无法提供全局保证.
注意
__tsan_go_atomic64_fetch_add
函数是 Go 语言运行时在使用数据竞态检测(ThreadSanitizer,简称 TSan)时的内部函数。它的实现细节通常是隐藏的,因为这个函数是由运行时的系统库提供的,不是由 Go 语言本身直接实现的。TSan 是一个用于检测多线程程序中数据竞态的工具,它会在运行时拦截所有的内存操作以检测潜在的数据竞态问题。
当开启 -race
模式编译一个 Go 程序时,编译器和链接器会将程序连接到一个包含 TSan 逻辑的特殊版本的运行时。在这个模式下,TSan 对原子操作的处理和普通模式下是不同的。
__tsan_go_atomic64_fetch_add
的实现概览:
确切的实现代码不是公开的,因为这部分代码属于 Go runtime 和 TSan 工具的一部分。但是,理解其大致行为和作用是有帮助的。以下是可能的实现步骤:
- 拦截操作:当一个原子操作被执行时,TSan 会拦截这个操作。在 Go 的
-race
模式下,这意味着代替调用标准的sync/atomic
包函数,你的代码会调用特殊的 TSan 函数,如__tsan_go_atomic64_fetch_add
。 - **数据竞态检测:TSan 使用 shadow memory 来追踪每个内存地址的访问历史,包括哪个线程访问了该内存,以及它是读操作还是写操作。当执行
__tsan_go_atomic64_fetch_add
时,TSan 会检查该内存地址是否可能存在数据竞态。 - 执行原子操作:在确保没有数据竞态后,TSan 会安全地执行原子加法操作。这通常是通过调用底层硬件支持的原子指令完成的,以确保整个操作是不可分割的。
- 更新监控数据**:操作完成后,TSan 会更新其监控数据,记录这次内存访问,以便于未来的数据竞态检测。
- 返回值:
__tsan_go_atomic64_fetch_add
会返回原始内存位置上的值(即操作前的值)。
示例伪代码:
下面是一个简化的、可能的 __tsan_go_atomic64_fetch_add
的伪代码实现,用于说明其功能,而不是实际的代码:
int64 __tsan_go_atomic64_fetch_add(int64 *addr, int64 delta) {
// TSan 检测,确定没有数据竞态
tsan_check(addr);
// 执行原子加法操作,并获取原始值
int64 original_value = atomic_fetch_add(addr, delta);
// 更新 TSan 监控数据
tsan_update(addr);
// 返回操作前的原始值
return original_value;
}
在这个伪代码中,tsan_check
用于确保当前的内存访问不会导致数据竞态,atomic_fetch_add
是底层的原子操作,而 tsan_update
会记录此次操作,以便于未来的监控。
请注意,实际的 __tsan_go_atomic64_fetch_add
函数实现更加复杂,因为它必须与 TSan 的其他部分交互,以实现完整的数据竞态检测和报告。实际上,你通常无法直接查看或修改这个函数的实现,因为它是由 Go 运行时和 TSan 的 C/C++ 代码实现的,并且在运行时被动态链接。
atomic.AddInt64
的使用还是比较简单的, 只需要传入一个指针, 同时指定delta就可以
atomic.AddInt64(&i, 64)
标签:AddInt64,tsan,TSan,add,源码,atomic,fetch
From: https://www.cnblogs.com/pDJJq/p/18104799/atomicaddint64-z1k1qgh