Skip to content

Commit 663ac98

Browse files
committed
Add async-await improving diagnostic post.
1 parent 5ac28c8 commit 663ac98

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
---
2+
layout: post
3+
title: "Improving async-await's \"Future is not Send\" diagnostic"
4+
author: David Wood
5+
description: "Highlighting a diagnostic improvement for async-await"
6+
team: the Async Foundations WG <https://rust-lang.github.io/compiler-team/working-groups/async-await/>
7+
---
8+
9+
Async-await is due to hit stable in the 1.39 release (only a month away!), and as announced in the
10+
["Async Foundations Update: Time for polish!"][previous_post] post last month, the Async
11+
Foundations WG has shifted its focus to polish. This post will highlight one aspect of that
12+
focus, diagnostic improvements, and in particular, the improvements that the working group has
13+
been making to the once-unhelpful "future is not send" diagnostic.
14+
15+
# Why doesn't my future implement `Send`?
16+
17+
One of the major places where async-await should be a pleasure to use is in multithreaded contexts,
18+
where having a future that can be sent to other threads is desirable. This might look something
19+
like the following (for brevity, there aren't any threads here, just a requirement that the
20+
future implement `std::marker::Send`):
21+
22+
```rust
23+
use std::sync::{Mutex, MutexGuard};
24+
25+
fn is_send<T: Send>(t: T) { }
26+
27+
async fn foo() {
28+
bar(&Mutex::new(22)).await
29+
}
30+
31+
async fn bar(x: &Mutex<u32>) {
32+
let g = x.lock().unwrap();
33+
baz().await
34+
}
35+
36+
async fn baz() { }
37+
38+
fn main() {
39+
is_send(foo());
40+
}
41+
```
42+
43+
When we try to compile this, we'll get an unwieldly and hard-to-follow diagnostic:
44+
45+
```
46+
error[E0277]: `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
47+
--> src/main.rs:23:5
48+
|
49+
23 | is_send(foo());
50+
| ^^^^^^^ `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
51+
|
52+
= help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
53+
= note: required because it appears within the type `for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}`
54+
= note: required because it appears within the type `[static generator@src/main.rs:13:30: 16:2 x:&std::sync::Mutex<u32> for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}]`
55+
= note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:13:30: 16:2 x:&std::sync::Mutex<u32> for<'r, 's> {&'r std::sync::Mutex<u32>, std::sync::MutexGuard<'s, u32>, impl std::future::Future, ()}]>`
56+
= note: required because it appears within the type `impl std::future::Future`
57+
= note: required because it appears within the type `impl std::future::Future`
58+
= note: required because it appears within the type `for<'r> {impl std::future::Future, ()}`
59+
= note: required because it appears within the type `[static generator@src/main.rs:9:16: 11:2 for<'r> {impl std::future::Future, ()}]`
60+
= note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:9:16: 11:2 for<'r> {impl std::future::Future, ()}]>`
61+
= note: required because it appears within the type `impl std::future::Future`
62+
= note: required because it appears within the type `impl std::future::Future`
63+
note: required by `is_send`
64+
--> src/main.rs:5:1
65+
|
66+
5 | fn is_send<T: Send>(t: T) {
67+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
68+
```
69+
70+
That's.. not great. Let's break down what's happening and understand what this error is trying to
71+
tell us.
72+
73+
```rust
74+
fn main() {
75+
is_send(foo());
76+
}
77+
```
78+
79+
In `main`, we are calling `foo` and passing the return value to `is_send`. `foo` is an `async fn`,
80+
so it doesn't return `()` (what you might expect for a function with no return type specified).
81+
Instead, it returns `impl std::future::Future<Output = ()>`, some unnamed type that implements
82+
`std::future::Future`:
83+
84+
```rust
85+
async fn foo() {
86+
bar(&Mutex::new(22)).await
87+
}
88+
89+
// becomes...
90+
91+
fn foo() -> impl std::future::Future<Output = ()> {
92+
async move {
93+
bar(&Mutex::new(22)).await
94+
}
95+
}
96+
```
97+
98+
If you want to learn more about the transformations that happen with async-await, consider
99+
reading the [`async`/`.await` primer chapter of the async book][primer].
100+
101+
```rust
102+
fn is_send<T: Send>(t: T) { }
103+
```
104+
105+
It looks like the error we're getting is because the future returned by `foo` doesn't satisfy
106+
the `T: Send` bound of `is_send`.
107+
108+
## How are async functions implemented?
109+
110+
To explain why our future doesn't implement `Send`, we first need to understand a little bit more
111+
about what async-await is doing under the hood. rustc implements `async fn`s using generators,
112+
an unstable language feature for resumable functions like the co-routines you might be familiar
113+
with from other languages. Generators are laid out like enums with variants containing all of the
114+
variables that are used across await points (which desugar to generator yields):
115+
116+
```rust
117+
async fn bar(x: &Mutex<u32>) {
118+
let g = x.lock().unwrap();
119+
baz().await // <- await point (suspend #0), `g` and `x` are in use before await point
120+
} // <- `g` and `x` dropped here, after await point
121+
```
122+
123+
```rust
124+
enum BarGenerator {
125+
Unresumed { x: &Mutex<u32>, g: MutexGuard<'_, u32>, },
126+
Suspend0 { x: &Mutex<u32>, g: MutexGuard<'_, u32>, }
127+
}
128+
```
129+
130+
If you want to learn more about the `async fn` implementation details, then Tyler Mandry has
131+
written [an excellent blog post][tmandry_post] diving into their work here in more depth which is
132+
definitely worth a read.
133+
134+
## So, why doesn't my future implement `Send`?
135+
136+
We now know that an `async fn` is represented like an enum behind-the-scenes. In synchronous Rust,
137+
you'll be used to your types automatically implementing `Send` when the
138+
[compiler determines it's appropriate][send_doc] - typically when all of the fields of your type
139+
also implement `Send`. It follows that the enum-like that represents our `async fn` would
140+
implement `Send` if all of the types in it do.
141+
142+
In other words, a future is safe to send across threads if all of the types that are held across
143+
`.await` points implement `Send`. This behaviour is useful because it lets us write generic code
144+
that interoperates smoothly with async-await, but without diagnostic support we get confusing error
145+
messages.
146+
147+
## Well, which type is the problem in the example?
148+
149+
Returning to our example, the future must be holding a type across an `.await` point that doesn't
150+
implement `Send`, but where? This is the primary question that the diagnostic improvement aims to
151+
help answer. Let's start by looking at `foo`:
152+
153+
```rust
154+
async fn foo() {
155+
bar(&Mutex::new(22)).await
156+
}
157+
```
158+
159+
`foo` invokes `bar`, passing a reference to a `std::sync::Mutex` and getting a future back, before
160+
`await`ing it.
161+
162+
```rust
163+
async fn bar(x: &Mutex<u32>) {
164+
let g: MutexGuard<u32> = x.lock().unwrap();
165+
baz().await
166+
} // <- `g` is dropped here
167+
```
168+
169+
`bar` unlocks the mutex before `await`ing `baz`. `std::sync::MutexGuard<u32>` does not implement
170+
`Send` and lives across the `baz().await` point (because `g` is dropped at the end of the scope)
171+
which causes the entire future not to implement `Send`.
172+
173+
That wasn't obvious from the error: we had to know that futures might implement `Send` depending
174+
on the types they capture *and* find the type which lives across an await point ourselves.
175+
176+
Fortunately, the Async Foundations WG has been working to improve this error, and
177+
[in nightly][play], we see the following diagnostic instead:
178+
179+
```
180+
error[E0277]: `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
181+
--> src/main.rs:23:5
182+
|
183+
5 | fn is_send<T: Send>(t: T) {
184+
| ------- ---- required by this bound in `is_send`
185+
...
186+
23 | is_send(foo());
187+
| ^^^^^^^ `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
188+
|
189+
= help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
190+
note: future does not implement `std::marker::Send` as this value is used across an await
191+
--> src/main.rs:15:3
192+
|
193+
14 | let g = x.lock().unwrap();
194+
| - has type `std::sync::MutexGuard<'_, u32>`
195+
15 | baz().await;
196+
| ^^^^^^^^^^^ await occurs here, with `g` maybe used later
197+
16 | }
198+
| - `g` is later dropped here
199+
```
200+
201+
Much better!
202+
203+
# How does it work?
204+
205+
When rustc's trait system determines that a trait wasn't implemented, in this case
206+
`std::marker::Send`, it emits this error. The trait system produces a chain of "obligations".
207+
Obligations are types which denote where a bound (e.g `T: Send` in `is_send`) originated,
208+
or where a bound was propagated.
209+
210+
To improve this diagnostic, the chain of obligations is now treated like a stack frame, where
211+
each "frame" of obligations represents each function's contribution to the error. Let's make
212+
that more concrete with a very rough approximation:
213+
214+
```rust
215+
Obligation {
216+
kind: DerivedObligation(/* generator that captured `g` */),
217+
source: /* `Span` type pointing at `bar`'s location in user code */,
218+
parent: Some(Obligation {
219+
kind: DerivedObligation(/* generator calling `bar` */),
220+
source: /* `Span` type pointing at `foo`'s location in user code */,
221+
parent: Some(Obligation {
222+
kind: ItemObligation(/* type representing `std::marker::Send` */),
223+
source: /* `Span` type pointing at `is_send`'s location in user code */,
224+
cause: None,
225+
}),
226+
}),
227+
}
228+
```
229+
230+
The compiler matches against the chain expecting an `ItemObligation` and some `DerivedObligation`s
231+
containing generators, which identifies the error we want to improve. Using information from these
232+
obligations, rustc can construct the specialized error shown above - if you'd like to see what the
233+
actual implementation looks like, check out PR [#64895][pr64895].
234+
235+
If you're interested in improving diagnostics like this, or even just fixing bugs, consider
236+
contributing to the compiler! There are many working groups to join and resources to help you get
237+
started (like the [rustc guide][rustc_guide] or the [compiler team documentation][compiler_team]).
238+
239+
# What's next?
240+
241+
More improvements to this diagnostic are planned and being worked on, so that it is applicable in
242+
more cases, and has specialized messages for `Send` and `Sync`, like below:
243+
244+
```
245+
error[E0277]: future cannot be sent between threads safely
246+
--> src/main.rs:23:5
247+
|
248+
5 | fn is_send<T: Send>(t: T) {
249+
| ------- ---- required by this bound in `is_send`
250+
...
251+
23 | is_send(foo());
252+
| ^^^^^^^ future returned by `foo` is not `Send`
253+
|
254+
= help: future is not `Send` as this value is used across an await
255+
note: future does not implement `std::marker::Send` as this value is used across an await
256+
--> src/main.rs:15:3
257+
|
258+
14 | let g = x.lock().unwrap();
259+
| - has type `std::sync::MutexGuard<'_, u32>`
260+
15 | baz().await;
261+
| ^^^^^^^^^^^ await occurs here, with `g` maybe used later
262+
16 | }
263+
| - `g` is later dropped here
264+
```
265+
266+
[primer]: https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html
267+
[previous_post]: https://blog.rust-lang.org/inside-rust/2019/10/07/AsyncAwait-WG-Focus-Issues.html
268+
[tmandry_post]: https://tmandry.gitlab.io/blog/posts/optimizing-await-1/
269+
[send_doc]: https://doc.rust-lang.org/std/marker/trait.Send.html
270+
[compiler_team]: https://rust-lang.github.io/compiler-team
271+
[rustc_guide]: https://rust-lang.github.io/rustc-guide
272+
[pr64895]: https://github.com/rust-lang/rust/pulls/64895
273+
[play]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=7e80a8bc151df8817e0983e55bf2667a

0 commit comments

Comments
 (0)