3

I am new to Rust and trying to make some code async to run a bunch of tasks in parallel. Here is a simplified example:

use futures::future::join_all;

#[tokio::main]
async fn main() {
    let mut list = Vec::new();
    for i in 1..10 {
        let my_str = format!("Value is: {:?}", &i);
        let future = do_something(&my_str);
        list.push(future);
    }
    join_all(list).await;
}

async fn do_something(value: &str)
{
    println!("value is: {:?}", value);
}

This fails with "Borrowed value does not live long enough" on the do_something(&my_str) call. I can get the code to compile by changing do_something to accept a String instead of an &str. However, it seems a bit strange to require a String when an &str would work. Is there a better pattern to use here? Thanks!

Daniel
  • 794
  • 10
  • 20
  • 1
    Incidentally, see https://tokio.rs/tokio/glossary#concurrency-and-parallelism for a difference between concurrency and parallelism. Tokio does the former, not the later, and in general with asynchronous programming you will achieve concurrency, not parallelism. – jthulhu Apr 20 '22 at 06:31
  • @BlackBeans good point - the code I am writing is IO bound, but the simplified example here is not. – Daniel Apr 20 '22 at 07:01

1 Answers1

1

"However, it seems a bit strange to require a String when an &str would work." But an &str can't work here because it only borrows my_str which gets destroyed before the future completes:

for i in 1..10 {
    // Create a new `String` and store it in `my_str`
    let my_str = format!("Value is: {:?}", &i);
    // Create a future that borrows `my_str`. Note that the future is not
    // yet started
    let future = do_something(&my_str);
    // Store the future in `list`
    list.push(future);
    // Destroy `my_str` since it goes out of scope and wasn't moved.
}
// Run the futures from `list` until they complete. At this point each
// future will try to access the string that they have borrowed, but those
// strings have already been freed!
join_all(list).await;

Instead your do_something should take ownership of the string along with responsibility for freeing it:

use futures::future::join_all;

#[tokio::main]
async fn main() {
    let mut list = Vec::new();
    for i in 1..10 {
        // Create a new `String` and store it in `my_str`
        let my_str = format!("Value is: {:?}", &i);
        // Create a future and _move_ `my_str` into it.
        let future = do_something(my_str);
        // Store the future in `list`
        list.push(future);
        // `my_str` is not destroyed since it was moved into the future.
    }
    join_all(list).await;
}

async fn do_something(value: String)
{
    println!("value is: {:?}", value);
    // Destroy `value` since it goes out of scope and wasn't moved.
}
Jmb
  • 18,893
  • 2
  • 28
  • 55
  • 1
    Thanks, that's really helpful. My take away here is to avoid accepting &str in async functions since they might be executed after the parameters have gone out of scope. – Daniel Apr 20 '22 at 07:08
  • What if `do_something` is some 3rd party function that can not be changed? – Saddle Point Jun 22 '23 at 14:44
  • 2
    @SaddlePoint then you just wrap it in a future that adapts the interface, e.g. `let future = async move { do_something (&my_str).await };` – Jmb Jun 22 '23 at 15:28