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