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.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 scopedspan
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!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.
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
.Without some way to write generic code for either runtime, the whole tokio ecosystem would end up bifurcated by this choice of runtime.
Would it makes sense to provide a “share nothing” runtime implementation that can be injected at startup?
Isn’t this
tokio::task::spawn_local
?
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: https://www.reddit.com/r/rust/comments/14cbe1u/why_does_actixwebs_handler_not_require_send/
Actix is a bit weird, but it has been around, and used in production, for a relatively long time.