1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-06-26 18:51:00 +02:00

Format all Markdown files with dprint (#1157)

This is the result of running `dprint fmt` after removing `src/` from
the list of excluded directories.

This also reformats the Rust code: we might want to tweak this a bit in
the future since some of the changes removes the hand-formatting. Of
course, this formatting can be seen as a mis-feature, so maybe this is
good overall.

Thanks to mdbook-i18n-helpers 0.2, the POT file is nearly unchanged
after this, meaning that all existing translations remain valid! A few
messages were changed because of stray whitespace characters:

     msgid ""
     "Slices always borrow from another object. In this example, `a` has to remain "
    -"'alive' (in scope) for at least as long as our slice. "
    +"'alive' (in scope) for at least as long as our slice."
     msgstr ""

The formatting is enforced in CI and we will have to see how annoying
this is in practice for the many contributors. If it becomes annoying,
we should look into fixing dprint/check#11 so that `dprint` can annotate
the lines that need fixing directly, then I think we can consider more
strict formatting checks.

I added more customization to `rustfmt.toml`. This is to better emulate
the dense style used in the course:

- `max_width = 85` allows lines to take up the full width available in
our code blocks (when taking margins and the line numbers into account).
- `wrap_comments = true` ensures that we don't show very long comments
in the code examples. I edited some comments to shorten them and avoid
unnecessary line breaks — please trim other unnecessarily long comments
when you see them! Remember we're writing code for slides 😄
- `use_small_heuristics = "Max"` allows for things like struct literals
and if-statements to take up the full line width configured above.

The formatting settings apply to all our Rust code right now — I think
we could improve this with https://github.com/dprint/dprint/issues/711
which lets us add per-directory `dprint` configuration files. However,
the `inherit: true` setting is not yet implemented (as far as I can
tell), so a nested configuration file will have to copy most or all of
the top-level file.
This commit is contained in:
Martin Geisler
2023-12-31 00:15:07 +01:00
committed by GitHub
parent f43e72e0ad
commit c9f66fd425
302 changed files with 3067 additions and 2622 deletions

View File

@ -1,8 +1,10 @@
# Async Traits
Async methods in traits are not yet supported in the stable channel ([An experimental feature exists in nightly and should be stabilized in the mid term.](https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-in-trait-nightly.html))
Async methods in traits are not yet supported in the stable channel
([An experimental feature exists in nightly and should be stabilized in the mid term.](https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-in-trait-nightly.html))
The crate [async_trait](https://docs.rs/async-trait/latest/async_trait/) provides a workaround through a macro:
The crate [async_trait](https://docs.rs/async-trait/latest/async_trait/)
provides a workaround through a macro:
```rust,editable,compile_fail
use async_trait::async_trait;
@ -25,7 +27,10 @@ impl Sleeper for FixedSleeper {
}
}
async fn run_all_sleepers_multiple_times(sleepers: Vec<Box<dyn Sleeper>>, n_times: usize) {
async fn run_all_sleepers_multiple_times(
sleepers: Vec<Box<dyn Sleeper>>,
n_times: usize,
) {
for _ in 0..n_times {
println!("running all sleepers..");
for sleeper in &sleepers {
@ -46,18 +51,18 @@ async fn main() {
}
```
<details>
<details>
* `async_trait` is easy to use, but note that it's using heap allocations to
- `async_trait` is easy to use, but note that it's using heap allocations to
achieve this. This heap allocation has performance overhead.
* The challenges in language support for `async trait` are deep Rust and
- The challenges in language support for `async trait` are deep Rust and
probably not worth describing in-depth. Niko Matsakis did a good job of
explaining them in [this
post](https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/)
explaining them in
[this post](https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/)
if you are interested in digging deeper.
* Try creating a new sleeper struct that will sleep for a random amount of time
- Try creating a new sleeper struct that will sleep for a random amount of time
and adding it to the Vec.
</details>

View File

@ -1,8 +1,8 @@
# Blocking the executor
Most async runtimes only allow IO tasks to run concurrently.
This means that CPU blocking tasks will block the executor and prevent other tasks from being executed.
An easy workaround is to use async equivalent methods where possible.
Most async runtimes only allow IO tasks to run concurrently. This means that CPU
blocking tasks will block the executor and prevent other tasks from being
executed. An easy workaround is to use async equivalent methods where possible.
```rust,editable,compile_fail
use futures::future::join_all;
@ -26,25 +26,25 @@ async fn main() {
<details>
* Run the code and see that the sleeps happen consecutively rather than
- Run the code and see that the sleeps happen consecutively rather than
concurrently.
* The `"current_thread"` flavor puts all tasks on a single thread. This makes the
effect more obvious, but the bug is still present in the multi-threaded
- The `"current_thread"` flavor puts all tasks on a single thread. This makes
the effect more obvious, but the bug is still present in the multi-threaded
flavor.
* Switch the `std::thread::sleep` to `tokio::time::sleep` and await its result.
- Switch the `std::thread::sleep` to `tokio::time::sleep` and await its result.
* Another fix would be to `tokio::task::spawn_blocking` which spawns an actual
- Another fix would be to `tokio::task::spawn_blocking` which spawns an actual
thread and transforms its handle into a future without blocking the executor.
* You should not think of tasks as OS threads. They do not map 1 to 1 and most
- You should not think of tasks as OS threads. They do not map 1 to 1 and most
executors will allow many tasks to run on a single OS thread. This is
particularly problematic when interacting with other libraries via FFI, where
that library might depend on thread-local storage or map to specific OS
threads (e.g., CUDA). Prefer `tokio::task::spawn_blocking` in such situations.
* Use sync mutexes with care. Holding a mutex over an `.await` may cause another
- Use sync mutexes with care. Holding a mutex over an `.await` may cause another
task to block, and that task may be running on the same thread.
</details>

View File

@ -1,9 +1,9 @@
# Cancellation
Dropping a future implies it can never be polled again. This is called *cancellation*
and it can occur at any `await` point. Care is needed to ensure the system works
correctly even when futures are cancelled. For example, it shouldn't deadlock or
lose data.
Dropping a future implies it can never be polled again. This is called
_cancellation_ and it can occur at any `await` point. Care is needed to ensure
the system works correctly even when futures are cancelled. For example, it
shouldn't deadlock or lose data.
```rust,editable,compile_fail
use std::io::{self, ErrorKind};
@ -29,7 +29,7 @@ impl LinesReader {
}
}
if bytes.is_empty() {
return Ok(None)
return Ok(None);
}
let s = String::from_utf8(bytes)
.map_err(|_| io::Error::new(ErrorKind::InvalidData, "not UTF-8"))?;
@ -69,46 +69,49 @@ async fn main() -> std::io::Result<()> {
<details>
* The compiler doesn't help with cancellation-safety. You need to read API
- The compiler doesn't help with cancellation-safety. You need to read API
documentation and consider what state your `async fn` holds.
* Unlike `panic` and `?`, cancellation is part of normal control flow
(vs error-handling).
- Unlike `panic` and `?`, cancellation is part of normal control flow (vs
error-handling).
* The example loses parts of the string.
- The example loses parts of the string.
* Whenever the `tick()` branch finishes first, `next()` and its `buf` are dropped.
- Whenever the `tick()` branch finishes first, `next()` and its `buf` are
dropped.
* `LinesReader` can be made cancellation-safe by making `buf` part of the struct:
```rust,compile_fail
struct LinesReader {
stream: DuplexStream,
bytes: Vec<u8>,
buf: [u8; 1],
- `LinesReader` can be made cancellation-safe by making `buf` part of the
struct:
```rust,compile_fail
struct LinesReader {
stream: DuplexStream,
bytes: Vec<u8>,
buf: [u8; 1],
}
impl LinesReader {
fn new(stream: DuplexStream) -> Self {
Self { stream, bytes: Vec::new(), buf: [0] }
}
impl LinesReader {
fn new(stream: DuplexStream) -> Self {
Self { stream, bytes: Vec::new(), buf: [0] }
}
async fn next(&mut self) -> io::Result<Option<String>> {
// prefix buf and bytes with self.
// ...
let raw = std::mem::take(&mut self.bytes);
let s = String::from_utf8(raw)
// ...
}
async fn next(&mut self) -> io::Result<Option<String>> {
// prefix buf and bytes with self.
// ...
let raw = std::mem::take(&mut self.bytes);
let s = String::from_utf8(raw)
// ...
}
```
}
```
* [`Interval::tick`](https://docs.rs/tokio/latest/tokio/time/struct.Interval.html#method.tick)
is cancellation-safe because it keeps track of whether a tick has been 'delivered'.
- [`Interval::tick`](https://docs.rs/tokio/latest/tokio/time/struct.Interval.html#method.tick)
is cancellation-safe because it keeps track of whether a tick has been
'delivered'.
* [`AsyncReadExt::read`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncReadExt.html#method.read)
- [`AsyncReadExt::read`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncReadExt.html#method.read)
is cancellation-safe because it either returns or doesn't read data.
* [`AsyncBufReadExt::read_line`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncBufReadExt.html#method.read_line)
is similar to the example and *isn't* cancellation-safe. See its documentation
- [`AsyncBufReadExt::read_line`](https://docs.rs/tokio/latest/tokio/io/trait.AsyncBufReadExt.html#method.read_line)
is similar to the example and _isn't_ cancellation-safe. See its documentation
for details and alternatives.
</details>

View File

@ -1,9 +1,9 @@
# `Pin`
When you await a future, all local variables (that would ordinarily be stored on
a stack frame) are instead stored in the Future for the current async block. If your
future has pointers to data on the stack, those pointers might get invalidated.
This is unsafe.
a stack frame) are instead stored in the Future for the current async block. If
your future has pointers to data on the stack, those pointers might get
invalidated. This is unsafe.
Therefore, you must guarantee that the addresses your future points to don't
change. That is why we need to "pin" futures. Using the same future repeatedly
@ -43,10 +43,7 @@ async fn worker(mut work_queue: mpsc::Receiver<Work>) {
async fn do_work(work_queue: &mpsc::Sender<Work>, input: u32) -> u32 {
let (tx, rx) = oneshot::channel();
work_queue
.send(Work {
input,
respond_on: tx,
})
.send(Work { input, respond_on: tx })
.await
.expect("failed to send on work queue");
rx.await.expect("failed waiting for response")
@ -65,48 +62,49 @@ async fn main() {
<details>
* You may recognize this as an example of the actor pattern. Actors
typically call `select!` in a loop.
- You may recognize this as an example of the actor pattern. Actors typically
call `select!` in a loop.
* This serves as a summation of a few of the previous lessons, so take your time
- This serves as a summation of a few of the previous lessons, so take your time
with it.
* Naively add a `_ = sleep(Duration::from_millis(100)) => { println!(..) }`
to the `select!`. This will never execute. Why?
- Naively add a `_ = sleep(Duration::from_millis(100)) => { println!(..) }` to
the `select!`. This will never execute. Why?
* Instead, add a `timeout_fut` containing that future outside of the `loop`:
- Instead, add a `timeout_fut` containing that future outside of the `loop`:
```rust,compile_fail
let mut timeout_fut = sleep(Duration::from_millis(100));
loop {
select! {
..,
_ = timeout_fut => { println!(..); },
}
```rust,compile_fail
let mut timeout_fut = sleep(Duration::from_millis(100));
loop {
select! {
..,
_ = timeout_fut => { println!(..); },
}
```
* This still doesn't work. Follow the compiler errors, adding `&mut` to the
`timeout_fut` in the `select!` to work around the move, then using
`Box::pin`:
}
```
- This still doesn't work. Follow the compiler errors, adding `&mut` to the
`timeout_fut` in the `select!` to work around the move, then using
`Box::pin`:
```rust,compile_fail
let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
loop {
select! {
..,
_ = &mut timeout_fut => { println!(..); },
}
```rust,compile_fail
let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
loop {
select! {
..,
_ = &mut timeout_fut => { println!(..); },
}
```
}
```
* This compiles, but once the timeout expires it is `Poll::Ready` on every
iteration (a fused future would help with this). Update to reset
`timeout_fut` every time it expires.
- This compiles, but once the timeout expires it is `Poll::Ready` on every
iteration (a fused future would help with this). Update to reset
`timeout_fut` every time it expires.
* Box allocates on the heap. In some cases, `std::pin::pin!` (only recently
- Box allocates on the heap. In some cases, `std::pin::pin!` (only recently
stabilized, with older code often using `tokio::pin!`) is also an option, but
that is difficult to use for a future that is reassigned.
* Another alternative is to not use `pin` at all but spawn another task that will send to a `oneshot` channel every 100ms.
- Another alternative is to not use `pin` at all but spawn another task that
will send to a `oneshot` channel every 100ms.
</details>