Ancestors

Toot

Written by arendjr@programming.dev on 2024-09-05 at 10:39

Async Rust can be a pleasure to work with (without Send + Sync + 'static)

https://programming.dev/post/19010984

=> More informations about this toot | More toots from arendjr@programming.dev

Descendants

Written by arendjr@programming.dev on 2024-09-05 at 11:37

I was aware that indeed the trait and lifetime bounds were an artifact of the Tokio work-stealing behavior, but Evan makes a very well-explained case for why we might want to consider stepping away from such behavior as a default in Rust. If anything, it makes me thankful the Rust team is taking a slow-and-steady approach to the whole async thing instead of just making Tokio part of the standard library as some have wished for. Hopefully this gets the consideration it deserves and we all end up with a more ergonomic solution in the end.

=> More informations about this toot | More toots from arendjr@programming.dev

Written by BB_C@programming.dev on 2024-09-05 at 15:57

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.

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: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!

=> More informations about this toot | More toots from BB_C@programming.dev

Written by arendjr@programming.dev on 2024-09-05 at 16:15

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.

=> More informations about this toot | More toots from arendjr@programming.dev

Written by BB_C@programming.dev on 2024-09-06 at 00:59

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.

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: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: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:

=> More informations about this toot | More toots from BB_C@programming.dev

Written by alienscience@programming.dev on 2024-09-05 at 17:31

Despite using Tokio underneath, I think that Actix does NOT do work stealing and uses mostly separate threads:

Given this architecture, I think the article might inaccurate when it says that Actix handlers must be Send + Sync. See also: reddit.com/…/why_does_actixwebs_handler_not_requi…

Actix is a bit weird, but it has been around, and used in production, for a relatively long time.

=> More informations about this toot | More toots from alienscience@programming.dev

Written by tatterdemalion@programming.dev on 2024-09-05 at 18:49

I’m not sure what tokio (or axum) can do to avoid the trait bounds. Would it makes sense to provide a “share nothing” runtime implementation that can be injected at startup? I wonder how the intermediate layers (e.g. axum) would indicate that futures are usable by a more generic runtime which may or may not need Send + 'static.

=> More informations about this toot | More toots from tatterdemalion@programming.dev

Written by Giooschi@lemmy.world on 2024-09-05 at 19:47

Would it makes sense to provide a “share nothing” runtime implementation that can be injected at startup?

Isn’t this tokio::task::spawn_local?

=> More informations about this toot | More toots from Giooschi@lemmy.world

Written by tatterdemalion@programming.dev on 2024-09-05 at 20:06

Not exactly. I’m talking specifically about being able to call axum::serve with non-Send futures.

=> More informations about this toot | More toots from tatterdemalion@programming.dev

Proxy Information
Original URL
gemini://mastogem.picasoft.net/thread/113084515739473143
Status Code
Success (20)
Meta
text/gemini
Capsule Response Time
302.66409 milliseconds
Gemini-to-HTML Time
4.789081 milliseconds

This content has been proxied by September (ba2dc).