Why does parameter go out of scope so late?

I have this line of code:

tokio::time::sleep(Duration::from_millis(thread_rng().gen_range(0..=1000))).await;

It won't compile, and the compiler tells me that the ThreadRng (from thread_rng() ) goes out of scope just after the await, at the semicolon.
My thought was: I do not send the ThreadRng, I only send the single integer from gen_range(). I can understand that the line does not compile if the ThreadRng is dropped only after the await point.
But why is it dropped only then, and not immediately after the call to gen_range() when it is no longer used?

Because all temporaries are dropped "at the semicolon".

It is important in other situations because the expression might borrow from the temporary, and if the temporary is dropped immediately, then that wouldn't compile.

1 Like

OK. That's clear: just the compiler's general mechanism.

First: I do not have a picture of the "other situations".
Second: the early drop is way better then just a few years ago when items would only be dropped at the block end: {...}
However, I would think that with await, where the consequences of not being dropped can be pretty "heavy", it would be worthwhile to drop anything that can be shown not to be used any more just before the await, instead of just after it. Or would it not be?

If you drop temporaries before the .await, then this would not compile:

file.write(&vec![1, 2, 3]).await?;

The vector is a temporary, and the write future contains a reference to it. Thus, for this to compile, the vector must stay alive until after the await.

2 Likes

I think I get my misunderstanding.
In my mind, only an integer (return value of gen_range()) was captured into the future. But I guess the entire expression is evaluated once the future is "polled" including the evaluation of the parameters of sleep()? Then it would make sense.

It doesn't really matter whether the temporaries were captured by the future or not. Making the rules for when things are dropped depend on something like that would make them much much more complicated than they are now.

Fair enough. Thanks for the quick reply. :+1:

One aspect that might help further understanding the situation here is that features like the borrow checker in Rust (or similarly the thread-safety checks using Send/Sync traits) only work as a check, not as a means of changing program behavior. I.e. the behavior of Rust programs is defined in a more straightforward manner, only then comes the borrow checker and all it can do is accept the program as-is or reject it, not change it a little to make it pass.

Maximally smart temporary lifetime rules would probably need to change this principle; for the current rules, a function turning &Foo into &Bar by means of re-borrowing, or Baz into i32 where the returned value has nothing to do with the input anymore, is essentially just the same, the only thing that distinguishes these cases in the first place is that in on case the types contain lifetime arguments that are related, but lifetimes are mostly just for the borrow checker to check.

The rule that borrow checking only checks and nothing else has two great benefits

  • it makes Rust easier to understand as a language; since you don’t need to fully understand the borrow checker in order to determine what a Rust program is going to do, you just need to trust that it does a good job in preventing undefined behavior, without needing to care about the details.
  • it allows for improvements in the borrow checker down the line; in fact such a change is in progress for some time now, under the name polonius, there’s a new, better, borrow checker, and using it will not be a breaking change provided that it simply accepts strictly more programs than the old one did. I.e. an improved borrow checker will have fewer false-negatives (rejecting reasonable programs), and changing the borrow checker this way will only make programs compile that didn’t compile before, so there’s no breaking changes in program behavior.
4 Likes

I think I understand what you are saying.
In this situation we are talking about a thread/safety check. The checker only sees that the ThreadRng is dropped after the await. It does not recognize that its last use was before entering the await (assuming that only the integer from gen_range() entered into the future). So we have a "false negative" here.
I guess I should not hold my breath for a polonius for Send/Sync checking to show up :slightly_smiling_face:

My main point was that the safety checks cannot (or should not be able to) have the effect that the compiler says “let’s change code behavior and drop the value earlier because as-is the checks fail”.

The question whether the ThreadRng variable being unused after the .await mean it’s safe to make the future Send nonetheless, and I assume the answer is actually “no there is no false-positive here”: The reason is that even though the ThreadRng is no longer used, it does still get dropped (which is arguably some kind of usage). And ThreadRng must never be dropped in a different thread, because it uses a non-thread-safe Rc internally:

Assume the future is moved to a different thread between polls, and the poll reaching the point after the .await where the ThreadRng is dropped happens at the same time when the original thread is being terminated and the thread-local containing the original copy of the Rc for that ThreadRng is also dropped, then you’re dropping two copies of the same Rc in parallel without synchronization, which is a data race, and can lead e.g. to leaks or double-frees.

1 Like

The question whether the ThreadRng variable being unused after the .await

I was under the impression that the ThreadRng's last use was in the gen_range() call, before the .await, and that only an integer was sent into .await, not any Rc.
In this code, does the Rc really run a risk of being dropped elsewhere?

EDIT: Hmm, I gues the whole thread gets moved to another thread, not just the sleep function, including the Rc. Ok. Satisfied.

Yes, maybe you have an incomplete mental model of how futures and .await work (which wouldn’t be surprising since the details are quite complex). An .await introduces a yield point (in a loop) for the surrounding future. Here’s a rough indication of the desugaring. See also this post for more context. These yield points mean that the future you’re currently implementing suspends, its state is stored in some variant of a states enum, and it can be continued at this yield point later by the next poll of the executor. Only later, after this .await loop finished, and thus after that yield point may have been used, will the temporary variable containing the ThreadRng be dropped.

Since the ThreadRng is created before this yield point, and dropped afterwards, it must become part of this state enum that makes up the future; since ThreadRng cannot be sent between threads, this mean that the surrounding future can then no longer be sent between threads.

So it’s not about “sending” something “to the .await”, but instead about the surrounding Future being sent. Futures are being sent between threads commonly in multi-threaded executors, so that all worker threads are kept busy in case of high workloads. (Or even just so that the future can be sent to the executor pool in the first place.)


Ah, you’ve edited you post, perhaps you did understand some of this already; I’m posting this answer nonetheless.