this post was submitted on 05 Sep 2024
58 points (100.0% liked)

Rust

5960 readers
8 users here now

Welcome to the Rust community! This is a place to discuss about the Rust programming language.

Wormhole

!performance@programming.dev

Credits

  • The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)

founded 1 year ago
MODERATORS
you are viewing a single comment's thread
view the rest of the comments
[–] BB_C@programming.dev 4 points 2 months ago (1 children)

I skimmed the latter parts of this post since I felt like I read it all before, but I think moro is new to me. I was intrigued to find out how scoped span exactly behaves.

async fn slp() {
    tokio::time::sleep(std::time::Duration::from_millis(1)).await
}

async fn _main() {
    let value = 22;
    let result_fut = moro::async_scope!(|scope| {
        dbg!(); // line 8
        let future1 = scope.spawn(async {
            slp().await;
            dbg!(); // line 11
            let future2 = scope.spawn(async {
                slp().await;
                dbg!(); // line 14
                value // access stack values that outlive scope
            });
            slp().await;
            dbg!(); // line 18

            let v = future2.await * 2;
            v
        });

        slp().await;
        dbg!(); // line 25
        let v = future1.await * 2;
        slp().await;
        dbg!(); // line 28
        v
    });
    slp().await;
    dbg!(); // line 32
    let result = result_fut.await;
    eprintln!("{result}"); // prints 88
}

fn main() {
    // same output with `new_current_thread()` of course
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
    rt.block_on(_main())
}

This prints:

[src/main.rs:32:5]
[src/main.rs:8:9]
[src/main.rs:25:9]
[src/main.rs:11:13]
[src/main.rs:18:13]
[src/main.rs:14:17]
[src/main.rs:28:9]
88

So scoped spawn doesn't really spawn tasks as one might mistakenly think!

[–] arendjr@programming.dev 4 points 2 months ago (1 children)

I think I would put the emphasis slightly differently: I don’t feel the confusion is around the word “spawn”, but it spawns futures rather than tasks. For tasks you might indeed expect them to be picked up in the background (which is what work-stealing does), but futures only execute when polled.

[–] BB_C@programming.dev 2 points 2 months ago

but futures only execute when polled.

The most interesting part here is the polling only has to take place on the scope itself. That was actually what I wanted to check, but got distracted because all spawns are awaited in the scope in moro's README example.

async fn slp() {
    tokio::time::sleep(std::time::Duration::from_millis(1)).await
}

async fn _main() {
    let result_fut = moro::async_scope!(|scope| {
        dbg!("d1");
        scope.spawn(async { 
            dbg!("f1a");
            slp().await;
            slp().await;
            slp().await;
            dbg!("f1b");
        });
        dbg!("d2"); // 11
        scope.spawn(async {
            dbg!("f2a");
            slp().await;
            slp().await;
            dbg!("f2b");
        });
        dbg!("d3"); // 14
        scope.spawn(async {
            dbg!("f3a");
            slp().await;
            dbg!("f3b");
        });
        dbg!("d4");
        async { dbg!("b1"); } // never executes
    });
    slp().await;
    dbg!("o1");
    let _ = result_fut.await;
}

fn main() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
    rt.block_on(_main())
}
[src/main.rs:32:5] "o1" = "o1"
[src/main.rs:7:9] "d1" = "d1"
[src/main.rs:15:9] "d2" = "d2"
[src/main.rs:22:9] "d3" = "d3"
[src/main.rs:28:9] "d4" = "d4"
[src/main.rs:9:13] "f1a" = "f1a"
[src/main.rs:17:13] "f2a" = "f2a"
[src/main.rs:24:13] "f3a" = "f3a"
[src/main.rs:26:13] "f3b" = "f3b"
[src/main.rs:20:13] "f2b" = "f2b"
[src/main.rs:13:13] "f1b" = "f1b"

The non-awaited jobs are run concurrently as the moro docs say. But what if we immediately await f2?

[src/main.rs:32:5] "o1" = "o1"
[src/main.rs:7:9] "d1" = "d1"
[src/main.rs:15:9] "d2" = "d2"
[src/main.rs:9:13] "f1a" = "f1a"
[src/main.rs:17:13] "f2a" = "f2a"
[src/main.rs:20:13] "f2b" = "f2b"
[src/main.rs:22:9] "d3" = "d3"
[src/main.rs:28:9] "d4" = "d4"
[src/main.rs:24:13] "f3a" = "f3a"
[src/main.rs:13:13] "f1b" = "f1b"
[src/main.rs:26:13] "f3b" = "f3b"

f1 and f2 are run concurrently, f3 is run after f2 finishes, but doesn't have to wait for f1 to finish, which is maybe obvious, but... (see below).

So two things here:

  1. Re-using the spawn terminology here irks me for some reason. I don't know what would be better though. Would defer_to_scope() be confusing if the job is awaited in the scope?
  2. Even if assumed obvious, a note about execution order when there is a mix of awaited and non-awaited jobs is worth adding to the documentation IMHO.