如何从多个线程改变共享变量,而不考虑数据竞争?

Int*_*tor 11 rust

如何改变i闭包内的变量?竞争条件被认为是可以接受的。

use rayon::prelude::*;

fn main() {

    let mut i = 0;
    let mut closure = |_| {
        i = i + 1;
    };

    (0..100).into_par_iter().for_each(closure);
}
Run Code Online (Sandbox Code Playgroud)

此代码失败并显示:

error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
  --> src\main.rs:6:23
   |
6  |     let mut closure = |_| {
   |                       ^^^ this closure implements `FnMut`, not `Fn`
7  |         i = i + 1;
   |         - closure is `FnMut` because it mutates the variable `i` here
...
10 |     (0..100).into_par_iter().for_each(closure);
   |                              -------- the requirement to implement `Fn` derives from here
Run Code Online (Sandbox Code Playgroud)

tre*_*tcl 8

竞争条件和数据竞争之间存在差异。

\n

竞争条件是指两个或多个事件的结果取决于哪个事件先发生,并且没有任何东西强制它们之间的相对顺序的任何情况。这可能没问题,只要所有可能的顺序都是可以接受的,您就可以接受您的代码中存在竞争。

\n

数据竞争是一种特定类型的竞争条件,其中事件是对同一内存的不同步访问,并且其中至少一个是突变。数据竞争是未定义的行为。你不能“接受”数据竞争,因为它的存在会使整个程序失效;一个存在不可避免的数据竞争的程序根本没有任何定义的行为,因此它没有做任何有用的事情。

\n

这是您的代码版本,存在竞争条件,但没有数据竞争代码版本:

\n
    use std::sync::atomic::{AtomicI32, Ordering};\n    let i = AtomicI32::new(0);\n    let closure = |_| {\n        i.store(i.load(Ordering::Relaxed) + 1, Ordering::Relaxed);\n    };\n\n    (0..100).into_par_iter().for_each(closure);\n
Run Code Online (Sandbox Code Playgroud)\n

由于loads 和stores 没有相对于并发执行的线程进行排序i,因此无法保证 的最终值恰好为 100。它可能是 99、72、41,甚至 1。此代码具有不确定性,但定义了行为,因为尽管您不知道事件的确切顺序或最终结果,但您仍然可以推理其行为。在这种情况下,你可以证明最终的值i必须至少为 1 且不大于 100。

\n

请注意,为了编写这段活泼的代码,我仍然必须使用AtomicI32原子loadstore。不关心不同线程中事件的顺序并不能让您不必考虑同步内存访问

\n

如果您的原始代码已编译,则会出现数据争用。\xc2\xb9 这意味着根本无法保证其行为。因此,假设您实际上接受数据竞争,这里的代码版本与允许编译器执行的操作一致:

\n
fn main() {}\n
Run Code Online (Sandbox Code Playgroud)\n

哦,对了,未定义的行为绝不能发生。因此,这个假设的编译器只是删除了您的所有代码,因为它从一开始就不允许运行。

\n

实际上比这更糟糕。假设你写了这样的东西:

\n
fn main() {\n    let mut i = 0;\n    let mut closure = |_| {\n        i = i + 1;\n    };\n\n    (0..100).into_par_iter().for_each(closure);\n\n    if i < 100 || i >= 100 {\n        println!("this should always print");\n    } else {\n        println!("this should never print");\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这段代码应该打印什么?如果没有数据争用,此代码必须发出以下内容:

\n
this should always print\n
Run Code Online (Sandbox Code Playgroud)\n

但如果我们允许数据竞争,它也可能会打印出以下内容:

\n
this should never print\n
Run Code Online (Sandbox Code Playgroud)\n

或者它甚至可以打印这个:

\n
this should never print\nthis should always print\n
Run Code Online (Sandbox Code Playgroud)\n

如果您认为它无法完成最后一件事,那您就错了。程序中未定义的行为是不能接受的,因为即使是与原始错误没有明显关系的正确代码,它也会使分析无效。

\n

unsafe如果您只是使用并忽略数据竞争的可能性,那么发生这种情况的可能性有多大?嗯,说实话,可能性不大。如果您用来unsafe绕过检查并查看生成的程序集,它甚至可能是正确的。但唯一可以确定的方法是直接用汇编语言编写,理解并编码到机器模型:如果你想使用 Rust,你必须编码到 Rust 的模型,即使这意味着你会损失一点性能。

\n

性能多少?如果有的话,可能不多。原子操作非常高效,在许多架构上,包括您现在可能正在使用的架构,在这种情况下,它们实际上与非原子操作一样快。如果您确实想知道您损失了多少潜在性能,请编写两个版本并对它们进行基准测试,或者简单地比较带有和不带有原子操作的汇编代码

\n
\n

\xc2\xb9 从技术上讲,我们不能说一定发生数据竞争,因为这取决于是否有任何线程实际上i同时访问。for_each例如,如果出于某种原因决定在同一操作系统线程上运行所有闭包,则此代码不会出现数据争用。但它可能存在数据竞争这一事实仍然会影响我们的分析,因为我们无法确定它没有。

\n


Net*_*ave 6

您不能完全做到这一点,例如,您需要确保底层发生一些安全同步。例如使用Arc+ 某种原子操作。您在文档中有一些示例:

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

let val = Arc::new(AtomicUsize::new(5));

for _ in 0..10 {
    let val = Arc::clone(&val);

    thread::spawn(move || {
        let v = val.fetch_add(1, Ordering::SeqCst);
        println!("{:?}", v);
    });
}
Run Code Online (Sandbox Code Playgroud)

操场

(正如 Adien4 所指出的:第二个示例中不需要 Arc 或移动 - Rayon 只需要FnSend + Sync这将我们引向您的示例,可以将其改编为:

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use rayon::prelude::*;

fn main() {

    let i = AtomicUsize::new(5);
    let mut closure = |_| {
        i.fetch_add(1, Ordering::SeqCst);
    };

    (0..100).into_par_iter().for_each(closure);
}
Run Code Online (Sandbox Code Playgroud)

操场

  • 在第二个示例中,您不需要“Arc”或“move” - Rayon 仅需要“Fn”为“Send + Sync”。[游乐场。](https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2018&amp;gist=4f4b564dcb70194693f4a80a4bae8bee) (2认同)