firemail
标题:
使用 store和 memory_order_release
[打印本页]
作者:
Qter
时间:
昨天 15:56
标题:
使用 store和 memory_order_release
好的,根据您提供的图片信息,这两段代码的核心区别在于
线程安全性
和
内存可见性
。这体现了普通变量操作与原子变量操作在并发编程中的根本不同。下面为您详细解释:
核心区别总结
特性
左侧代码(普通赋值)
右侧代码(使用 store和 memory_order_release)
原子性
不具备
。操作可能不是一步完成的,在多线程环境下可能被中断,导致数据竞争。
具备
。store操作是原子的,保证赋值操作一次性完成,不会被其他线程打断。
内存顺序
无保证
。编译器或CPU可能会为了优化而重排指令顺序,导致意外的执行结果。
有保证
。使用 std::memory_order_release建立了明确的内存顺序约束。
内存可见性
无保证
。一个线程对变量的修改,可能不会立即被其他线程看到。
有保证
。确保本线程中所有在 store操作之前的读写操作,对其他以 acquire方式读取该原子的线程是可见的。
详细解释
1. 原子性 - 防止数据竞争
左侧 m_pause = false;
这是一个简单的赋值语句。在高级语言中它是一行代码,但在底层可能对应多条CPU指令。
如果多个线程同时读写 m_pause(例如,一个线程要设置它为 true来暂停,另一个线程要检查它的值),可能会发生
数据竞争
。结果是不可预测的,可能导致程序崩溃、死锁或出现诡异的行为。
右侧 m_pause.store(false, std::memory_order_release);
这里 m_pause应该是一个 std::atomic<bool>类型的变量。
.store()是一个原子写操作。它保证无论底层硬件如何,赋值操作都是作为一个不可分割的单元执行的。这意味着不会出现一个线程读到一个“修改了一半”的无效值,从而彻底避免了数据竞争。
2. 内存顺序与可见性 - 控制操作顺序
这是更深入、更重要的区别。
左侧:无顺序保证
编译器和CPU为了提升性能,可能会对指令进行重排序,只要在单线程环境下最终结果不变。但在多线程环境下,这会导致严重问题。
举例
:你的代码可能像下面这样:// 线程 1data_ready = true; // 普通赋值,无内存顺序保证important_data = 42; // 设置一些重要的数据// 线程 2if (data_ready) { // 普通读取 print(important_data); // 可能会打印出未初始化的值!}
编译器或CPU可能会重排线程1的两行代码,先执行 important_data = 42;再执行 data_ready = true;在单线程下没问题。但如果重排后,线程2可能看到了 data_ready为 true,但却看不到 important_data已经被设置为42,从而打印出错误的值。
右侧:使用 std::memory_order_release
std::memory_order_release(释放操作)建立了一种“同步”关系。它通常与另一个线程中的 load(memory_order_acquire)(获取操作)配对使用。
规则
:在 store(memory_order_release)
之前
的所有内存读写操作(包括普通变量),其结果都对另一个成功 load(memory_order_acquire)到该原子变量新值的线程
可见
。
效果
:它就像一个单向的“内存屏障”,防止“释放操作”之前的任何读写操作被重排到“释放操作”
之后
。这确保了当另一个线程看到 m_pause变为 false时,它也能看到本线程在修改 m_pause之前所做出的所有其他更改。
结论与类比
您可以这样理解:
左侧普通赋值
:像是在嘈杂的集市上,你隔着很远对同伴喊话。他可能听不清、听错了,或者听到你后面说的话却漏掉了前面的话。
(不可靠)
右侧原子操作 with release
:像是你和一个同伴约定好,你把一封信(m_pause的新值)放在一个特定的信箱里。你放信的这个动作(store-release)本身是原子的,不会被干扰。同时,你确保在放信
之前
,已经把信里提到的所有相关文件都整理好放在了桌上。当你的同伴从信箱里取到信时(load-acquire),他不仅知道了信的内容,也
一定能看到
你之前整理好的所有文件。
(可靠、有序)
因此,右侧代码是一种为多线程环境设计的、安全的编程方式,而左侧代码在并发环境下是危险且不正确的。
作者:
Qter
时间:
昨天 16:04
m_pause.compare_exchange_strong(expected_pause, false, std::memory_order_acq_rel) 这行代码实现了一个原子性的比较与交换操作,是多线程编程中实现无锁数据结构或同步原语的核心技术之一。下面这个表格能帮你快速理解它的各个组成部分:
组成部分 含义与作用
m_pause 一个 std::atomic<bool> 或类似的原子布尔变量,通常作为线程间的控制标志。
expected_pause 期望值(传入引用)。函数会先判断 m_pause 的当前值是否等于 expected_pause。
false 期望成立时设置的值。如果 m_pause 等于 expected_pause,则将其设置为 false。
std::memory_order_acq_rel 内存顺序。保证操作前后的指令不会被重排越过此操作,并确保修改结果对其他线程可见。
返回值 布尔值。成功交换返回 true;否则返回 false,并且 expected_pause 会被更新为 m_pause 的实际当前值。
💡 核心工作机制与内存顺序
这个函数的工作流程可以概括为:
1. 比较:原子性地比较 m_pause 的当前值是否等于 expected_pause。
2. 决策与执行:
◦ 如果相等:将 m_pause 的值设置为 false,操作返回 true。
◦ 如果不相等:不会修改 m_pause,而是将 m_pause 的当前值写入 expected_pause,操作返回 false。这一步非常关键,因为它让你知道了最新的实际值,以便重试。
选择 std::memory_order_acq_rel 作为内存顺序,意味着:
• 成功时(交换发生):具有 release 语义。确保在该操作之前的所有内存写操作(在当前线程中)的结果,都能被其他在线程中随后通过 acquire 操作读取到该原子变量的线程看到。
• 失败时:具有 acquire 语义。确保能安全地获取 m_pause 的最新值。
这种内存序在读写同一个原子变量以实现同步的场景中非常典型和重要。
🔄 典型使用场景与伪失败说明
这个操作通常用在循环中,以确保在条件满足时最终能完成设置,特别是在多线程竞争环境下。例如,一个线程可能想将暂停标志 m_pause 从 true 改为 false 以恢复运行:
// 假设初始状态下 m_pause 为 true,我们想将它设置为 false
bool expected_pause = true;
// 循环尝试,直到成功地将值从 true 改为 false
while (m_pause.compare_exchange_strong(expected_pause, false, std::memory_order_acq_rel)) {
// 如果进入循环,说明 compare_exchange_strong 返回了 false,交换未成功。
// 这意味着在我们尝试时,m_pause 的当前值已经不等于 expected_pause 了。
// 此时 expected_pause 已被函数自动更新为 m_pause 的最新当前值。
// 我们需要根据最新的 expected_pause 来决定下一步(比如,如果它已经为 false,可能就跳出循环)。
if (!expected_pause) {
break; // 标志已经被其他线程设为 false 了
}
// 否则,重设 expected_pause 为 true,继续尝试
expected_pause = true;
}
// 跳出循环,说明我们成功地将 m_pause 从 true 设置为了 false
需要注意的是,compare_exchange_strong 与 compare_exchange_weak 不同,通常不会出现“伪失败”(即即使比较值相等也可能失败),因此更适合不放在循环内或对可靠性要求极高的场景。
希望这个详细的解释能帮助你彻底理解这行代码的用法!如果你对无锁编程的其他概念感兴趣,我们可以继续深入探讨。
欢迎光临 firemail (http://www.firemail.wang:8088/)
Powered by Discuz! X3