如何改变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)
竞争条件和数据竞争之间存在差异。
\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);\nRun Code Online (Sandbox Code Playgroud)\n由于loads 和stores 没有相对于并发执行的线程进行排序i,因此无法保证 的最终值恰好为 100。它可能是 99、72、41,甚至 1。此代码具有不确定性,但定义了行为,因为尽管您不知道事件的确切顺序或最终结果,但您仍然可以推理其行为。在这种情况下,你可以证明最终的值i必须至少为 1 且不大于 100。
请注意,为了编写这段活泼的代码,我仍然必须使用AtomicI32原子load和store。不关心不同线程中事件的顺序并不能让您不必考虑同步内存访问。
如果您的原始代码已编译,则会出现数据争用。\xc2\xb9 这意味着根本无法保证其行为。因此,假设您实际上接受数据竞争,这里的代码版本与允许编译器执行的操作一致:
\nfn main() {}\nRun Code Online (Sandbox Code Playgroud)\n哦,对了,未定义的行为绝不能发生。因此,这个假设的编译器只是删除了您的所有代码,因为它从一开始就不允许运行。
\n实际上比这更糟糕。假设你写了这样的东西:
\nfn 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}\nRun Code Online (Sandbox Code Playgroud)\n这段代码应该打印什么?如果没有数据争用,此代码必须发出以下内容:
\nthis should always print\nRun Code Online (Sandbox Code Playgroud)\n但如果我们允许数据竞争,它也可能会打印出以下内容:
\nthis should never print\nRun Code Online (Sandbox Code Playgroud)\n或者它甚至可以打印这个:
\nthis should never print\nthis should always print\nRun Code Online (Sandbox Code Playgroud)\n如果您认为它无法完成最后一件事,那您就错了。程序中未定义的行为是不能接受的,因为即使是与原始错误没有明显关系的正确代码,它也会使分析无效。
\nunsafe如果您只是使用并忽略数据竞争的可能性,那么发生这种情况的可能性有多大?嗯,说实话,可能性不大。如果您用来unsafe绕过检查并查看生成的程序集,它甚至可能是正确的。但唯一可以确定的方法是直接用汇编语言编写,理解并编码到机器模型:如果你想使用 Rust,你必须编码到 Rust 的模型,即使这意味着你会损失一点性能。
性能多少?如果有的话,可能不多。原子操作非常高效,在许多架构上,包括您现在可能正在使用的架构,在这种情况下,它们实际上与非原子操作一样快。如果您确实想知道您损失了多少潜在性能,请编写两个版本并对它们进行基准测试,或者简单地比较带有和不带有原子操作的汇编代码。
\n\xc2\xb9 从技术上讲,我们不能说一定会发生数据竞争,因为这取决于是否有任何线程实际上i同时访问。for_each例如,如果出于某种原因决定在同一操作系统线程上运行所有闭包,则此代码不会出现数据争用。但它可能存在数据竞争这一事实仍然会影响我们的分析,因为我们无法确定它没有。
您不能完全做到这一点,例如,您需要确保底层发生一些安全同步。例如使用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 只需要Fn)Send + 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)
| 归档时间: |
|
| 查看次数: |
1206 次 |
| 最近记录: |