Skip to content

Commit d3ae03e

Browse files
committed
update original
1 parent f4e6fc1 commit d3ae03e

2 files changed

Lines changed: 61 additions & 6 deletions

File tree

async-book/src/part-guide/async-await.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ From here on in, I'm going to try to be precise about the terminology around tas
5959
An async task in Rust is just a future (usually a 'big' future made by combining many others). In other words, a task is a future which is executed. However, there are times when a future is 'executed' without being a runtime's task. This kind of a future is intuitively a *task* but not a *runtime's task*. I'll spell this out more when we get to an example of it.
6060

6161

62-
## Async functions
62+
## Async functions
6363

6464
The `async` keyword is a modifier on function declarations. E.g., we can write `pub async fn send_to_server(...)`. An async function is simply a function declared using the `async` keyword, and what that means is that it is a function which can be executed asynchronously, in other words the caller *can choose not to* wait for the function to complete before doing something else.
6565

@@ -73,9 +73,11 @@ Within an async function, code is executed in the usual, sequential way[^preempt
7373

7474
We stated above that a future is a computation that will be ready at some point in the future. To get the result of that computation, we use the `await` keyword. If the result is ready immediately or can be computed without waiting, then `await` simply does that computation to produce the result. However, if the result is not ready, then `await` hands control over to the scheduler so that another task can proceed (this is cooperative multitasking mentioned in the previous chapter).
7575

76-
The syntax for using await is `some_future.await`, i.e., it is a postfix keyword used with the `.` operator. That means it can be used ergonomically in chains of method calls and field accesses.
76+
In Rust, the syntax for using await is `some_future.await`, i.e., it is a postfix keyword used with the `.` operator. That means it can be used ergonomically in chains of method calls and field accesses. This is in contrast to languages like Python or JavaScript, where `await` is a prefix operator placed before an expression, such as `await some_function()`.
7777

78-
Consider the following functions:
78+
To see why postfix await is often more ergonomic, suppose you're calling an async function that makes a network request and want to access the status code of the response. With the prefix `await` syntax, you would need to prepend `await` to `fetch()`, then wrap the expression in parentheses to propagate errors with `?`, and then access the status code, like `(await fetch())?.status_code`. In postfix syntax, you can write `fetch().await?.status_code`. This becomes especially helpful in longer chains. E.g., an expression with two prefix awaits looks like `(await (await fetch())?.json())?.data`, whereas the postfix equivalent is `fetch().await?.json().await?.data`, which reads more naturally.
79+
80+
Now let's look at how `async` and `await` in practice. Consider the following functions:
7981

8082
```rust,norun
8183
// An async function, but it doesn't need to wait for anything.

async-book/src/part-guide/io.md

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,64 @@ For the rest of this section, we'll assume you have a mix of latency-sensitive t
101101

102102
There are essentially three solutions for running long-running or blocking tasks: use a runtime's built-in facilities, use a separate thread, or use a separate runtime.
103103

104-
In Tokio, you can use [`spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html) to spawn a task which might block. This works like `spawn` for spawning a task, but runs the task in a separate thread pool which is optimized for tasks which might block (the task will likely run on it's own thread). Note that this runs regular synchronous code, not an async task. That means that the task can't be cancelled (even though it's `JoinHandle` has an `abort` method). Other runtimes provide similar functionality.
104+
In Tokio, you can use [`spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html) to spawn a task which might block. This works like [`spawn`](https://docs.rs/tokio/latest/tokio/task/fn.spawn.html) for spawning a task, but runs the task in a separate thread pool which is optimized for tasks which might block (the task will likely run on it's own thread). Note that this runs regular synchronous code, not an async task. That means that the task can't be cancelled (even though its `JoinHandle` has an `abort` method). Other runtimes provide similar functionality.
105105

106-
You can spawn a thread to do the blocking work using [`std::thread::spawn`](https://doc.rust-lang.org/stable/std/thread/fn.spawn.html) (or similar functions). This is pretty straightforward. If you need to run a lot of tasks, you'll probably need some kind of thread pool or work scheduler. If you keep spawning threads and have many more than there are cores available, you'll end up sacrificing throughput. [Rayon](https://github.com/rayon-rs/rayon) is a popular choice which makes it easy to run and manage parallel tasks. You might get better performance with something which is more specific to your workload and/or has some knowledge of the tasks being run.
106+
This example uses `spawn_blocking` to perform blocking I/O by calling a synchronous filesystem function from the standard library. Note that [`tokio::fs`](https://docs.rs/tokio/latest/tokio/fs/index.html) also exists and provides asynchronous filesystem APIs; however, under the hood it too uses blocking operations wrapped in `spawn_blocking`.
107107

108-
You can use a separate instances of the async runtime for latency-sensitive tasks and for long-running tasks. This is suitable for CPU-bound tasks, but you still shouldn't use blocking IO, even on the runtime for long-running tasks. For CPU-bound tasks, this is a good solution in that it is the only one which supports the long-running tasks be async tasks. It is also flexible (since the runtimes can be configured to be optimal for the kind of task they're running; indeed, it is necessary to put some effort into runtime configuration to get optimal performance) and lets you benefit from using mature, well-engineered sub-systems like Tokio. You can even use two different async runtimes. In any case, the runtimes must be run on different threads.
108+
```rust,norun
109+
use tokio;
110+
111+
#[tokio::main]
112+
async fn main() {
113+
let contents = tokio::task::spawn_blocking(|| {
114+
std::fs::read_to_string("file.txt").unwrap()
115+
})
116+
.await
117+
.unwrap();
118+
119+
// do something with contents
120+
}
121+
```
122+
123+
Because tasks spawned with `spawn_blocking` cannot be aborted, it is intended for work that eventually completes. Tasks that may block indefinitely, such as a server listening for incoming requests, are better run on a dedicated thread so they do not occupy a thread from Tokio's blocking thread pool for an extended period. You can create one with [`std::thread::spawn`](https://doc.rust-lang.org/stable/std/thread/fn.spawn.html) or a similar API.
124+
125+
If you need to run a lot of tasks, you'll probably need some kind of thread pool or work scheduler. If you keep spawning threads and have many more than there are cores available, you'll end up sacrificing throughput. [Rayon](https://github.com/rayon-rs/rayon) is a popular choice which makes it easy to run and manage parallel tasks. You might get better performance with something which is more specific to your workload and/or has some knowledge of the tasks being run.
126+
127+
Here is an example of using Rayon together with Tokio. It utilizes [`tokio::oneshot::channel`](https://docs.rs/tokio/latest/tokio/sync/oneshot/fn.channel.html) to communicate results between a task spawned by Rayon and the current task in Tokio.
128+
129+
```rust,norun
130+
use rayon::prelude::*;
131+
132+
#[tokio::main]
133+
async fn main() {
134+
let data = 1..=10;
135+
136+
let (send, recv) = tokio::sync::oneshot::channel();
137+
// Spawn a task on rayon to avoid blocking the current task
138+
std::thread::spawn(move || {
139+
// Use rayon's parallel iterators to compute the results in parallel
140+
let results = data.into_par_iter().map(compute).collect::<Vec<_>>();
141+
// Send the result back to Tokio.
142+
send.send(results).unwrap();
143+
});
144+
145+
// Wait for the rayon task and get the results
146+
let results = recv.await.unwrap();
147+
println!("Results: {:?}", results);
148+
}
149+
150+
fn compute(input: u64) -> u64 {
151+
// Simulate a CPU-intensive computation by
152+
// summing up a large number of integers.
153+
let mut sum = 0u64;
154+
for i in 0..100_000_000 {
155+
sum = sum.wrapping_add(i * i);
156+
}
157+
sum % input
158+
}
159+
```
160+
161+
You can use a separate instance of the async runtime for latency-sensitive tasks and for long-running tasks. This is suitable for CPU-bound tasks, but you still shouldn't use blocking IO, even on the runtime for long-running tasks. For CPU-bound tasks, this is a good solution in that it is the only one which supports the long-running tasks be async tasks. It is also flexible (since the runtimes can be configured to be optimal for the kind of task they're running; indeed, it is necessary to put some effort into runtime configuration to get optimal performance) and lets you benefit from using mature, well-engineered sub-systems like Tokio. You can even use two different async runtimes. In any case, the runtimes must be run on different threads.
109162

110163
On the other hand, you do need to do a bit more thinking: you must ensure that you are running tasks on the right runtime (which can be harder than it sounds) and communication between tasks can be complicated. We'll discuss synchronisation between sync and async contexts next, but it can be even trickier between multiple async runtimes. Each runtime is it's own little universe of tasks and the schedulers are totally independent. Tokio channels and locks *can* be used from different runtimes (even non-Tokio ones), but other runtimes' primitives may not work in this way.
111164

0 commit comments

Comments
 (0)