Skip to main content

异步并发

async await

use std::time::Duration;
use tokio::time::sleep;

async fn hello() {
println!("Hello async Rust!");
}

async fn add(x: u32, y: u32) -> u32 {
sleep(Duration::from_millis(1000)).await;
x + y
}

#[tokio::main]
async fn main() {
hello().await;

let sum = add(1, 2).await;
println!("sum: {sum}");
}

thread

use std::thread;
use std::time::Duration;

fn main() {
// Spawning too many threads can crash this program (OS thread and memory limits)
let mut handles = vec![]; // To store thread join handles
for i in 0..1_000_000_000 { // Loop to spawn 1 million threads
handles.push(std::thread::spawn(move || { // Spawn a new OS thread
std::thread::sleep(Duration::from_millis(100)); // Simulate work (I/O wait)
println!("Thread: {} 🍔 is ready", i); // Print when done
}));
}

// Wait for all spawned threads to complete
for h in handles {
h.join().unwrap(); // Main thread waits for each spawned thread
}
}

// std::thread::spawn(move || { ... }) creates and starts a new OS thread. The `move` keyword transfers ownership of any captured variables (such as `i`) into the new thread's closure. It returns a JoinHandle.

// The `handles` vector: we store each JoinHandle in this vector. A JoinHandle lets us wait for the corresponding thread to finish.

// std::thread::sleep(Duration::from_millis(100)): this simulates an I/O-bound operation by pausing the current thread for 100 milliseconds.

// h.join().unwrap(): in the second loop, the main thread calls join() on each thread's JoinHandle. This blocks the main thread until that particular thread finishes. unwrap() is used here to keep things simple, so that a panic is triggered if the thread panicked.

async-await version

use tokio::time::{sleep, Duration}; // Use tokio's sleep

// Add Tokio as a dependency in Cargo.toml:
// tokio = { version = "1", features = ["full"] }
// And use the tokio::main macro for your main function.

#[tokio::main]
async fn main() {
let mut handles = vec![]; // To store Tokio task JoinHandles

for i in 0..3_000_000_00 { // Loop to spawn 1 million async tasks
// Create an async block (a future)
let fut = async move { // move transfers ownership of all variables into fut
sleep(Duration::from_millis(100)).await; // Asynchronous sleep
println!("Async: {} 🍔 is ready", i);
};
// Spawn the future as a Tokio task on the runtime
let handler = tokio::task::spawn(fut);
handles.push(handler);
}

// Wait for all spawned Tokio tasks to complete
for h in handles {
h.await.unwrap(); // Await the JoinHandle (which is also a future)
}
}

/* #[tokio::main]: this macro turns our `async fn main()` into a regular `fn main()` that initializes the Tokio runtime and runs the async code.

async move { ... } creates an async block. This block does not execute immediately; instead, it defines a "future." The `move` keyword ensures that all captured variables (such as `i`) are moved into the future.

tokio::time::sleep(Duration::from_millis(100)).await is Tokio's asynchronous sleep pattern. When .await is encountered here:
Execution of that particular async block is paused.
Control is handed back to the Tokio executor.
Crucially, the OS thread running this async block is not blocked. The executor can use that thread to run other async tasks that are ready.
After 100 milliseconds, Tokio schedules this task to resume from where it left off.

tokio::task::spawn(fut) takes a future `fut` and schedules it to run on Tokio's thread pool. This is a non-blocking operation that immediately returns a JoinHandle (specifically, a tokio::task::JoinHandle). The JoinHandle is itself also a future, which resolves once the spawned task completes.

h.await.unwrap(): in the final loop, the `main` async function awaits each task's JoinHandle. This ensures that the program waits for all one million "burger-making" tasks to finish before `main` exits.
*/

join! — all tasks must complete

Purpose: join! is used when you need to run multiple async operations simultaneously and wait for all of them to complete before continuing with other work.

Behavior: it polls all the provided futures and drives them to completion. The join! macro itself only completes once all the futures passed to it have completed.

Return value: once complete, join! returns a tuple. This tuple contains the result of each future, in the same order as the futures were passed to the macro.

Analogy: you can think of join! as saying, "Wait for all of these results to come back. I need every one of them."

select! — as soon as one completes

Purpose: use select! when you have multiple async operations and you only care about the result of the first one to complete.

Behavior: it polls all the provided futures concurrently. As soon as any one of the futures completes, select! returns.

Cancellation: this is a key difference: once a future completes and select! is ready to return, all the other futures that were being polled but had not yet completed are immediately cancelled. Their execution is stopped and they are dropped. This avoids unnecessary work and resource consumption.

Return value: select! returns the result of the single future that completed first.

Analogy: the principle of select! is, "Just give me one of these results — whichever one comes back first."

📢 Share this article