Here's an example. You want to do some computations on an array of values:
fn main() {
let mut v = vec![1, 2, 3];
for i in &mut v {
*i += 1;
}
println!("v: {:?}", v);
}
They want to speed this up with threads. So they ask "how do I do threads in Rust" and get pointed to std::thread. So they write this code:
use std::thread;
fn main() {
let mut v = vec![1, 2, 3];
for i in &mut v {
thread::spawn(move ||{
*i += 1;
});
}
println!("v: {:?}", v);
}
and they get this error message:
error[E0597]: `v` does not live long enough
--> src/main.rs:6:18
|
6 | for i in &mut v {
| ^^^^^^
| |
| borrowed value does not live long enough
| argument requires that `v` is borrowed for `'static`
...
13 | }
| - `v` dropped here while still borrowed
(there's more to the error message but I'm cutting it to the start)
So they ask "hey how do I make v live for 'static" and someone says "you use Arc" so they write this:
use std::thread;
use std::sync::Arc;
fn main() {
let v = Arc::new(vec![1, 2, 3]);
for i in v.iter_mut() {
thread::spawn(move ||{
*i += 1;
});
}
println!("v: {:?}", v);
}
and get this error:
error[E0596]: cannot borrow data in an `Arc` as mutable
--> src/main.rs:7:18
|
7 | for i in v.iter_mut() {
| ^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::sync::Arc<std::vec::Vec<i32>>`
So then they ask "hey I have an arc, but I want to mutate things inside of it, how do I do that?" and the answer is "use a mutex", so they write this:
use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let v = Arc::new(Mutex::new(vec![1, 2, 3]));
for i in v.lock().unwrap().iter_mut() {
thread::spawn(move ||{
*i += 1;
});
}
println!("v: {:?}", v);
}
but this still doesn't work, because the lock is held during multiple threads of execution. So they figure out that they can do this:
use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let v = Arc::new(Mutex::new(vec![1, 2, 3]));
let mut joins = Vec::new();
for i in 0..3 {
let v = v.clone();
let handle = thread::spawn(move ||{
v.lock().unwrap()[i] += 1;
});
joins.push(handle);
}
for handle in joins {
handle.join().unwrap();
}
println!("v: {:?}", v);
}
I've skipped a few iterations here because this comment is already too large. The point is, they've now accomplished the task, but the boilerplate is way way way out of control.
A more experienced Rust person would see this pattern and go "oh, hey, these threads don't actually live forever, because we want to join them all, but the compiler doesn't know that with thread::spawn because it's so general. What we want is scoped threads" and writes this:
use scoped_threadpool::Pool;
fn main() {
let mut pool = Pool::new(3);
let mut v = vec![1, 2, 3];
pool.scoped(|scope| {
for i in &mut v {
scope.execute(move ||{
*i += 1;
});
}
});
println!("v: {:?}", v);
}
and moves on with life. Way more efficient, way easier to write, extremely hard for a new person to realize that this is what they should be doing.
One user recommended this comment. What they said:
This is a bit corollary to the article so my apologies for that.
What I find most unproductive in Rust is the idioms that don't find their place in the official docs. Steve Klabnik wrote an excellent comment here in HN 4-5 months ago about how to use a thread pool which illustrated my gripes with Rust PERFECTLY: https://news.ycombinator.com/item?id=24992747
I understand this is not part of the stdlib. I do. But the officially blessed way without 3rd party crates is indeed quite hard to get right or to even write in one go.
Another thing that severely disappointed me is the lack of serious plans for Rust 2021 Edition. I really was looking forward to a terser syntax for a bunch of idioms (sadly in the meantime they flew out of my head and now I can't remember them).
The 6-week cycle is IMO not a problem. The changes have been fairly small -- but some of them very impactful, like the constant (pun intended) moving of functions from normal ones to `const` ones.
Here's an example. You want to do some computations on an array of values:
They want to speed this up with threads. So they ask "how do I do threads in Rust" and get pointed to std::thread. So they write this code:
and they get this error message:
(there's more to the error message but I'm cutting it to the start)
So they ask "hey how do I make v live for 'static" and someone says "you use Arc" so they write this:
and get this error:
So then they ask "hey I have an arc, but I want to mutate things inside of it, how do I do that?" and the answer is "use a mutex", so they write this:
but this still doesn't work, because the lock is held during multiple threads of execution. So they figure out that they can do this:
I've skipped a few iterations here because this comment is already too large. The point is, they've now accomplished the task, but the boilerplate is way way way out of control.
A more experienced Rust person would see this pattern and go "oh, hey, these threads don't actually live forever, because we want to join them all, but the compiler doesn't know that with thread::spawn because it's so general. What we want is scoped threads" and writes this:
and moves on with life. Way more efficient, way easier to write, extremely hard for a new person to realize that this is what they should be doing.