Here's a pretty good mental model of how futures work in Rust:
Imagine you had a list of one thousand elements called futures. Each future has a function called poll
, which does a little bit of work and then returns quickly. By looping through the list and calling poll
on each future, you can do work on all one thousand tasks at once in a single thread.
However, note that this requires the futures to cooperate. If a future spends a long time inside poll
, then your thread doesn't have time to go work on the other tasks. In your case you are using thread::sleep
, which has exactly this behaviour — it makes poll
take a long time. You can see this because it's an operation that takes time without an .await
.
The Tokio library provides two things: The loop that polls all the futures you spawn on it, and various replacements for sleeping and IO that cooperate by not spending a lot of time inside poll
. For example, you should use delay_for
for sleeping inside async functions.
That said, you problem is still not solved, because as you observed, .await
will wait for the thing you await. I did have to mention it though, because you were using the thread::sleep
function. To spawn a task that runs independently, you can use the tokio::spawn
function, which returns a JoinHandle
you can await later. This will make the task you spawned start running immediately, and allow you to wait for it later (e.g. you can put them in a vector and loop through them later).
There are also other tools for running things concurrently such as:
- the
join!
macro that waits for a fixed number of tasks concurrently, - the
select!
macro that waits for a fixed number tasks, but exits when one finishes - the
join_all
function, which is likejoin!
but for vectors of tasks - the
FuturesUnordered
collection, which allows you to wait for the next task and add tasks as you go
Note that when you exit main, all other tasks are immediately cancelled.