From bc17d2151e3036ab79524a473097d825901ab133 Mon Sep 17 00:00:00 2001 From: Nick Cameron Date: Fri, 15 Nov 2024 12:21:57 +1300 Subject: [PATCH 1/3] Write the first half of the advanced async/await chapter Signed-off-by: Nick Cameron --- src/07_workarounds/02_err_in_async_blocks.md | 48 ------ src/SUMMARY.md | 1 - src/part-guide/adv-async-await.md | 157 +++++++++++++++---- src/part-guide/concurrency-primitives.md | 3 + 4 files changed, 130 insertions(+), 79 deletions(-) delete mode 100644 src/07_workarounds/02_err_in_async_blocks.md diff --git a/src/07_workarounds/02_err_in_async_blocks.md b/src/07_workarounds/02_err_in_async_blocks.md deleted file mode 100644 index 3e947986..00000000 --- a/src/07_workarounds/02_err_in_async_blocks.md +++ /dev/null @@ -1,48 +0,0 @@ -# `?` in `async` Blocks - -Just as in `async fn`, it's common to use `?` inside `async` blocks. -However, the return type of `async` blocks isn't explicitly stated. -This can cause the compiler to fail to infer the error type of the -`async` block. - -For example, this code: - -```rust,edition2018 -# struct MyError; -# async fn foo() -> Result<(), MyError> { Ok(()) } -# async fn bar() -> Result<(), MyError> { Ok(()) } -let fut = async { - foo().await?; - bar().await?; - Ok(()) -}; -``` - -will trigger this error: - -``` -error[E0282]: type annotations needed - --> src/main.rs:5:9 - | -4 | let fut = async { - | --- consider giving `fut` a type -5 | foo().await?; - | ^^^^^^^^^^^^ cannot infer type -``` - -Unfortunately, there's currently no way to "give `fut` a type", nor a way -to explicitly specify the return type of an `async` block. -To work around this, use the "turbofish" operator to supply the success and -error types for the `async` block: - -```rust,edition2018 -# struct MyError; -# async fn foo() -> Result<(), MyError> { Ok(()) } -# async fn bar() -> Result<(), MyError> { Ok(()) } -let fut = async { - foo().await?; - bar().await?; - Ok::<(), MyError>(()) // <- note the explicit type annotation here -}; -``` - diff --git a/src/SUMMARY.md b/src/SUMMARY.md index ecbab002..4adfb8bb 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -62,7 +62,6 @@ - [TODO: Cancellation and Timeouts]() - [TODO: `FuturesUnordered`]() - [Workarounds to Know and Love](07_workarounds/01_chapter.md) - - [`?` in `async` Blocks](07_workarounds/02_err_in_async_blocks.md) - [`Send` Approximation](07_workarounds/03_send_approximation.md) - [Recursion](07_workarounds/04_recursion.md) - [`async` in Traits](07_workarounds/05_async_in_traits.md) diff --git a/src/part-guide/adv-async-await.md b/src/part-guide/adv-async-await.md index 4054eb53..91264bec 100644 --- a/src/part-guide/adv-async-await.md +++ b/src/part-guide/adv-async-await.md @@ -2,19 +2,134 @@ ## Unit tests +How to unit test async code? The issue is that you can only await from inside an async context, and unit tests in Rust are not async. Luckily, most runtimes provide a convenience attribute for tests similar to the one for `async main`. Using Tokio, it looks like this: + +```rust,norun +#[tokio::test] +async fn test_something() { + // Write a test here, including all the `await`s you like. +} +``` + +There are many ways to configure the test, see the [docs](https://docs.rs/tokio/latest/tokio/attr.test.html) for details. + +There are some more advanced topics in testing async code (e.g., testing for race conditions, deadlock, etc.) and we'll cover some of those [later]() in this guide. + + ## Blocking and cancellation -- Two important concepts to be aware of early, we'll revisit in more detail as we go along -- Cancellation - - How to do it - - drop a future - - cancellation token - - abort functions - - Why it matters, cancellation safety (forward ref) -- Blocking - - IO and computation can block - - why it's bad - - how to deal is a forward ref to io chapter +Blocking and cancellation are important to keep in mind when programming with async Rust. These concepts are not localised to any particular feature or function, but are ubiquitous properties of the system which you must understand to write correct code. + +### Blocking IO + +We say a thread (note we're talking about OS threads here, not async tasks) is blocked when it can't make any progress. That's usually because it is waiting for the OS to complete a task on it's behalf (usually IO). Importantly, while a thread is blocked, the OS knows not to schedule it so that other threads can make progress. This is fine in a multithreaded program because it lets other threads make progress while the blocked thread is waiting. However, in an async program, there are other tasks which should be scheduled on the same OS thread, but the OS doesn't know about those and keeps the whole thread waiting. This means that rather than the single task waiting for it's IO to complete (which is fine), many tasks have to wait (not fine). + +We'll talk soon about non-blocking/async IO. For now, just know that non-blocking IO is IO which the async runtime knows about and so will only block the task which is waiting for it, not the whole thread. It is very important to only use non-blocking IO from an async task, never blocking IO (which is the only kind provided in Rust's standard library). + +### Blocking computation + +You can also block the thread by doing computation (this is not quite the same as blocking IO, since the OS is not involved, but the effect is similar). If you have long-running computation (with or without blocking IO) without yielding control to the runtime, then that task will never give the scheduler a chance to schedule other tasks. Remember that async programming uses cooperative multitasking? Here a task is not cooperating, so other tasks won't get a chance to get work done. We'll discuss ways to mitigate this later. + +There are many other ways to block a whole thread, and we'll come back to blocking several times in this guide. + +### Cancellation + +Cancellation means stopping a future (or task) from executing. Since in Rust, futures must be driven forward by an external force (like the async runtime), if a future is no longer driven forward then it will not execute any more. If a future is dropped (remember, a future is just a plain old Rust object), then it can never make any more progress and is cancelled. + +Cancellation can be initiated in a few ways: + +- calling [`abort`](https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html#method.abort) on a task's 'JoinHandle' (or an `AbortHandle`), +- via a [`CancellationToken`](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html) (which requires the future being cancelled to notice the token and cooperatively cancel itself), +- implicitly, by a function or macro like [`select`](https://docs.rs/tokio/latest/tokio/macro.select.html), +- by simply dropping a future if you have a direct reference to it. + +The first two are specific to Tokio, though most runtimes provide similar facilities. The second requires cooperation of the future being cancelled, but the others do not. In these other cases, the cancelled future will get no notification of cancellation and no opportunity to clean up (besides its destructor). Note that even if a future has a cancellation token, it can still be cancelled via the other methods which won't trigger the cancellation token. + +From the perspective of writing async code (in async functions, blocks, futures, etc.), the code might stop executing at any `await` (including hidden ones in macros) and never start again. In order for your code to be correct (specifically to be *cancellation safe*), it must never leave any data in an inconsistent state at any await point. + +An example of how this can go wrong is if an async function reads data into an internal buffer, then awaits the next datum. If reading the data is destructive (i.e., cannot be re-read from the original source) and the async function is cancelled, then the internal buffer will be dropped, and the data in it will be lost. + +We'll be coming back to cancellation and cancellation safety a few times in this guide, and there is a whole [chapter]() on the topic in the reference section. + + +## Async blocks + +A regular block in Rust (`{ ... }`) groups code together in the source and creates a scope of encapsulation for names. At runtime, the block is executed in order and evaluates to the value of it's last expression (or void (`()`) if there is no trailing expression). + +Similarly to async functions, an async block is a deferred version of a regular block. An async block scopes code and names together, but at runtime it is not immediately executed and evaluates to a future. To execute the block and obtain the result, it must be `await`ed. E.g., + +```rust,norun +let s1 = { + let a = 42; + format!("The answer is {a}") +}; + +let s2 = async { + let q = question().await; + format!("The question is {q}") +}; +``` + +If we were to execute this snippet, `s1` would be a string which could be printed, but `s2` would be a future - `question()` would not have been called. To print `s2`, we first have to `s2.await`. + +An async block is the simplest way to create a future, and the simplest way to create an async context for deferred work. + +Unfortunately, control flow with async blocks is a little quirky. Because an async block creates a future rather than straightforwardly executing, it behaves more like a function than a regular block with respect to control flow. `break` and `continue` cannot go 'through' an async block like they can with regular blocks, instead you have to use `return`: + +```rust,norun +loop { + { + if ... { + // ok + continue; + } + } + + async { + if ... { + // not ok + // continue; + + // ok - continues with the next execution of the `loop` + return; + } + }.await +} +``` + +To implement `break` you would need to test the value of the block. + +Likewise, `?` inside an async block will terminate execution of the future in the presence of an error, causing the `await`ed block to take the value of the error, but won't exit the surrounding function (like `?` in a regular block would). You'll need another `?` after `await` for that: + +```rust,norun +async { + let x = foo()?; // This `?` only exits the async block, not the surrounding function. + consume(x); + Ok(()) +}.await? +``` + +Annoyingly, this often confuses the compiler since (unlike functions) the 'return' type of an async block is not explicitly stated. You'll probably need to add some type annotations on variables or use turbofish types to make this work, e.g., `Ok::<_, MyError>(())` instead of `Ok(())` in the above example. + +A function which returns an async block is pretty similar to an async function. Writing `async fn foo() -> ... { ... }` is roughly equivalent to `fn foo() -> ... { async { ... } }`. In fact, from the caller's perspective they are equivalent, and changing from one form to the other is not a breaking change. Furthermore, you can override one with the other when implementing an async trait (see below). However, you do have to adjust the type, making the `Future` explicit in the async block version: `async fn foo() -> Foo` becomes `fn foo() -> impl Future` (you might also need to make other bounds explicit, e.g., `Send` and `'static`). + +You would usually prefer the async function version since it is simpler and clearer. However, the async block version is more flexible since you can execute some code when the function is called (by writing it outside the async block) and some code when the result is awaited (the code inside the async block). + + +## Async closures + +- closures + - coming soon (https://github.com/rust-lang/rust/pull/132706, https://blog.rust-lang.org/inside-rust/2024/08/09/async-closures-call-for-testing.html) + - async blocks in closures vs async closures + + +## Lifetimes and borrowing + +- Mentioned the static lifetime above +- Lifetime bounds on futures (`Future + '_`, etc.) +- Borrowing across await points +- I don't know, I'm sure there are more lifetime issues with async functions ... + ## `Send + 'static` bounds on futures @@ -22,6 +137,7 @@ - spawn local to avoid them - What makes an async fn `Send + 'static` and how to fix bugs with it + ## Async traits - syntax @@ -36,18 +152,6 @@ - history and async-trait crate -## Async blocks and closures - -- async block syntax - - what it means -- using an async block in a function returning a future - - subtype of async method -- closures - - coming soon (https://github.com/rust-lang/rust/pull/132706, https://blog.rust-lang.org/inside-rust/2024/08/09/async-closures-call-for-testing.html) - - async blocks in closures vs async closures -- errors in async blocks - - https://rust-lang.github.io/async-book/07_workarounds/02_err_in_async_blocks.html - ## Recursion - Allowed (relatively new), but requires some explicit boxing @@ -56,10 +160,3 @@ - https://blog.rust-lang.org/2024/03/21/Rust-1.77.0.html#support-for-recursion-in-async-fn - async-recursion macro (https://docs.rs/async-recursion/latest/async_recursion/) - -## Lifetimes and borrowing - -- Mentioned the static lifetime above -- Lifetime bounds on futures (`Future + '_`, etc.) -- Borrowing across await points -- I don't know, I'm sure there are more lifetime issues with async functions ... diff --git a/src/part-guide/concurrency-primitives.md b/src/part-guide/concurrency-primitives.md index aa874393..a698dc8c 100644 --- a/src/part-guide/concurrency-primitives.md +++ b/src/part-guide/concurrency-primitives.md @@ -8,6 +8,9 @@ - different versions in different runtimes/other crates - focus on the Tokio versions +From [comment](https://github.com/rust-lang/async-book/pull/230#discussion_r1829351497): A framing I've started using is that tasks are not the async/await form of threads; it's more accurate to think of them as parallelizable futures. This framing does not match Tokio and async-std's current task design; but both also have trouble propagating cancellation. See parallel_future and tasks are the wrong abstraction for more. + + ## Join - Tokio/futures-rs join macro From 61738fd0b347c263b17c47eb6484ab4e9049f2e5 Mon Sep 17 00:00:00 2001 From: Nick Cameron Date: Mon, 18 Nov 2024 14:56:21 +1300 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Travis Cross --- src/part-guide/adv-async-await.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/part-guide/adv-async-await.md b/src/part-guide/adv-async-await.md index 91264bec..b3516534 100644 --- a/src/part-guide/adv-async-await.md +++ b/src/part-guide/adv-async-await.md @@ -13,7 +13,7 @@ async fn test_something() { There are many ways to configure the test, see the [docs](https://docs.rs/tokio/latest/tokio/attr.test.html) for details. -There are some more advanced topics in testing async code (e.g., testing for race conditions, deadlock, etc.) and we'll cover some of those [later]() in this guide. +There are some more advanced topics in testing async code (e.g., testing for race conditions, deadlock, etc.), and we'll cover some of those [later]() in this guide. ## Blocking and cancellation @@ -22,9 +22,9 @@ Blocking and cancellation are important to keep in mind when programming with as ### Blocking IO -We say a thread (note we're talking about OS threads here, not async tasks) is blocked when it can't make any progress. That's usually because it is waiting for the OS to complete a task on it's behalf (usually IO). Importantly, while a thread is blocked, the OS knows not to schedule it so that other threads can make progress. This is fine in a multithreaded program because it lets other threads make progress while the blocked thread is waiting. However, in an async program, there are other tasks which should be scheduled on the same OS thread, but the OS doesn't know about those and keeps the whole thread waiting. This means that rather than the single task waiting for it's IO to complete (which is fine), many tasks have to wait (not fine). +We say a thread (note we're talking about OS threads here, not async tasks) is blocked when it can't make any progress. That's usually because it is waiting for the OS to complete a task on its behalf (usually I/O). Importantly, while a thread is blocked, the OS knows not to schedule it so that other threads can make progress. This is fine in a multithreaded program because it lets other threads make progress while the blocked thread is waiting. However, in an async program, there are other tasks which should be scheduled on the same OS thread, but the OS doesn't know about those and keeps the whole thread waiting. This means that rather than the single task waiting for its I/O to complete (which is fine), many tasks have to wait (which is not fine). -We'll talk soon about non-blocking/async IO. For now, just know that non-blocking IO is IO which the async runtime knows about and so will only block the task which is waiting for it, not the whole thread. It is very important to only use non-blocking IO from an async task, never blocking IO (which is the only kind provided in Rust's standard library). +We'll talk soon about non-blocking/async I/O. For now, just know that non-blocking I/O is I/O which the async runtime knows about and so will only block the task which is waiting for it, not the whole thread. It is very important to only use non-blocking I/O from an async task, never blocking I/O (which is the only kind provided in Rust's standard library). ### Blocking computation @@ -38,23 +38,23 @@ Cancellation means stopping a future (or task) from executing. Since in Rust, fu Cancellation can be initiated in a few ways: -- calling [`abort`](https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html#method.abort) on a task's 'JoinHandle' (or an `AbortHandle`), -- via a [`CancellationToken`](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html) (which requires the future being cancelled to notice the token and cooperatively cancel itself), -- implicitly, by a function or macro like [`select`](https://docs.rs/tokio/latest/tokio/macro.select.html), -- by simply dropping a future if you have a direct reference to it. +- Calling [`abort`](https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html#method.abort) on a task's 'JoinHandle' (or an `AbortHandle`). +- Via a [`CancellationToken`](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html) (which requires the future being cancelled to notice the token and cooperatively cancel itself). +- Implicitly, by a function or macro like [`select`](https://docs.rs/tokio/latest/tokio/macro.select.html). +- By simply dropping a future if you own it. -The first two are specific to Tokio, though most runtimes provide similar facilities. The second requires cooperation of the future being cancelled, but the others do not. In these other cases, the cancelled future will get no notification of cancellation and no opportunity to clean up (besides its destructor). Note that even if a future has a cancellation token, it can still be cancelled via the other methods which won't trigger the cancellation token. +The first two are specific to Tokio, though most runtimes provide similar facilities. The second requires cooperation of the future being canceled, but the others do not. In these other cases, the canceled future will get no notification of cancellation and no opportunity to clean up (besides its destructor). Note that even if a future has a cancellation token, it can still be canceled via the other methods which won't trigger the cancellation token. From the perspective of writing async code (in async functions, blocks, futures, etc.), the code might stop executing at any `await` (including hidden ones in macros) and never start again. In order for your code to be correct (specifically to be *cancellation safe*), it must never leave any data in an inconsistent state at any await point. -An example of how this can go wrong is if an async function reads data into an internal buffer, then awaits the next datum. If reading the data is destructive (i.e., cannot be re-read from the original source) and the async function is cancelled, then the internal buffer will be dropped, and the data in it will be lost. +An example of how this can go wrong is if an async function reads data into an internal buffer, then awaits the next datum. If reading the data is destructive (i.e., cannot be re-read from the original source) and the async function is canceled, then the internal buffer will be dropped, and the data in it will be lost. We'll be coming back to cancellation and cancellation safety a few times in this guide, and there is a whole [chapter]() on the topic in the reference section. ## Async blocks -A regular block in Rust (`{ ... }`) groups code together in the source and creates a scope of encapsulation for names. At runtime, the block is executed in order and evaluates to the value of it's last expression (or void (`()`) if there is no trailing expression). +A regular block (`{ ... }`) groups code together in the source and creates a scope of encapsulation for names. At runtime, the block is executed in order and evaluates to the value of its last expression (or the unit type (`()`) if there is no trailing expression). Similarly to async functions, an async block is a deferred version of a regular block. An async block scopes code and names together, but at runtime it is not immediately executed and evaluates to a future. To execute the block and obtain the result, it must be `await`ed. E.g., @@ -70,7 +70,7 @@ let s2 = async { }; ``` -If we were to execute this snippet, `s1` would be a string which could be printed, but `s2` would be a future - `question()` would not have been called. To print `s2`, we first have to `s2.await`. +If we were to execute this snippet, `s1` would be a string which could be printed, but `s2` would be a future; `question()` would not have been called. To print `s2`, we first have to `s2.await`. An async block is the simplest way to create a future, and the simplest way to create an async context for deferred work. From 3997c79c6dac1ff6864174f3c6a3d8bba657c250 Mon Sep 17 00:00:00 2001 From: Nick Cameron Date: Mon, 18 Nov 2024 15:32:37 +1300 Subject: [PATCH 3/3] Address review comments Signed-off-by: Nick Cameron --- src/part-guide/adv-async-await.md | 46 +++++++++++++++++++++---------- src/part-guide/streams.md | 1 + 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/part-guide/adv-async-await.md b/src/part-guide/adv-async-await.md index b3516534..a7058e7d 100644 --- a/src/part-guide/adv-async-await.md +++ b/src/part-guide/adv-async-await.md @@ -24,39 +24,56 @@ Blocking and cancellation are important to keep in mind when programming with as We say a thread (note we're talking about OS threads here, not async tasks) is blocked when it can't make any progress. That's usually because it is waiting for the OS to complete a task on its behalf (usually I/O). Importantly, while a thread is blocked, the OS knows not to schedule it so that other threads can make progress. This is fine in a multithreaded program because it lets other threads make progress while the blocked thread is waiting. However, in an async program, there are other tasks which should be scheduled on the same OS thread, but the OS doesn't know about those and keeps the whole thread waiting. This means that rather than the single task waiting for its I/O to complete (which is fine), many tasks have to wait (which is not fine). -We'll talk soon about non-blocking/async I/O. For now, just know that non-blocking I/O is I/O which the async runtime knows about and so will only block the task which is waiting for it, not the whole thread. It is very important to only use non-blocking I/O from an async task, never blocking I/O (which is the only kind provided in Rust's standard library). +We'll talk soon about non-blocking/async I/O. For now, just know that non-blocking I/O is I/O which the async runtime knows about and so will only the current task will wait, the thread will not be blocked. It is very important to only use non-blocking I/O from an async task, never blocking I/O (which is the only kind provided in Rust's standard library). ### Blocking computation -You can also block the thread by doing computation (this is not quite the same as blocking IO, since the OS is not involved, but the effect is similar). If you have long-running computation (with or without blocking IO) without yielding control to the runtime, then that task will never give the scheduler a chance to schedule other tasks. Remember that async programming uses cooperative multitasking? Here a task is not cooperating, so other tasks won't get a chance to get work done. We'll discuss ways to mitigate this later. +You can also block the thread by doing computation (this is not quite the same as blocking I/O, since the OS is not involved, but the effect is similar). If you have a long-running computation (with or without blocking I/O) without yielding control to the runtime, then that task will never give the runtime's scheduler a chance to schedule other tasks. Remember that async programming uses cooperative multitasking. Here a task is not cooperating, so other tasks won't get a chance to get work done. We'll discuss ways to mitigate this later. There are many other ways to block a whole thread, and we'll come back to blocking several times in this guide. ### Cancellation -Cancellation means stopping a future (or task) from executing. Since in Rust, futures must be driven forward by an external force (like the async runtime), if a future is no longer driven forward then it will not execute any more. If a future is dropped (remember, a future is just a plain old Rust object), then it can never make any more progress and is cancelled. +Cancellation means stopping a future (or task) from executing. Since in Rust (and in contrast to many other async/await systems), futures must be driven forward by an external force (like the async runtime), if a future is no longer driven forward then it will not execute any more. If a future is dropped (remember, a future is just a plain old Rust object), then it can never make any more progress and is canceled. Cancellation can be initiated in a few ways: +- By simply dropping a future (if you own it). - Calling [`abort`](https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html#method.abort) on a task's 'JoinHandle' (or an `AbortHandle`). -- Via a [`CancellationToken`](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html) (which requires the future being cancelled to notice the token and cooperatively cancel itself). +- Via a [`CancellationToken`](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html) (which requires the future being canceled to notice the token and cooperatively cancel itself). - Implicitly, by a function or macro like [`select`](https://docs.rs/tokio/latest/tokio/macro.select.html). -- By simply dropping a future if you own it. -The first two are specific to Tokio, though most runtimes provide similar facilities. The second requires cooperation of the future being canceled, but the others do not. In these other cases, the canceled future will get no notification of cancellation and no opportunity to clean up (besides its destructor). Note that even if a future has a cancellation token, it can still be canceled via the other methods which won't trigger the cancellation token. +The middle two are specific to Tokio, though most runtimes provide similar facilities. Using a `CancellationToken` requires cooperation of the future being canceled, but the others do not. In these other cases, the canceled future will get no notification of cancellation and no opportunity to clean up (besides its destructor). Note that even if a future has a cancellation token, it can still be canceled via the other methods which won't trigger the cancellation token. -From the perspective of writing async code (in async functions, blocks, futures, etc.), the code might stop executing at any `await` (including hidden ones in macros) and never start again. In order for your code to be correct (specifically to be *cancellation safe*), it must never leave any data in an inconsistent state at any await point. +From the perspective of writing async code (in async functions, blocks, futures, etc.), the code might stop executing at any `await` (including hidden ones in macros) and never start again. In order for your code to be correct (specifically to be *cancellation safe*), it must work correctly whether it completes normally or whether it terminates at any await point[^cfThreads]. -An example of how this can go wrong is if an async function reads data into an internal buffer, then awaits the next datum. If reading the data is destructive (i.e., cannot be re-read from the original source) and the async function is canceled, then the internal buffer will be dropped, and the data in it will be lost. +```rust,norun +async fn some_function(input: Option) { + let Some(input) = input else { + return; // Might terminate here (`return`). + }; + + let x = foo(input)?; // Might terminate here (`?`). + + let y = bar(x).await; // Might terminate here (`await`). + + // ... + + // Might terminate here (implicit return). +} +``` + +An example of how this can go wrong is if an async function reads data into an internal buffer, then awaits the next datum. If reading the data is destructive (i.e., cannot be re-read from the original source) and the async function is canceled, then the internal buffer will be dropped, and the data in it will be lost. It is important to consider how a future and any data it touches will be impacted by canceling the future, restarting the future, or starting a new future which touches the same data. We'll be coming back to cancellation and cancellation safety a few times in this guide, and there is a whole [chapter]() on the topic in the reference section. +[^cfThreads]: It is interesting to compare cancellation in async programming with canceling threads. Canceling a thread is possible (e.g., using `pthread_cancel` in C, there is no direct way to do this in Rust), but it is almost always a very, very bad idea since the thread being canceled can terminate anywhere. in contrast, canceling an async task can only happen at an await point. As a consequence, it is very rare to cancel an OS thread without terminating the whole porcess and so as a programmer, you generally don't worry about this happening. In async Rust however, cancellation is definitely something which *can* happen. We'll be discussing how to deal with that as we go along. ## Async blocks A regular block (`{ ... }`) groups code together in the source and creates a scope of encapsulation for names. At runtime, the block is executed in order and evaluates to the value of its last expression (or the unit type (`()`) if there is no trailing expression). -Similarly to async functions, an async block is a deferred version of a regular block. An async block scopes code and names together, but at runtime it is not immediately executed and evaluates to a future. To execute the block and obtain the result, it must be `await`ed. E.g., +Similarly to async functions, an async block is a deferred version of a regular block. An async block scopes code and names together, but at runtime it is not immediately executed and evaluates to a future. To execute the block and obtain the result, it must be `await`ed. E.g.: ```rust,norun let s1 = { @@ -72,9 +89,9 @@ let s2 = async { If we were to execute this snippet, `s1` would be a string which could be printed, but `s2` would be a future; `question()` would not have been called. To print `s2`, we first have to `s2.await`. -An async block is the simplest way to create a future, and the simplest way to create an async context for deferred work. +An async block is the simplest way to start an async context and create a future. It is commonly used to create small futures which are only used in one place. -Unfortunately, control flow with async blocks is a little quirky. Because an async block creates a future rather than straightforwardly executing, it behaves more like a function than a regular block with respect to control flow. `break` and `continue` cannot go 'through' an async block like they can with regular blocks, instead you have to use `return`: +Unfortunately, control flow with async blocks is a little quirky. Because an async block creates a future rather than straightforwardly executing, it behaves more like a function than a regular block with respect to control flow. `break` and `continue` cannot go 'through' an async block like they can with regular blocks; instead you have to use `return`: ```rust,norun loop { @@ -90,14 +107,15 @@ loop { // not ok // continue; - // ok - continues with the next execution of the `loop` + // ok - continues with the next execution of the `loop`, though note that if there was + // code in the loop after the async block that would be executed. return; } }.await } ``` -To implement `break` you would need to test the value of the block. +To implement `break` you would need to test the value of the block (a common idiom is to use [`ControlFlow`](https://doc.rust-lang.org/std/ops/enum.ControlFlow.html) for the value of the block, which also allows use of `?`). Likewise, `?` inside an async block will terminate execution of the future in the presence of an error, causing the `await`ed block to take the value of the error, but won't exit the surrounding function (like `?` in a regular block would). You'll need another `?` after `await` for that: @@ -109,7 +127,7 @@ async { }.await? ``` -Annoyingly, this often confuses the compiler since (unlike functions) the 'return' type of an async block is not explicitly stated. You'll probably need to add some type annotations on variables or use turbofish types to make this work, e.g., `Ok::<_, MyError>(())` instead of `Ok(())` in the above example. +Annoyingly, this often confuses the compiler since (unlike functions) the 'return' type of an async block is not explicitly stated. You'll probably need to add some type annotations on variables or use turbofished types to make this work, e.g., `Ok::<_, MyError>(())` instead of `Ok(())` in the above example. A function which returns an async block is pretty similar to an async function. Writing `async fn foo() -> ... { ... }` is roughly equivalent to `fn foo() -> ... { async { ... } }`. In fact, from the caller's perspective they are equivalent, and changing from one form to the other is not a breaking change. Furthermore, you can override one with the other when implementing an async trait (see below). However, you do have to adjust the type, making the `Future` explicit in the async block version: `async fn foo() -> Foo` becomes `fn foo() -> impl Future` (you might also need to make other bounds explicit, e.g., `Send` and `'static`). diff --git a/src/part-guide/streams.md b/src/part-guide/streams.md index aee675cb..e20ab3dd 100644 --- a/src/part-guide/streams.md +++ b/src/part-guide/streams.md @@ -21,6 +21,7 @@ - Taking a future instead of a closure - Some example combinators - unordered variations +- StreamGroup ## Implementing an async iterator