Puzzle 19: Explanation
Let’s learn how threads and delays work in Rust.
Test it out
Hit “Run” to see the code’s output.
[package] name = "sleepless" version = "0.1.0" edition = "2018" [dependencies] tokio = { version = "1.7", features = ["full"] }
Explanation
The outcome is surprising because the join
macro promises to run the three instances of count_and_wait()
concurrently. But, the output shows that the tasks are running sequentially, which tends to surprise newcomers to Rust’s async
system. Understanding the differences between asynchronous and thread programming can help us avoid pitfalls and pick the right model for our program.
Asynchronous programs and multi-threaded programs operate differently, and each has its own strengths and weaknesses. Asynchronous (future-based) tasks are not the same as threaded tasks. They require some care to ensure that they operate concurrently. However, it’s entirely possible to run an asynchronous program on one thread. The following diagram shows the basic differences between threaded and asynchronous execution:
In a threaded model, each task operates inside a full operating system-supported thread. Threads are scheduled independently of other threads and processes. An asynchronous model stores tasks in a task queue and runs them until they yield control back to the executing program.
Let’s examine a few approaches to running this teaser concurrently.
Native threads
Threads are pre-emptively scheduled by your operating system. While the thread is suspended, other threads continue to run. A purely threaded version of this teaser looks like this:
use std::thread;use std::time::Duration;fn count_and_wait(n: u64) -> u64 {println!("Starting {}", n);std::thread::sleep(Duration::from_millis(n * 100));println!("Returning {}", n);n}fn main() -> Result<(), Box<dyn std::error::Error>> {let a = thread::spawn(|| count_and_wait(1));let b = thread::spawn(|| count_and_wait(2));let c = thread::spawn(|| count_and_wait(3));a.join().unwrap();b.join().unwrap();c.join().unwrap();Ok(())}
The program spawns three threads, and they each run concurrently. Because the program calls sleep
and delays execution on each ...