Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write the first half of the advanced async/await chapter #236

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

nrc
Copy link
Member

@nrc nrc commented Nov 14, 2024

No description provided.

@traviscross
Copy link

cc @rust-lang/wg-async

@traviscross
Copy link

I notice that the recent PRs have been merged with the CI red. The first breaking one seems to be 58eeade. What's the plan here?


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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 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).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 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).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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).

Maybe there's a better way to frame it than "block the task"? That is, I wonder if using the same word, "blocking", might be confusing to people, as the mechanism is very different, even though we know what we mean if we say this.


### 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 a chance to wake 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.

Similarly, I wonder about using the words "scheduling" or "scheduler" with respect to tasks. The runtime of course is scheduling tasks and acting as a scheduler, in a sense, but it may lead people astray.


### 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 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 object), then it can never make any more progress and is cancelled.

We try to avoid saying "in Rust" in the docs. The idea behind that is, "of course it's in Rust, that's what the whole book is about."


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`:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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`:

// continue;

// ok - continues with the next execution of the `loop`
return;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're directly comparing it with continue here, we should probably point out that it's only equivalent because the async { .. } block is the last statement in the loop.

}
```

To implement `break` you would need to test the value of the block.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe or maybe not it's worth mentioning ControlFlow.

}.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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.


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<Output = Foo>` (you might also need to make other bounds explicit, e.g., `Send` and `'static`).
Copy link

@traviscross traviscross Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Rust 2024, due to the changes to lifetime capture rules, these should be exactly equivalent. And in either case, auto traits like Send are going to leak to the returned opaque future.

It'd probably be more accurate to say that there are times when you want or need to desugar to the -> impl Future<..> form so that you can specify bounds on that returned opaque type rather than saying that one needs to adjust those things when converting between the forms.

@traviscross
Copy link

This looks great overall.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants