4

是否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

我知道横梁保证所有线程都已完成,并且由于所有权回到主线程,我也知道不会有未完成的借用,但在我看来,仍然可能有未完成的未决写入,如果不是CPU,然后在缓存中。

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

4

3 回答 3

5

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

这不是into_inner保证它,它是join

into_inner可以保证的是,自最后一次并发写入(线程的,最后一次被删除并用 解包等)以来已经执行了一些同步,或者原子从一开始就没有发送到另一个线程。任何一种情况都足以使读取数据无竞争。joinArctry_unwrap

Crossbeam文档明确说明了join在范围末尾使用:

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

关于丢失写入:

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

如文档中多处所述, Rust继承了原子的 C++ 内存模型。在 C++11及更高版本中,线程的完成join. 这意味着在join完成时,连接线程执行的所有操作必须对调用 的线程可见join,因此在这种情况下不可能丢失写入。

就原子而言,您可以将 ajoin视为线程在完成执行之前对其执行释放存储的原子的获取读取。

于 2017-10-16T19:05:34.247 回答
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. 因此,正如在单独的答案中所解释的那样,连接线程执行的所有操作都必须对调用者线程可见。

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

宽松的访问绝对是最弱的。它们可以自由地重新排序,并且不提供发生前的关系。尽管如此,轻松的操作仍然是原子的。也就是说,它们不算作数据访问,对它们进行的任何读-修改-写操作都是原子发生的。宽松的操作适用于您肯定想要发生的事情,但不是特别在意的事情。例如,如果您不使用计数器来同步任何其他访问,则可以由多个线程使用宽松的 fetch_add 安全地递增计数器。

提到的用例正是我们在这里所做的。每个线程不需要观察递增的计数器来做出决定,但所有操作都是原子的。最后,线程joins 与主线程同步,从而暗示发生之前的关系,并保证操作在那里可见。由于 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';
}
于 2017-10-17T08:51:02.647 回答
0

您可以调用into_inner(消耗AtomicUsize)的事实意味着该后备存储上没有更多的借用。

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

有关更多信息,请参阅说明into_inner

于 2017-10-16T17:48:04.903 回答