< 返回版块

kipade 发表于 2025-12-01 08:14

各位大佬,请教一个问题:我有这么一个多线程的场景,有一个线程循环在运行时通过不断地检测某bool变量来决定线程是否退出,当该变量为false时线程就退出,外部想要停止这个线程时,直接将这个全局的变量置为false即可让它退出。这个场景非常简单,一个bool变量或者一个数值变量即可完成控制,完全不需要对这个变量进行加锁保护,也不需要考虑这个变量的存储一致性。 请问,在Rust中除了Arc之外真的就没有像描述的那样简单地通过简单变量来达成吗? 好像直接用Arc也不行。如果直接用简单变量,是否一定会UB?

评论区

写评论
amazinq 2025-12-01 14:40

这地方写的不太对,普通变量下,编译器大概率是优化成:

let stop = STOP; // 编译器假设了该值只读,优化读到循环外增加loop效率
if stop { // 只读一次,无法感知后续的新值变化
    return;
}
loop {
    // todo
}

--
👇
比如不会突然优化成这样:

let stop = STOP;
loop {
    if stop {
        break;
    }
    // todo
}
amazinq 2025-12-01 14:21

有兴趣可看一下前面提到过的那篇关于原子和锁的文章,写的挺好的,看完收获都不少,这里是中文版的

amazinq 2025-12-01 14:16

感觉你陷入了某种莫名纠结,需要理一下Atomic的逻辑关系

loop {
    let stop = STOP; // load STOP; 模拟一次全局共享变量读
    if stop {
        break;
    }
    // todo
}

无论STOP的实现,是原子类型的AtomicBool::load(&STOP, Relaxed)读,亦或是普通变量的bool读,在编译完成之后(不考虑破坏性优化),都是一条最基础的加载指令mov STOP, reg,这两者编译结果在执行上没有任何实质上的区别。

你可能担心Atomic影响内存排序效率,但是Relaxed已经说明了它不会对其他指令添加任何额外的影响。

