简体   繁体   English

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

[英]Does calling `into_inner()` on an atomic take into account all the relaxed writes?

Does into_inner() return all the relaxed writes in this example program? into_inner()是否返回此示例程序中的所有轻松写入? If so, which concept guarantees this? 如果是这样,哪个概念保证了这个?

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 ) https://play.rust-lang.org/?gist=96f49f8eb31a6788b970cf20ec94f800&version=stable

I understand that crossbeam guarantees that all threads are finished and since the ownership goes back to the main thread, I also understand that there will be no outstanding borrows, but the way I see it, there could still be outstanding pending writes, if not on the CPUs, then in the caches. 我知道crossbeam保证所有线程都已完成,并且由于所有权返回到主线程,我也明白没有未完成的借用,但是我看到它的方式,如果没有,仍然可能有未完成的待处理写入CPU,然后在缓存中。

Which concept guarantees that all writes are finished and all caches are synced back to the main thread when into_inner() is called? into_inner()时,哪个概念保证所有写入都已完成,所有缓存都会同步回主线程? Is it possible to lose writes? 是否有可能丢失写入?

Does into_inner() return all the relaxed writes in this example program? into_inner()是否返回此示例程序中的所有轻松写入? If so, which concept guarantees this? 如果是这样,哪个概念保证了这个?

It's not into_inner that guarantees it, it's join . 它不是into_inner它的into_inner ,而是它的join

What into_inner guarantees is that either some synchronization has been performed since the final concurrent write ( join of thread, last Arc having been dropped and unwrapped with try_unwrap , etc.), or the atomic was never sent to another thread in the first place. 什么into_inner保证是,要么同步的一些已经因为最终并发写入执行( join线程,最后Arc已被删除并解开与try_unwrap ,等等),或原子从未在首位发送到另一个线程。 Either case is sufficient to make the read data-race-free. 这两种情况都足以使读取数据无竞争。

Crossbeam documentation is explicit about using join at the end of a scope: Crossbeam 文档明确指出在作用域末尾使用join

This [the thread being guaranteed to terminate] is ensured by having the parent thread join on the child thread before the scope exits. 通过在作用域退出之前让父线程加入子线程来确保[保证终止的线程]。

Regarding losing writes: 关于丢失写入:

Which concept guarantees that all writes are finished and all caches are synced back to the main thread when into_inner() is called? into_inner()时,哪个概念保证所有写入都已完成,所有缓存都会同步回主线程? Is it possible to lose writes? 是否有可能丢失写入?

As stated in various places in the documentation, Rust inherits the C++ memory model for atomics. 正如文档中的各个 地方所述,Rust继承了原子的C ++内存模型。 In C++11 and later, the completion of a thread synchronizes with the corresponding successful return from join . 在C ++ 11及更高版本中,线程的完成来自join的相应成功返回同步 This means that by the time join completes, all actions performed by the joined thread must be visible to the thread that called join , so it is not possible to lose writes in this scenario. 这意味着,在join完成时,连接线程执行的所有操作必须对调用join的线程可见,因此在此方案中不可能丢失写入。

In terms of atomics, you can think of a join as an acquire read of an atomic that the thread performed a release store on just before it finished executing. 在原子方面,您可以将join视为对原子的获取读取,该线程在完成执行之前执行了一个发布存储。

I will include this answer as a potential complement to the other two. 我将把这个答案作为对其他两个的潜在补充。

The kind of inconsistency that was mentioned, namely whether some writes could be missing before the final reading of the counter, is not possible here. 提到的那种不一致,即在最终读取计数器之前是否可能缺少某些写入,这里是不可能的。 It would have been undefined behaviour if writes to a value could be postponed until after its consumption with into_inner . 如果对值的写入可以推迟到使用into_inner消耗之后,那么它将是未定义的行为。 However, there are no unexpected race conditions in this program, even without the counter being consumed with into_inner , and even without the help of crossbeam scopes. 然而,没有意外的竞争条件在这个程序中,即使没有计数器经与消耗into_inner ,甚至没有的帮助下crossbeam范围。

Let us write a new version of the program without crossbeam scopes and where the counter is not consumed ( 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)
);

This version still works pretty well! 这个版本仍然很好用! Why? 为什么? Because a synchronizes-with relation is established between the ending thread and its corresponding join . 因为在结束线程与其对应的join之间建立了同步关系。 And so, as well explained in a separate answer , all actions performed by the joined thread must be visible to the caller thread. 因此,如在单独的答案中解释的那样,联接线程执行的所有操作必须对调用者线程可见。

One could probably also wonder whether even the relaxed memory ordering constraint is sufficient to guarantee that the full program behaves as expected. 人们可能也想知道,即使是宽松的内存排序约束是否足以保证完整程序的行为符合预期。 This part is addressed by the Rust Nomicon , emphasis mine: 这部分由Rust Nomicon解决 ,强调我的:

Relaxed accesses are the absolute weakest. 轻松的访问是绝对最弱的。 They can be freely re-ordered and provide no happens-before relationship. 它们可以自由地重新排序,并且不会在之前发生关系。 Still, relaxed operations are still atomic . 尽管如此, 放松的操作仍然是原子的 That is, they don't count as data accesses and any read-modify-write operations done to them occur atomically. 也就是说,它们不算作数据访问,并且对它们执行的任何读 - 修改 - 写操作都是以原子方式进行的。 Relaxed operations are appropriate for things that you definitely want to happen, but don't particularly otherwise care about. 轻松的操作适合您绝对想要发生的事情,但不要特别注意。 For instance, incrementing a counter can be safely done by multiple threads using a relaxed fetch_add if you're not using the counter to synchronize any other accesses. 例如,如果您没有使用计数器同步任何其他访问,则可以使用宽松的fetch_add通过多个线程安全地完成递增计数器

The mentioned use case is exactly what we are doing here. 提到的用例正是我们在这里所做的。 Each thread is not required to observe the incremented counter in order to make decisions, and yet all operations are atomic. 每个线程不需要观察递增的计数器以做出决定,但所有操作都是原子的。 In the end, the thread join s synchronize with the main thread, thus implying a happens-before relation, and guaranteeing that the operations are made visible there. 最后,线程join与主线程同步,从而暗示先发生关系,并保证操作在那里可见。 As Rust adopts the same memory model as C++11's (this is implemented by LLVM internally), we can see regarding the C++ std::thread::join function that "The completion of the thread identified by *this synchronizes with the corresponding successful return" . 由于Rust采用与C ++ 11相同的内存模型(这是由LLVM内部实现的),我们可以看到关于C ++ std :: thread :: join函数“由*this标识的线程完成与此对应的同步成功回归“ In fact, the very same example in C++ is available in cppreference.com as part of the explanation on the relaxed memory order constraint: 实际上, 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';
}

The fact that you can call into_inner (which consumes the AtomicUsize ) means that there are no more borrows on that backing storage. 您可以调用into_inner (使用AtomicUsize )这一AtomicUsize意味着该后备存储不再需要借用。

Each fetch_add is an atomic with the Relaxed ordering, so once the threads are complete there shouldn't be any thing that changes it (if so, then there's a bug in crossbeam). 每个fetch_add都是一个带有Relaxed排序的原子,所以一旦线程完成就不应该有任何改变它的东西(如果是这样的话,那么横梁中就有一个bug)。

See the description on into_inner for more info 有关详细信息,请参阅into_inner的说明

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM