ilmoi
ilmoi

Reputation: 2534

Unexpected tokio::task::spawn_blocking behavior

I'm experimenting with tokio's tokio::spawn and tokio::task::spawn and turns out I don't understand how the latter behaves.

When I run the following code:

#[tokio::main]
pub async fn main() {
    // I'm spawning one block of functions
    let h = tokio::task::spawn_blocking(move || {
        block_one();
    });

    // and another block of functions
    let h2 = tokio::spawn(async move {
        block_two().await;
    });

    // then I collect the handles
    h.await.unwrap();
    h2.await.unwrap();
}

#[tokio::main] //needed as this block is not treated as syncronous by main
pub async fn block_one() {
    let mut handles = vec![];

    for i in 1..10 {
        let h = tokio::spawn(async move {
            println!("Starting func #{}", i);
            i_take_random_time().await;
            println!("Ending func #{}", i);
        });
        handles.push(h);
    }

    for h in handles {
        h.await.unwrap();
    }
}

pub async fn block_two() {
    let mut handles = vec![];

    for i in 10001..10010 {
        let h = tokio::spawn(async move {
            println!("Starting func #{}", i);
            i_take_random_time().await;
            println!("Ending func #{}", i);
        });
        handles.push(h);
    }

    for h in handles {
        h.await.unwrap();
    }
}

My expectation is that the first block of functions will run in full - only then the second block will run. That's how I understand "spawn_blocking" - it blocks futher execution until whatever's inside of it is done.

What I actually get is that the second block of functions starts first (in full, all 10 of them) - only then the first block starts. So it's exactly backwards from what I expected.

To further confuse things, when I modify the above code to have spawn_blocking for both blocks - all the 20 functions start together, as if both blocks are part of one big async loop. Again not what I expected - I would think the first block would run, blocking before it's done, and THEN the second would run.

Can someone help me decipher what's going on?

The full code to reproduce the 2 scenarios above is available in this repo.

Note: there's two levels of asynchronicity here: BETWEEN blocks and WITHIN blocks. Hope helps avoid any confusion.

Upvotes: 5

Views: 4975

Answers (1)

Alice Ryhl
Alice Ryhl

Reputation: 4239

It sounds like you expect spawn_blocking to block other things from running, but its purpose is the exact opposite. The purpose of spawn_blocking is to avoid blocking other things from running.

The main place where spawn_blocking is used is for operations that would otherwise block the thread due to their use of non-async operations such as std::net. It does this by offloading them to a separate thread pool. The name comes from the fact that you are spawning a blocking operation so it can run elsewhere.

To wait for the first block to finish, you would do this:

#[tokio::main]
pub async fn main() {
    // I'm spawning one block of functions
    let h = tokio::task::spawn_blocking(move || {
        block_one();
    });

    // wait for the first block
    h.await.unwrap();

    // then spawn another block of functions
    let h2 = tokio::spawn(async move {
        block_two().await;
    });

    h2.await.unwrap();
}

Note that using #[tokio::main] (or block_on) immediately inside spawn_blocking is very rarely what you want. Just spawn an ordinary task with tokio::spawn.

Upvotes: 11

Related Questions