多线程无同步/原子操作访问uint64_t,是否会读到非预期范围值?
会出现吗?答案是:在特定场景下完全可能读到从未写入过的值,但你的0-100递增场景可能暂时不会暴露问题
先给你拆解清楚:
核心原因:CPU对64位内存操作的原子性支持差异
不是所有CPU架构都能一次性完成64位数据的读取或写入。比如32位ARM、32位x86这类架构,会把uint64_t的读写拆成两次32位的内存操作。这就给竞态条件留了口子——如果读线程刚好卡在两次32位操作之间读取,就会得到一个"拼凑"出来的值:一半是旧值,一半是新值。
你的0-100递增场景为什么没出问题?
因为0到100的数值都远小于2^32(4294967296),所以uint64_t的高32位始终是0,写入操作只会修改低32位。这时候:
- 在32位架构上,写入只是单一的32位存储操作,读线程要么读到完整的旧值(低32位旧+高32位0),要么完整的新值(低32位新+高32位0),不会出现奇怪的中间值;
- 在64位架构上,CPU原生支持64位原子读写,读线程只能读到旧值或新值。
所以你当前的测试场景下,确实很难观测到异常,但这只是巧合,不是必然安全。
什么时候会读到从未写入过的值?
当变量的值需要跨32位边界时,比如从4294967295(0xFFFFFFFF)递增到4294967296(0x100000000):
- 在32位架构上,写入操作会拆成两步:先把低32位从
0xFFFFFFFF改成0x00000000,再把高32位从0x00000000改成0x00000001; - 如果读线程在这两步之间读取,就会得到
0x0000000000000000——这个值既不是旧值也不是新值,完全是从未写入过的"畸形值",而且远远超出了你预期的范围。
另外,即使是64位架构,如果编译器对写入操作做了奇怪的优化(比如把64位变量拆成两个32位变量处理),也可能出现类似问题,只是概率极低。
为什么你现在没看到异常?
大概率是这几个原因:
- 你用的是64位CPU/系统,原生支持64位原子操作;
- 测试的数值范围太小,没触及32位边界;
- 竞态条件是偶发的,需要成千上万次测试才可能触发一次。
要不要处理?必须要!
哪怕当前场景没问题,也绝对不能依赖这种"巧合":
- 代码移植到32位架构就会立刻出问题;
- 后续如果修改逻辑,让变量值超出低32位范围,会出现难以排查的偶发bug;
- 编译器版本或优化选项变化,也可能打破当前的"安全"状态。
正确的做法是用原子操作来同步,比如C++里的std::atomic<uint64_t>,或者C语言里的_Atomic uint64_t,确保读写操作的原子性和内存可见性,彻底杜绝竞态条件。
内容的提问来源于stack exchange,提问作者avatli
相关产品推荐
相关产品推荐