现在说回普通变量和对应原子类型(只能是Relaxed级别,因为只有它不影响排序)的区别:它们只在源码层面含义不同,影响的是编译时的类型检查和对应优化。在大多支持原子操作的平台上(也许有例外,没有研究),二者的编译产物是完全一致的(常用asm输出测试rustc -O --emit=asm --crate-type=lib src/lib.rs配合#[unsafe(no_mangle)],可以查看函数编译后的汇编指令)。

Atomic类型对比普通类型,语义上至少就多了内部可变性的属性,这也是很关键会影响编译的语义属性,让编译器知道这个值可能突然发生变更,从而提供了语义上的正确性保证。比如不会突然优化成这样:

let stop = STOP; // 编译器假设了该值只读,优化读到循环外增加loop效率
loop {
    if stop {  // 寄存器值,每次都从寄存器中判断旧值,完全无法感知到新值变动
        break;
    }
    // todo
}

--
👇
kipade: 从语言层面,的确是,Atomic毕竟是对执行总线有影响的。只能折中了

作者 kipade 2025-12-01 13:27

从语言层面,的确是,Atomic毕竟是对执行总线有影响的。只能折中了

--
👇
amazinq: 除此之外,关于数据不需要保证,还有前面的不考虑存储一致性,在底层语言中似乎已经意味着UB了。因为目前在用Java做游戏,所以感觉这些很像是Java里多线程访问/修改非volatile的变量,只为了省去java里的volatile代价?这里完全不同的是,JVM层面的变量读写,并不是直接对应CPU层面的读写,因为JVM有自己的内存布局,也有完全独立的字节码执行机制,所以即使未声明volatile变量在多线程访问时,在经过一段未知的内存同步时间后,依然是可以正确访问到最新变量值的,这种具有未知延迟的变更感知虽然能用,但终究是非常规用法,没有必要。 对rust编译器就不会去管这些,只要符合代码自身的语义要求,编译器可以做出任何可能有利的假设并执行优化,然而优化结果可不一定是我们期望的,所以即使是不使用Atomic类型,最终我们的实现还是要跟Atomic的逻辑一样的。

--
👇
amazinq: 实际上,Atomic+Relaxed组合在常见平台上是没有任何额外代价的(原子与锁-理解处理器-读写操作),它们跟普通变量行为完全一致,因为普通读写指令已经自带了原子属性,这由硬件架构决定的。

在Rust这类底层编译语言里,程序一旦编译完成,cpu要执行的二进制指令就是可确定的,所以对共享变量的读编译结果要么是读一次放到寄存器缓存起来,后续只用寄存器的旧值(显然我们不希望被编译成这样子),要么就只能是每次重新读(经CPU内存架构),所以这也就无所谓什么额外成本代价了,因为这是业务逻辑所必需的。

--
👇
kipade: 事实上,这只是一个简单场景的例子。而实际应用中有很多类似的场景,比如统计,这些数据有一个共同点就是高频,数据不需要保证,即使是Atomic也是有代价的,对业务的影响是否可见取决于频次。如果真没有更高级的玩法,那恐怕unsafe就成唯一解了......这不合理,对一个简单类型的访问无论如何也不应该UB,又不需要解引用......

amazinq 2025-12-01 12:43

除此之外,关于数据不需要保证,还有前面的不考虑存储一致性,在底层语言中似乎已经意味着UB了。因为目前在用Java做游戏,所以感觉这些很像是Java里多线程访问/修改非volatile的变量,只为了省去java里的volatile代价?这里完全不同的是,JVM层面的变量读写,并不是直接对应CPU层面的读写,因为JVM有自己的内存布局,也有完全独立的字节码执行机制,所以即使未声明volatile变量在多线程访问时,在经过一段未知的内存同步时间后,依然是可以正确访问到最新变量值的,这种具有未知延迟的变更感知虽然能用,但终究是非常规用法,没有必要。 对rust编译器就不会去管这些,只要符合代码自身的语义要求,编译器可以做出任何可能有利的假设并执行优化,然而优化结果可不一定是我们期望的,所以即使是不使用Atomic类型,最终我们的实现还是要跟Atomic的逻辑一样的。

--
👇
amazinq: 实际上,Atomic+Relaxed组合在常见平台上是没有任何额外代价的(原子与锁-理解处理器-读写操作),它们跟普通变量行为完全一致,因为普通读写指令已经自带了原子属性,这由硬件架构决定的。

在Rust这类底层编译语言里,程序一旦编译完成,cpu要执行的二进制指令就是可确定的,所以对共享变量的读编译结果要么是读一次放到寄存器缓存起来,后续只用寄存器的旧值(显然我们不希望被编译成这样子),要么就只能是每次重新读(经CPU内存架构),所以这也就无所谓什么额外成本代价了,因为这是业务逻辑所必需的。

--
👇
kipade: 事实上,这只是一个简单场景的例子。而实际应用中有很多类似的场景,比如统计,这些数据有一个共同点就是高频,数据不需要保证,即使是Atomic也是有代价的,对业务的影响是否可见取决于频次。如果真没有更高级的玩法,那恐怕unsafe就成唯一解了......这不合理,对一个简单类型的访问无论如何也不应该UB,又不需要解引用......

amazinq 2025-12-01 12:04

实际上,Atomic+Relaxed组合在常见平台上是没有任何额外代价的(原子与锁-理解处理器-读写操作),它们跟普通变量行为完全一致,因为普通读写指令已经自带了原子属性,这由硬件架构决定的。

在Rust这类底层编译语言里,程序一旦编译完成,cpu要执行的二进制指令就是可确定的,所以对共享变量的读编译结果要么是读一次放到寄存器缓存起来,后续只用寄存器的旧值(显然我们不希望被编译成这样子),要么就只能是每次重新读(经CPU内存架构),所以这也就无所谓什么额外成本代价了,因为这是业务逻辑所必需的。

--
👇
kipade: 事实上,这只是一个简单场景的例子。而实际应用中有很多类似的场景,比如统计,这些数据有一个共同点就是高频,数据不需要保证,即使是Atomic也是有代价的,对业务的影响是否可见取决于频次。如果真没有更高级的玩法,那恐怕unsafe就成唯一解了......这不合理,对一个简单类型的访问无论如何也不应该UB,又不需要解引用......

作者 kipade 2025-12-01 11:31

事实上,这只是一个简单场景的例子。而实际应用中有很多类似的场景,比如统计,这些数据有一个共同点就是高频,数据不需要保证,即使是Atomic也是有代价的,对业务的影响是否可见取决于频次。如果真没有更高级的玩法,那恐怕unsafe就成唯一解了......这不合理,对一个简单类型的访问无论如何也不应该UB,又不需要解引用......

--
👇
amazinq: 直接用简单变量,不一定会UB,但是想要不UB就必须人为做出很多保证,编译器没法帮我们检查和控制,而且涉及unsafe操作,得不偿失。Atomic类型就已经是封装好的即用的共享变量了,大部分场景下Relaxed内存序就够用了

我心飞翔 2025-12-01 11:16

这种情况我一般都是通过 channel 实现

我心飞翔 2025-12-01 11:14

AtomicBool

amazinq 2025-12-01 10:49

直接用简单变量,不一定会UB,但是想要不UB就必须人为做出很多保证,编译器没法帮我们检查和控制,而且涉及unsafe操作,得不偿失。Atomic类型就已经是封装好的即用的共享变量了,大部分场景下Relaxed内存序就够用了

mag1c1an1 2025-12-01 09:45

AtomicBool

1 共 11 条评论, 1 页