Skip to content

Commit b1cf2ee

Browse files
committed
Improving clarity of the story
1 parent 144075e commit b1cf2ee

File tree

1 file changed

+3
-3
lines changed

1 file changed

+3
-3
lines changed

src/vision/status_quo/barbara_simulates_hydrodynamics.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ Her first attempt to was to emulate a common CFD design pattern: using message p
1717
This solution worked, but Barbara had two problems with it. First, it gave her no control over CPU usage so the solution would greedily use all available CPU resources. Second, using messages to communicate solution values between patches did not scale when her team added a new feature (tracer particles) that added additional solution data the additional messages caused by this change created so much overhead that parallel processing was no faster than serial. So, Barbara decided to find a better solution.
1818

1919
### Solution Path
20-
What Barbara wanted use the CPU more efficiently: she would decouple the work that needed to be done (the patches) from the workers (threads) this would allow her to more finely control how many resources were used. So, she began looking for a tool in Rust that would meet this design pattern. When she read about `async` and how it allowed the user to define units of work, called tasks, and send those to an executor which would manage the execution of those tasks across a set of workers, she thought she'd found exactly what she needed. Further reading indicate that `tokio` was the runtime of choice for `async` in the community and so she began building a new CFD tool with `async` and `tokio`. And to move away from the message passing design, because the number of messages being passed was proportional to the number of trace particles being traced.
20+
To address the first problem: Barbara would decouple the work that needed to be done (solving each patch) from the workers (threads) this would allow her to more finely control how many resources were used. So, she began looking for a tool in Rust that would meet this design pattern. When she read about `async` and how it allowed the user to define units of work, called tasks, and send those to an executor which would manage the execution of those tasks across a set of workers, she thought she'd found exactly what she needed. Further reading indicate that `tokio` was the runtime of choice for `async` in the community and so she began building a new CFD tool with `async` and `tokio`. And to move away from the message passing design, because the number of messages being passed was proportional to the number of trace particles being traced.
2121

2222
As Barbara began working on her new design with `tokio`, her use of `async` went from a general (from the textbook) use of basic `async` features to a more specific implementation leveraging exactly the features that were most suited for her needs. At first, Barbara was under a false impression about what async executors do. She had assumed that a multi-threaded executor could automatically move the execution of an async block to a worker thread. When this turned out to wrong, she went to Stackoverflow and learned that async tasks must be explicitly spawned into a thread pool if they are to be executed on a worker thread. This meant that the algorithm to be parallelized became strongly coupled to both the spawner and the executor. Code that used to cleanly express a physics algorithm now had interspersed references to the task spawner, not only making it harder to understand, but also making it impossible to try different execution strategies, since with Tokio the spawner and executor are the same object (the Tokio runtime). Barbara felt that a better design for data parallelism would enable better separation of concerns: a group of interdependent compute tasks, and a strategy to execute them in parallel.
2323

24-
Along with moving the execution of the computational tasks to `async`, Barbara also used this as an opportunity to remove the message passing that was used to coordinate the computation of each patch. She used the `async` API to define dependencies between patches so that a patch would only begin computing its solution when its neighboring patches had completed. This also required setting up shared state that would store the solutions for all the patches as they were computed, so that dependents could access them. Learning how to properly use shared data with `async` was a new challenge. The initial design:
24+
With the move to `async`, Barbara saw an opportunity to solve her second program. Rather than using message passing to coordinate patch computation, she used the `async` API to define dependencies between patches so that a patch would only begin computing its solution when its neighboring patches had completed. She setup a shared data structure to track the solutions for each patch now that messages would not be passing that data. Learning how to properly use shared data with `async` was a new challenge. The initial design:
2525
```rust
2626
let mut stage_primitive_and_scalar = |index: BlockIndex, state: BlockState<C>, hydro: H, geometry: GridGeometry| {
2727
let stage = async move {
@@ -34,7 +34,7 @@ Along with moving the execution of the computational tasks to `async`, Barbara a
3434
```
3535
lacked performance because she needed to clone the value for every task. So, Barbara switched over to using `Arc` to keep a thread safe RC to the shared data. But this change introduced a lot of `.map` and `.unwrap` function calls, making the code much harder to read. She realized that managing the dependency graph was not intuitive when using `async` for concurrency.
3636

37-
During the move to `async` Barbara ran into a steep learning curve with error handling. The initial version of her design just used `panic!`s to fail the program if an error was encountered, but the stack traces were almost unreadable. She asked her teammate Grace to migrate over to using the `Result` idiom for error handling and Grace found a major inconvenience. The Rust type inference inconsistently breaks when propagating `Result` in `async` blocks. Grace frequently found that she had to specify the type of the error when creating a result value:
37+
A new problem arose during the move to `async`: a steep learning curve with error handling. The initial version of her design used `panic!`s to fail the program if an error was encountered, but the stack traces were almost unreadable. She asked her teammate Grace to migrate over to using the `Result` idiom for error handling and Grace found a major inconvenience. The Rust type inference inconsistently breaks when propagating `Result` in `async` blocks. Grace frequently found that she had to specify the type of the error when creating a result value:
3838
```rust
3939
Ok::<_, HydroError>( ( p.to_shared(), s.to_shared() ) )
4040
```

0 commit comments

Comments
 (0)