Boost Rust Performance: Master Multithreading vs Futures for Concurrency
This article explores Rust's concurrent programming techniques, comparing traditional multithreading with Futures combinators, detailing thread creation, joining, returning values, the move keyword, and async execution using Tokio, and provides guidance on selecting the appropriate strategy based on task characteristics.
In Rust, executing asynchronous operations concurrently can significantly improve program performance. This article examines two common concurrency strategies: multithreading and Futures combinators.
Multithread Overview
A thread is a sequence of instructions executed by the CPU, acting as a container for a running process. Multithreading allows multiple tasks to run simultaneously, improving performance but adding complexity.
Create and Manage Threads
Use std::thread::spawn to create a new thread:
<code>use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..100 {
println!("{} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
</code>This code creates a new thread that runs a loop printing numbers and sleeping; the main thread runs a similar loop.
Join Threads
When the main thread ends, child threads are terminated unless joined. Use a JoinHandle and its join method to wait for a thread to finish:
<code>fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("{} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("{} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().expect("error joining");
}
</code>The join call ensures the main thread does not exit before the child thread completes.
Join Position
The placement of join matters; calling it before the main loop forces the main thread to wait for the child thread before proceeding.
<code>fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("{} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
handle.join().expect("error joining");
for i in 1..5 {
println!("{} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
</code>Retrieve Return Values with JoinHandle
Threads can return values via JoinHandle . The example creates two threads that each return a number, which are then summed:
<code>fn main() {
let handle_1 = thread::spawn(|| {
for i in 1..10 {
println!("{} from the spawned thread 1!", i);
thread::sleep(Duration::from_millis(1));
}
100
});
let handle_2 = thread::spawn(|| {
for i in 1..10 {
println!("{} from the spawned thread 2!", i);
thread::sleep(Duration::from_millis(1));
}
200
});
let result_1 = handle_1.join().expect("error joining");
let result_2 = handle_2.join().expect("error joining");
println!("final result: {} from the main thread!", result_1 + result_2);
}
</code>Using the move Keyword
If a closure needs ownership of external variables, prepend move :
<code>fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("vector: {:?}", v);
});
handle.join().expect("error joining");
}
</code>Async Operations with Tokio
Creating Async Tasks with tokio::spawn
tokio::spawn creates asynchronous tasks that may run on the current thread or a worker thread, depending on the runtime configuration (default uses the rt-multi-thread feature).
<code>use std::{thread, time::Duration};
use tokio;
#[tokio::main]
async fn main() {
let spawn_1 = tokio::spawn(async {
for i in 1..5 {
println!("{} from the thread 1!", i);
thread::sleep(Duration::from_millis(1));
}
});
let spawn_2 = tokio::spawn(async {
for i in 1..5 {
println!("{} from the thread 2!", i);
thread::sleep(Duration::from_millis(1));
}
});
spawn_1.await.expect("error awaiting");
spawn_2.await.expect("error awaiting");
}
</code>Futures Combinators
Sequential Execution of Futures
An async function returns a Future that can be awaited. The example runs two async operations sequentially:
<code>#[tokio::main]
async fn main() {
let start = Instant::now();
let future_1 = async_operation(1);
let future_2 = async_operation(2);
future_1.await;
future_2.await;
println!("futures: {:?}", start.elapsed());
}
async fn async_operation(thread: i8) {
for i in 1..5 {
println!("{} from the operation {}!", i, thread);
tokio::time::sleep(Duration::from_millis(400)).await;
thread::sleep(Duration::from_millis(100));
}
}
</code>Concurrent Execution of Futures
Use futures::future::join_all to run multiple futures concurrently and await their completion:
<code>#[tokio::main]
async fn main() {
let start = Instant::now();
let future_1 = async_operation(1);
let future_2 = async_operation(2);
join_all([future_1, future_2]).await;
println!("futures: {:?}", start.elapsed());
}
</code>Summary
Multithreading vs Futures
Multithreading typically uses multiple OS threads, while Futures combinators often run on a single thread.
Multithreading suits long‑running, independent, memory‑ or CPU‑intensive tasks.
Futures combinators are better for short‑lived, I/O‑bound tasks that may not need a return value.
Choosing the Right Concurrency Strategy
Consider task type and complexity, dependencies between tasks, and available resources when selecting between multithreading and Futures.
Architecture Development Notes
Focused on architecture design, technology trend analysis, and practical development experience sharing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.