在原子上调用`into_inner()`是否考虑了所有轻松的写入?

时间:2017-10-16 17:18:24

标签: multithreading rust relaxed-atomics

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()时,哪个概念保证所有写入都已完成,并且所有缓存都会同步回主线程?是否有可能丢失写入?

3 个答案:

答案 0 :(得分:5)

  

into_inner()是否会返回此示例程序中的所有轻松写入?如果是这样,哪个概念保证了这个?

它不是into_inner保证它,join

into_inner保证,自从最后一次并发写入(join线程,最后Arc已被删除和解包后,已执行某些同步与try_unwrap等),或者原子从未首先发送到另一个线程。这两种情况都足以使读取数据无竞争。

Crossbeam documentation明确指出在范围的末尾使用join

  

通过在作用域退出之前让父线程加入子线程来确保[保证终止的线程]。

关于丢失写入:

  

在调用into_inner()时,哪个概念保证所有写入都已完成,并且所有缓存都会同步回主线程?是否有可能失去写作?

正如文档中的various places所述,Rust继承了原子的C ++内存模型。 In C++11以及稍后,join完成了相应的成功返回join。这意味着,在join完成时,联接线程执行的所有操作必须对调用join的线程可见,因此在此方案中不可能丢失写入。

就原子学而言,你可以将DB2F71B3B998D600946FD47636122256FB9FA10A33A66707133A73CC458B9D90A593C32DD054BCF5CEA430CE512D659D689BD3E61ECCD4B18B3921FF7FB81F0433B9CE995A3DC096A8DD5C3A5F8EC6EB4C0CF1036CDBA0E29F20B54E1F690002A01F29A7BB9622B05835C23EBDF8F1A0D4581C9579B29877F1457053B681DA72A0DD4BF1133B857BEB7C3971416F12D6630F7939DF3C44DFB4555B5A8260134FC3AEB328CF76697367A336E881FE291F860E5E7BE708F9BA7C046632868B17468AC7BD8013032F17BDA9DF9DFF0552B33C8431075BA0936BDFF9E6173EF1901AFE27FD72422EAD6F77059F15BADB4F376F56C1F04D6DB52E509954399DEC28D9:10001 视为原子的获取读取,该线程在执行完成之前执行了一个发布存储。

答案 1 :(得分:1)

我将把这个答案作为对其他两个人的潜在补充。

这里提到的那种不一致,即在最终读取计数器之前是否可能缺少某些写入,这里是不可能的。如果对值的写入可能被推迟到使用into_inner消耗之后,那么它将是未定义的行为。但是,即使没有计数器被into_inner使用,即使没有crossbeam范围的帮助,此程序也没有意外的竞争条件。

让我们编写一个没有横梁范围的程序的新版本,以及不使用计数器的地方(Playground):

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之间建立了 synchronize-with 关系。因此,如a separate answer中所述,联接线程执行的所有操作必须对调用者线程可见。

人们可能也想知道,即使是宽松的内存排序约束是否足以保证完整程序的行为符合预期。这部分由Rust Nomicon解决,强调我的:

  

轻松的访问是绝对最弱的。它们可以自由地重新排序,并且不会在之前发生关系。不过,放松的操作仍然是原子的。也就是说,它们不算作数据访问,并且对它们执行的任何读 - 修改 - 写操作都是以原子方式进行的。轻松的操作适合您绝对想要发生的事情,但不要特别注意。例如,如果你没有使用计数器来同步任何其他访问,那么使用宽松的fetch_add 可以安全地完成递增计数器。

上述用例正是我们在这里所做的。每个线程不需要观察递增的计数器以做出决定,但所有操作都是原子的。最后,线程join与主线程同步,从而暗示发生在之前的关系,并保证操作在那里可见。由于Rust采用与C ++ 11相同的内存模型(这是由LLVM内部实现的),我们可以看到关于“由*this标识的线程完成的C ++ std::thread::join函数与相应的成功返回“同步。事实上,C ++ is available in cppreference.com中的相同例子是对宽松内存顺序约束的解释的一部分:

#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';
}

答案 2 :(得分:0)

您可以调用into_inner(使用AtomicUsize)这一事实意味着该后备存储不再需要借用。

每个fetch_add都是Relaxed排序的原子,所以一旦线程完成,就不应该有任何改变它的东西(如果是这样的话,那么横梁就会出现错误)。 / p>

请参阅the description on into_inner for more info