[英]Does calling `into_inner()` on an atomic take into account all the relaxed writes?
into_inner()
是否返回此示例程序中的所有轻松写入? 如果是这样,哪个概念保证了这个?
extern crate crossbeam;
use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
let thread_count = 10;
let increments_per_thread = 100000;
let i = AtomicUsize::new(0);
crossbeam::scope(|scope| {
for _ in 0..thread_count {
scope.spawn(|| {
for _ in 0..increments_per_thread {
i.fetch_add(1, Ordering::Relaxed);
}
});
}
});
println!(
"Result of {}*{} increments: {}",
thread_count,
increments_per_thread,
i.into_inner()
);
}
( https://play.rust-lang.org/?gist=96f49f8eb31a6788b970cf20ec94f800&version=stable )
我知道crossbeam保证所有线程都已完成,并且由于所有权返回到主线程,我也明白没有未完成的借用,但是我看到它的方式,如果没有,仍然可能有未完成的待处理写入CPU,然后在缓存中。
当into_inner()
时,哪个概念保证所有写入都已完成,所有缓存都会同步回主线程? 是否有可能丢失写入?
into_inner()
是否返回此示例程序中的所有轻松写入? 如果是这样,哪个概念保证了这个?
它不是into_inner
它的into_inner
,而是它的join
。
什么into_inner
保证是,要么同步的一些已经因为最终并发写入执行( join
线程,最后Arc
已被删除并解开与try_unwrap
,等等),或原子从未在首位发送到另一个线程。 这两种情况都足以使读取数据无竞争。
Crossbeam 文档明确指出在作用域末尾使用join
:
通过在作用域退出之前让父线程加入子线程来确保[保证终止的线程]。
关于丢失写入:
当
into_inner()
时,哪个概念保证所有写入都已完成,所有缓存都会同步回主线程? 是否有可能丢失写入?
正如文档中的各个 地方所述,Rust继承了原子的C ++内存模型。 在C ++ 11及更高版本中,线程的完成与来自join
的相应成功返回同步 。 这意味着,在join
完成时,连接线程执行的所有操作必须对调用join
的线程可见,因此在此方案中不可能丢失写入。
在原子方面,您可以将join
视为对原子的获取读取,该线程在完成执行之前执行了一个发布存储。
我将把这个答案作为对其他两个的潜在补充。
提到的那种不一致,即在最终读取计数器之前是否可能缺少某些写入,这里是不可能的。 如果对值的写入可以推迟到使用into_inner
消耗之后,那么它将是未定义的行为。 然而,没有意外的竞争条件在这个程序中,即使没有计数器经与消耗into_inner
,甚至没有的帮助下crossbeam
范围。
让我们编写一个没有横梁范围的程序的新版本,以及不使用计数器的地方( 游乐场 ):
let thread_count = 10;
let increments_per_thread = 100000;
let i = Arc::new(AtomicUsize::new(0));
let threads: Vec<_> = (0..thread_count)
.map(|_| {
let i = i.clone();
thread::spawn(move || for _ in 0..increments_per_thread {
i.fetch_add(1, Ordering::Relaxed);
})
})
.collect();
for t in threads {
t.join().unwrap();
}
println!(
"Result of {}*{} increments: {}",
thread_count,
increments_per_thread,
i.load(Ordering::Relaxed)
);
这个版本仍然很好用! 为什么? 因为在结束线程与其对应的join
之间建立了同步关系。 因此,如在单独的答案中解释的那样,联接线程执行的所有操作必须对调用者线程可见。
人们可能也想知道,即使是宽松的内存排序约束是否足以保证完整程序的行为符合预期。 这部分由Rust Nomicon解决 ,强调我的:
轻松的访问是绝对最弱的。 它们可以自由地重新排序,并且不会在之前发生关系。 尽管如此, 放松的操作仍然是原子的 也就是说,它们不算作数据访问,并且对它们执行的任何读 - 修改 - 写操作都是以原子方式进行的。 轻松的操作适合您绝对想要发生的事情,但不要特别注意。 例如,如果您没有使用计数器同步任何其他访问,则可以使用宽松的fetch_add通过多个线程安全地完成递增计数器 。
提到的用例正是我们在这里所做的。 每个线程不需要观察递增的计数器以做出决定,但所有操作都是原子的。 最后,线程join
与主线程同步,从而暗示先发生关系,并保证操作在那里可见。 由于Rust采用与C ++ 11相同的内存模型(这是由LLVM内部实现的),我们可以看到关于C ++ std :: thread :: join函数“由*this
标识的线程完成与此对应的同步成功回归“ 。 实际上, cppreference.com中提供了C ++中的相同示例,作为轻松内存顺序约束的解释的一部分:
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
void f()
{
for (int n = 0; n < 1000; ++n) {
cnt.fetch_add(1, std::memory_order_relaxed);
}
}
int main()
{
std::vector<std::thread> v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f);
}
for (auto& t : v) {
t.join();
}
std::cout << "Final counter value is " << cnt << '\n';
}
您可以调用into_inner
(使用AtomicUsize
)这一AtomicUsize
意味着该后备存储不再需要借用。
每个fetch_add
都是一个带有Relaxed
排序的原子,所以一旦线程完成就不应该有任何改变它的东西(如果是这样的话,那么横梁中就有一个bug)。
有关详细信息,请参阅into_inner
的说明
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.