Repository navigation
The rules for how non-Send local variables "infect" an async function, making its Future type non-Send also, are stricter than they need to be. #63768
Description
Activity
Even if
NonSendStructdoesn't have an explicit implementation ofDropit's still dropped at the end of scope so is alive across the yield point.Having rustc try and infer shorter scopes to kill variables before yield points sounds like it might lead to surprising situations when you try and use a variable that appears live which will suddenly make your future
!Send, or adding aDropimplementation to the variable or any of its fields. It would be good for it to do so as an optimization when it doesn't visibly affect the execution, but it doesn't seem like that should affect the type checking.See also #57478 about explicitly dropping the variable before the yield point.
Reacted by Kevin Cox- addedA-async-awaitArea: Async & AwaitArea: Async & AwaitT-langRelevant to the language teamRelevant to the language teamC-feature-requestCategory: A feature request, i.e: not implemented / a PR.Category: A feature request, i.e: not implemented / a PR.
on Aug 21, 2019 Thanks for clarifying for me. While I was playing with this I was thinking about the interaction between
Dropand non-lexical lifetimes. Like you can have two consecutive uses of a struct that mutably borrow the same variable, so long as the first usage is "dead" before the second starts. But then that code can break if the struct or any of its members gains aDropimplementation in the future, or if you add a new usage of the first instance after the second instance has been constructed.In my mind, borrow checking and "
Sendchecking" (whatever that's called) could have worked similarly, using the same "liveness" concept. Though I have no idea what's practical in the compiler, and the issue you linked makes it sound like this might be impractical. But so you see what I mean about how it feels like "liveness" could apply to both?In particular, I found it surprising that "use a nested scope" works as a solution. To me, non-lexical lifetimes were sort of about how "we don't need to use nested scopes anymore," and it seems like a shame to regress on that a little.
But then again there are a bunch of complexities around NLL that I can't think of an equivalent for here. Like
Vecis marked#[may_dangle]for its contents, such that it doesn't have to "keep them alive" even though it hasn't been destructed yet. I don't know what the equivalent would be forSend-ness. If aVec<T>is "alive but never used again" across a yield point, andTisn'tSendbut also doesn't needDrop, do we feel like this future is morally stillSend? I think it could be. But I don't know if#[may_dangle]per se can be reused to indicate that, or if the language would need some additional feature to express it.@oconnor663 NLL has nothing to do with drop order, so it won't help there. NLL only changed lifetime analysis, which will allow strictly more programs, but won't change the behavior of old programs.
@KrishnaSannasi for sure, I was only bringing up NLL as a metaphor for what might be possible/desirable to do about the
Send/Sync/Drop-iness of async functions. It seems like the reasoning we do today around "Is this borrow alive?" could be very similar to reasoning we might want to do around "Does this object cross an.awaitboundary and therefore 'infect' the async function?":- Types which don't need
Dropat all could be considered not to cross.awaitboundaries after their last mention. - Types which need
Dropfor their fields but don't implementDropthemselves could have.awaitboundary crossing evaluated independently for each field. (I believe NLL does something similar for lifetimes? If theDropfield is independent of the containing struct's lifetime parameters, those lifetimes are not extended to end of scope.) - Container types which implement
Dropwith#[may_dangle]on their contents could be treated as though the contents did not cross any.awaitboundaries after the container's last mention, if the contents aren'tDrop. (And this could interact with the second case, if only a sub-field of the contents implementsDrop.)
- Types which don't need
Ok, that looks like it could work, altough I'm not sure if this would make undesirable changes to drop order (haven't really thought about drop order too much before, so I don't know much about the details). Someone else can evaulate that.
I guess I'd frame it as "looking very closely at the existing drop order and figuring out whether a
!Sendobject (or field) really lives past an await point."- addedAsyncAwait-TriagedAsync-await issues that have been triaged during a working group meeting.Async-await issues that have been triaged during a working group meeting.and removed
on Aug 27, 2019 - added and removedAsyncAwait-TriagedAsync-await issues that have been triaged during a working group meeting.Async-await issues that have been triaged during a working group meeting.
on Oct 1, 2019 2 remaining items
To add, the compiler doesn't respect calling drop before the await point.
Using a scope: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6cd230309e9226b37804354aa36db789
Using adrop: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=626e1855f403a3f37b847ea947963559Reacted by Olivier 'reivilibre'Bumping this issue. In more complex architectures wherein there are nested scopes, the usual workaround leads to unfriendly code
Reacted by Kai Mast- changed the title
[-]An async fn which isn't Send but which should be?[/-][+]The rules for how non-Send local variables "infect" an async function, making its Future type non-Send also, are stricter than they need to be.[/+]on Mar 29, 2021 Added to #69663
I was recently surprised by this not compiling:
fn assert_send() { fn is_send(f: impl Future + Send) {} is_send(foo()); // ERROR: `Cell<isize>` cannot be shared between threads safely, the trait `Sync` is not implemented for `Cell<isize>` } fn spawn_local(x: impl Future) -> impl Future<Output = ()> { async { todo!() } } async fn foo() { let not_send = async { let v = RefCell::new(1); let mut u = v.borrow_mut(); async { }.await; *u += 1; }; spawn_local(not_send).await; }
But this working just fine:
async fn foo() { let handle = spawn_local(async { let v = RefCell::new(1); let mut u = v.borrow_mut(); async { }.await; *u += 1; }); handle.await; }
Even more surprisingly, this doesn't work either:
async fn foo() { spawn_local(async { let v = RefCell::new(1); let mut u = v.borrow_mut(); async { }.await; *u += 1; }).await; }
Reacted by Harish Rajagopal, Moritz Hedtke and BoltAny hope this will get fixed soon?
As far as I know, none of the examples here are erring out. However, I suspect that when the non-
Sendlocals have non-trivial destructors, the infection exists unless explicitdrop(..)s are inserted, of course.So... can we check this off the list this time? Or are we still missing something? 🤔
So... can we check this off the list this time? Or are we still missing something? 🤔
Still missing something, unfortunately. As of 1.79.0 and on Nightly 2024-07-10, this compiles:
use tokio::sync::watch::Receiver; async fn affected_fn(recv: &Receiver<()>) { { let b1 = recv.borrow(); let _ = &*b1; drop(b1); } async {}.await; } pub fn test_fn(cache: &Receiver<()>) { fn assert_send(_f: impl std::future::Future + Send) {} assert_send(affected_fn(cache)) }
But this variant of
affected_fndoes not, with the only difference being the removal of the explicit scope:async fn affected_fn(recv: &Receiver<()>) { let b1 = recv.borrow(); let _ = &*b1; drop(b1); async {}.await; }
Nor does this one, which gets rid of the local entirely, and provokes a bizarre "with
recv.borrow()maybe used later" diagnostic:async fn affected_fn(recv: &Receiver<()>) { let _ = &*recv.borrow(); async {}.await; }
Changing that line to
{ let _ = &*recv.borrow(); }allows it to compile, as does getting rid of thelet _ =(but that provokes a warning suggesting the version that doesn't compile).Reacted by wieDasDingThis is very needed in case of match expression and async, especially with non async libraries like
synExample:
let type: syn::Type = syn::parse_str(""); match type { Type::BareFn(val) => { drop(val); let inputs = val.inputs.clone(); let mut referencing_type_ids: Vec<UMLBaseTypeId> = vec![]; for i in 0..inputs.len() { let args = inputs[i].clone(); let uml_base_type = Box::pin(async_function( args.clone(), module_name, db.clone(), type_maps.clone(), )) .await? // error here } } }- marked Dropped non-Send incorrectly reported as living, resulting in non-Send Future #136060 as a duplicate of this issue
on Feb 3, 2025 - marked False positives of local variables crossing await scopes. #137721 as a duplicate of this issue
on Mar 1, 2025 - marked Async function is incorrectly declared as being !Send even when nothing !Send is held over an await #140841 as a duplicate of this issue
on May 9, 2025 Leaving my trials on this here as it's still an issue in 2025:
// Does work, future is Send! :D async fn test_one() { { let x = Rc::new(5); println!("{x}"); } async { }.await; } // Doesn't work, future is !Send despite `drop`. :< async fn test_two() { let x = Rc::new(5); println!("{x}"); drop(x); async { }.await; }
Seems to be an issue with NLL? Output from
cargo check --verbose:error: future cannot be sent between threads safely --> src/main.rs:22:18 | 22 | tokio::spawn(test_two()).await.unwrap(); | ^^^^^^^^^^ future returned by `test_two` is not `Send` | = help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<i32>` note: future is not `Send` as this value is used across an await --> src/main.rs:20:19 | 17 | let x = Rc::new(5); | - has type `Rc<i32>` which is not `Send` ... 20 | async { }.await; | ^^^^^ await occurs here, with `x` maybe used later note: required by a bound in `tokio::spawn` --> /home/hiibolt/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.48.0/src/task/spawn.rs:171:21 | 169 | pub fn spawn<F>(future: F) -> JoinHandle<F::Output> | ----- required by a bound in this function 170 | where 171 | F: Future + Send + 'static, | ^^^^ required by this bound in `spawn`
Here's an example which I think should compile, but which doesn't (cargo 1.39.0-nightly 3f700ec43 2019-08-19):
The error is "
*mut ()cannot be sent between threads safely", which is to saymy_future()is!Send. I'm surprised by that, because thenonsendvariable is never used after the.awaitpoint, and it's notDrop. Some other notes:drop(nonsend)call after thelet nonsend = ...line doesn't help.my_future. That is, ifnonsendis created after the.awaitpoint, the future is stillSend.Are there any future plans to have the rustc look more closely at which locals do or do not need to be stored across an
.await?