Understanding tokio::spawn and tokio::spawn_blocking

I added numbers to the above post to make it easier to call out, and created GitHub - joshka/spike-tokio

I Added a loop counter (either AtomicUsize or Arc<AtomicUsize>) to each example to show what’s going on a bit better. You’ll note that there’s two broadly similar counts, ~70 where the loop is waiting 20ms between iterations, and ~6300 where it is printing characters to the terminal as fast as possible. The amount of time spent is similar because this is controlled by the time it takes to run the fibonacci method (I’m using 42 for the number).

1 Fibonacci number: 267914296, loop_count: 6431

cargo run --bin item1 3.82s user 0.09s system 164% cpu 2.374 total

2 Fibonacci number: 267914296, loop_count: 73

cargo run --bin item2 1.82s user 0.02s system 92% cpu 1.977 total

3 Fibonacci number: 267914296, loop_count: 6385

cargo run --bin item3 3.84s user 0.10s system 167% cpu 2.355 total

4 Fibonacci number: 267914296, loop_count: 74

cargo run --bin item4 1.82s user 0.02s system 92% cpu 1.999 total

5 Fibonacci number: 267914296, loop_count: 73

cargo run --bin item5 1.82s user 0.02s system 93% cpu 1.975 total

I’m not sure what’s not understandable about this one. It spawns a task, which starts running while fib is running on the main thread.

6 Never returns

Spawned tasks may be cancelled using the JoinHandle::abort or AbortHandle::abort methods. When one of these methods are called, the task is signalled to shut down next time it yields at an .await point. If the task is already idle, then it will be shut down as soon as possible without running again before being shut down. Additionally, shutting down a Tokio runtime (e.g. by returning from #[tokio::main]) immediately cancels all tasks on it.

..

Be aware that calls to JoinHandle::abort just schedule the task for cancellation, and will return before the cancellation has completed. To wait for cancellation to complete, wait for the task to finish by awaiting the JoinHandle. Similarly, the JoinHandle::is_finished method does not return true until the cancellation has finished.

There’s no await point in the loop, so it never gets interrupted. Add yield_now().await; and it does. Also calling std::process::exit(1) makes things die.
Also, anything that runs in the code after that point runs at the same time as the async task, so if for instance you’re printing the result of fib, then it will be interspersed with the random junk, so it’s important to await the handle.

So adding:

            yield_now().await;
        }
    });
    let result = fib(NUMBER);
    handle.abort();
    if let Err(err) = handle.await {
        eprintln!("Error: {:?}", err);
    }

gives:
Error: JoinError::Cancelled(Id(13))
Fibonacci number: 267914296, loop_count: 6305
cargo run --bin item6 3.88s user 0.14s system 154% cpu 2.609 total

7 Never completes

But it does run any code after the abort (e.g. printing the result / loop count. The problem here is also mentioned at tokio::task - Rust

Be aware that tasks spawned using spawn_blocking cannot be aborted because they are not async. If you call abort on a spawn_blocking task, then this will not have any effect, and the task will continue running normally. The exception is if the task has not started running yet; in that case, calling abort may prevent the task from starting.

To get this one to succeed, you’d want to add some form of cancelation token / message that the loop can check.

    let (tx, mut rx) = tokio::sync::oneshot::channel::<()>();

// then in the loop

        if rx.try_recv().is_ok() {
            break;
        }

// and after

    tx.send(()).unwrap();

Fibonacci number: 267914296, loop_count: 77
cargo run --bin item7 1.84s user 0.02s system 59% cpu 3.150 total

8 never completes

same deal as 7. blocking task can’t be aborted
Applying the same change

Fibonacci number: 267914296, loop_count: 6448
cargo run --bin item8 3.84s user 0.10s system 156% cpu 2.526 total


I suspect given this note about canceling, you’d probably get the same effect to calling abort as just letting the method end.

Additionally, shutting down a Tokio runtime (e.g. by returning from #[tokio::main]) immediately cancels all tasks on it.

See fix: make things work right · joshka/spike-tokio@18e6c4f · GitHub for corrections

1 Like