LocalKey, references, and async functions #7939
-
|
I discovered use tokio::time::{Duration, sleep};
async fn wait_a_while_and_greet(delay: Duration, name: &str) -> String {
println!("...");
sleep(delay).await;
format!("Hello, {}.", name)
}
#[tokio::main]
async fn main() {
let delay = Duration::from_secs(1);
// This works fine, because the name has a static lifetime.
let greeting = wait_a_while_and_greet(delay, "Alice").await;
println!("{}", greeting);
// This works fine, because although we're putting a reference into the future,
// we consume the future immediately.
let greeting = wait_a_while_and_greet(delay, &("Bob".to_string())).await;
println!("{}", greeting);
}Ok, this is basic, works as expected. Now let's try using a use tokio::time::{Duration, sleep};
async fn wait_a_while_and_greet(delay: Duration, name: &str) -> String {
println!("...");
sleep(delay).await;
format!("Hello, {}.", name)
}
tokio::task_local! {
static NAME: String
}
// Cloning is no big deal, right?
async fn greet_a_clone(delay: Duration) -> String {
wait_a_while_and_greet(delay, &NAME.get()).await
}
#[tokio::main]
async fn main() {
let delay = Duration::from_secs(1);
// This will work, but what if our clone gets anxiety?
let greeting = NAME
.scope(
"Carol".to_string(),
async move { greet_a_clone(delay).await },
)
.await;
println!("{}", greeting);
}This also works, but it requires cloning. And so we get to my problem: I want to put something into the use tokio::time::{Duration, sleep};
async fn wait_a_while_and_greet(delay: Duration, name: &str) -> String {
println!("...");
sleep(delay).await;
format!("Hello, {}.", name)
}
tokio::task_local! {
static NAME: String
}
// What if cloning is fraught with ethical problems?
async fn greet_by_reference(delay: Duration) -> String {
// This doesn't work, because the wrapper reference is used by the future and we get lifetime issues.
// NAME.with(|name| wait_a_while_and_greet(delay, name)).await
// What if we have an async closure inside .with?
// This doesn't work, because of https://github.com/rust-lang/rust/issues/108114
// NAME.with(async move |name| wait_a_while_and_greet(delay, name).await).await
todo!("Can this be done?");
}
#[tokio::main]
async fn main() {
let delay = Duration::from_secs(1);
let greeting = NAME
.scope("Dave".to_string(), async move {
greet_by_reference(delay).await
})
.await;
println!("{}", greeting);
}I was surprised that |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
|
The problem is that the value in the async fn wait_a_while_and_greet(delay: Duration, name: &str) -> String {
println!("...");
sleep(delay).await; // <-- the String may be deleted here
format!("Hello, {}.", name) // <-- the &str may be dangling now
}However, you can use an use std::sync::Arc;
use tokio::time::{Duration, sleep};
async fn wait_a_while_and_greet(delay: Duration, name: &str) -> String {
println!("...");
sleep(delay).await; // <-- value in task local may change
format!("Hello, {}.", name) // <-- but `name` still valid here (with old value)
}
tokio::task_local! {
static NAME: Arc<str>
}
// Cloning is no big deal, right?
async fn greet_a_clone(delay: Duration) -> String {
wait_a_while_and_greet(delay, &NAME.get().clone()).await
}
#[tokio::main]
async fn main() {
let delay = Duration::from_secs(1);
// This will work, but what if our clone gets anxiety?
let greeting = NAME
.scope(
"Carol".into(),
async move { greet_a_clone(delay).await },
)
.await;
println!("{}", greeting);
} |
Beta Was this translation helpful? Give feedback.
-
|
Yeah, that tradeoff is annoying, but it’s also the part that keeps this sound. The problem isn’t just “async Rust likes clones”. It’s that once you hit For this case I’d probably make the task-local value cheap to clone instead of trying to borrow it across awaits: use std::sync::Arc;
tokio::task_local! {
static NAME: Arc<str>;
}
async fn greet(delay: Duration) -> String {
let name = NAME.get().clone();
wait_a_while_and_greet(delay, &name).await
}That clone is just an If this is request/app state and not really “task-local context”, another option is to pass an So yeah, clones/Arcs feel less elegant than sync borrowing, but here they’re basically the ownership boundary across |
Beta Was this translation helpful? Give feedback.
Yeah, that tradeoff is annoying, but it’s also the part that keeps this sound.
The problem isn’t just “async Rust likes clones”. It’s that once you hit
.await, the current future can be suspended, and the task-local scope may no longer be the same when it resumes. So holding a borrowed&strfromLocalKeyacross.awaitwould let the future keep a reference to something Tokio can’t guarantee is still alive.For this case I’d probably make the task-local value cheap to clone instead of trying to borrow it across awaits: