1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
|
# Rayon FAQ
This file is for general questions that don't fit into the README or
crate docs.
## How many threads will Rayon spawn?
By default, Rayon uses the same number of threads as the number of
CPUs available. Note that on systems with hyperthreading enabled this
equals the number of logical cores and not the physical ones.
If you want to alter the number of threads spawned, you can set the
environmental variable `RAYON_NUM_THREADS` to the desired number of
threads or use the
[`ThreadPoolBuilder::build_global` function](https://docs.rs/rayon/*/rayon/struct.ThreadPoolBuilder.html#method.build_global)
method.
## How does Rayon balance work between threads?
Behind the scenes, Rayon uses a technique called **work stealing** to
try and dynamically ascertain how much parallelism is available and
exploit it. The idea is very simple: we always have a pool of worker
threads available, waiting for some work to do. When you call `join`
the first time, we shift over into that pool of threads. But if you
call `join(a, b)` from a worker thread W, then W will place `b` into
its work queue, advertising that this is work that other worker
threads might help out with. W will then start executing `a`.
While W is busy with `a`, other threads might come along and take `b`
from its queue. That is called *stealing* `b`. Once `a` is done, W
checks whether `b` was stolen by another thread and, if not, executes
`b` itself. If W runs out of jobs in its own queue, it will look
through the other threads' queues and try to steal work from them.
This technique is not new. It was first introduced by the
[Cilk project][cilk], done at MIT in the late nineties. The name Rayon
is an homage to that work.
[cilk]: http://supertech.csail.mit.edu/cilk/
## What should I do if I use `Rc`, `Cell`, `RefCell` or other non-Send-and-Sync types?
There are a number of non-threadsafe types in the Rust standard library,
and if your code is using them, you will not be able to combine it
with Rayon. Similarly, even if you don't have such types, but you try
to have multiple closures mutating the same state, you will get
compilation errors; for example, this function won't work, because
both closures access `slice`:
```rust
/// Increment all values in slice.
fn increment_all(slice: &mut [i32]) {
rayon::join(|| process(slice), || process(slice));
}
```
The correct way to resolve such errors will depend on the case. Some
cases are easy: for example, uses of [`Rc`] can typically be replaced
with [`Arc`], which is basically equivalent, but thread-safe.
Code that uses `Cell` or `RefCell`, however, can be somewhat more complicated.
If you can refactor your code to avoid those types, that is often the best way
forward, but otherwise, you can try to replace those types with their threadsafe
equivalents:
- `Cell` -- replacement: `AtomicUsize`, `AtomicBool`, etc
- `RefCell` -- replacement: `RwLock`, or perhaps `Mutex`
However, you have to be wary! The parallel versions of these types
have different atomicity guarantees. For example, with a `Cell`, you
can increment a counter like so:
```rust
let value = counter.get();
counter.set(value + 1);
```
But when you use the equivalent `AtomicUsize` methods, you are
actually introducing a potential race condition (not a data race,
technically, but it can be an awfully fine distinction):
```rust
let value = tscounter.load(Ordering::SeqCst);
tscounter.store(value + 1, Ordering::SeqCst);
```
You can already see that the `AtomicUsize` API is a bit more complex,
as it requires you to specify an
[ordering](https://doc.rust-lang.org/std/sync/atomic/enum.Ordering.html). (I
won't go into the details on ordering here, but suffice to say that if
you don't know what an ordering is, and probably even if you do, you
should use `Ordering::SeqCst`.) The danger in this parallel version of
the counter is that other threads might be running at the same time
and they could cause our counter to get out of sync. For example, if
we have two threads, then they might both execute the "load" before
either has a chance to execute the "store":
```
Thread 1 Thread 2
let value = tscounter.load(Ordering::SeqCst);
// value = X let value = tscounter.load(Ordering::SeqCst);
// value = X
tscounter.store(value+1); tscounter.store(value+1);
// tscounter = X+1 // tscounter = X+1
```
Now even though we've had two increments, we'll only increase the
counter by one! Even though we've got no data race, this is still
probably not the result we wanted. The problem here is that the `Cell`
API doesn't make clear the scope of a "transaction" -- that is, the
set of reads/writes that should occur atomically. In this case, we
probably wanted the get/set to occur together.
In fact, when using the `Atomic` types, you very rarely want a plain
`load` or plain `store`. You probably want the more complex
operations. A counter, for example, would use `fetch_add` to
atomically load and increment the value in one step. Compare-and-swap
is another popular building block.
A similar problem can arise when converting `RefCell` to `RwLock`, but
it is somewhat less likely, because the `RefCell` API does in fact
have a notion of a transaction: the scope of the handle returned by
`borrow` or `borrow_mut`. So if you convert each call to `borrow` to
`read` (and `borrow_mut` to `write`), things will mostly work fine in
a parallel setting, but there can still be changes in behavior.
Consider using a `handle: RefCell<Vec<i32>>` like:
```rust
let len = handle.borrow().len();
for i in 0 .. len {
let data = handle.borrow()[i];
println!("{}", data);
}
```
In sequential code, we know that this loop is safe. But if we convert
this to parallel code with an `RwLock`, we do not: this is because
another thread could come along and do
`handle.write().unwrap().pop()`, and thus change the length of the
vector. In fact, even in *sequential* code, using very small borrow
sections like this is an anti-pattern: you ought to be enclosing the
entire transaction together, like so:
```rust
let vec = handle.borrow();
let len = vec.len();
for i in 0 .. len {
let data = vec[i];
println!("{}", data);
}
```
Or, even better, using an iterator instead of indexing:
```rust
let vec = handle.borrow();
for data in vec {
println!("{}", data);
}
```
There are several reasons to prefer one borrow over many. The most
obvious is that it is more efficient, since each borrow has to perform
some safety checks. But it's also more reliable: suppose we modified
the loop above to not just print things out, but also call into a
helper function:
```rust
let vec = handle.borrow();
for data in vec {
helper(...);
}
```
And now suppose, independently, this helper fn evolved and had to pop
something off of the vector:
```rust
fn helper(...) {
handle.borrow_mut().pop();
}
```
Under the old model, where we did lots of small borrows, this would
yield precisely the same error that we saw in parallel land using an
`RwLock`: the length would be out of sync and our indexing would fail
(note that in neither case would there be an actual *data race* and
hence there would never be undefined behavior). But now that we use a
single borrow, we'll see a borrow error instead, which is much easier
to diagnose, since it occurs at the point of the `borrow_mut`, rather
than downstream. Similarly, if we move to an `RwLock`, we'll find that
the code either deadlocks (if the write is on the same thread as the
read) or, if the write is on another thread, works just fine. Both of
these are preferable to random failures in my experience.
## But wait, isn't Rust supposed to free me from this kind of thinking?
You might think that Rust is supposed to mean that you don't have to
think about atomicity at all. In fact, if you avoid interior
mutability (`Cell` and `RefCell` in a sequential setting, or
`AtomicUsize`, `RwLock`, `Mutex`, et al. in parallel code), then this
is true: the type system will basically guarantee that you don't have
to think about atomicity at all. But often there are times when you
WANT threads to interleave in the ways I showed above.
Consider for example when you are conducting a search in parallel, say
to find the shortest route. To avoid fruitless search, you might want
to keep a cell with the shortest route you've found thus far. This
way, when you are searching down some path that's already longer than
this shortest route, you can just stop and avoid wasted effort. In
sequential land, you might model this "best result" as a shared value
like `Rc<Cell<usize>>` (here the `usize` represents the length of best
path found so far); in parallel land, you'd use a `Arc<AtomicUsize>`.
Now we can make our search function look like:
```rust
fn search(path: &Path, cost_so_far: usize, best_cost: &Arc<AtomicUsize>) {
if cost_so_far >= best_cost.load(Ordering::SeqCst) {
return;
}
...
best_cost.store(...);
}
```
Now in this case, we really WANT to see results from other threads
interjected into our execution!
|