Scoped threads in Rust
Amit Nadiger
Polyglot(Rust??, Move, C++, C, Kotlin, Java) Blockchain, Polkadot, UTXO, Substrate, Sui, Aptos, Solana, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Cas, Engineering management.
Rust is known for its strong emphasis on memory safety and concurrency. One of the powerful features it offers for concurrent programming is scoped threads. Scoped threads, managed by std::thread::scope, provide a safe and convenient way to spawn threads that are guaranteed to complete before the scope exits. This ensures that threads do not outlive the data they are working with, preventing common concurrency issues such as data races and dangling references.
What are Scoped Threads?
Scoped threads are threads that are confined to a specific lexical scope. The primary advantage of scoped threads is that they provide a way to safely access local variables without complex synchronization mechanisms. When the scope exits, all spawned threads are automatically joined, ensuring that they complete execution and preventing any threads from running after the scope has ended.
use std::thread;
fn main() {
let data = vec![1, 2, 3];
println!("Main thread started ");
thread::scope(|scope| {
scope.spawn(|| {
for num in &data {
println!("Scoped thread: {}", num);
}
});
});
// All scoped threads are joined here before exiting the scope
println!("Main thread continued ");
}
/*
Main thread started
Scoped thread: 1
Scoped thread: 2
Scoped thread: 3
Main thread continued
*/
In the above example, 1 threads are created within the same scope. The spawned threads can safely access the vector because it is guaranteed to complete before the scope exit data.
Example 2: Modifying Shared Data
In the below example, the vector is wrapped in an to allow safe concurrent modification. Each thread locks the mutex before modifying the vector. Because the threads are scoped, we are assured that all modifications are completed before the scope exits.dataArc<Mutex<_>>
use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
println!("Main thread started : {:?}", data.lock().unwrap());
let data_clone = Arc::clone(&data);
thread::scope(|scope| {
let data = Arc::clone(&data_clone);
scope.spawn(move || {
let mut data = data.lock().unwrap();
data.push(4);
println!("Thread 1: {:?}", data);
});
let data = Arc::clone(&data_clone);
scope.spawn(move || {
let mut data = data.lock().unwrap();
data.push(5);
println!("Thread 2: {:?}", data);
});
});
// All scoped threads are joined here before exiting the scope
println!("Main thread ended : {:?}", data.lock().unwrap());
}
/*
Main thread started : [1, 2, 3]
Thread 1: [1, 2, 3, 4]
Thread 2: [1, 2, 3, 4, 5]
Main thread ended : [1, 2, 3, 4, 5]
*/
Example 3.0: Parallel Computation
Below is simpleexample demonstrates how scoped threads can be used to perform parallel computation.
Please note in below example the vector v is barrowed and not moved or cloned.
fn main() {
let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let midpoint = v.len() / 2;
std::thread::scope(|scope| {
scope.spawn(|| {
let first = &v[..midpoint];
println!("Here's the first half of v: {first:?}");
});
scope.spawn(|| {
let second = &v[midpoint..];
println!("Here's the second half of v: {second:?}");
});
});
println!("Here's v: {v:?}");
}
/*
Here's the first half of v: [1, 2, 3, 4, 5]
Here's the second half of v: [6, 7, 8, 9, 10]
Here's v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
*/
Example 3: Parallel Computation
Below is more advanced example where we calculate the square of each number in the vector in parallel. Each thread computes the square of a single number and stores the result in the numbers results vector. Because the threads are scoped, we are guaranteed that all computations are completed before the scope exits.
Note : In below example vector numbers is barrowed with each thread and not moved or cloned.
Since results vector is shared with each thread mutably, we need to protect it with Arc/Mutex. Since variables cant be shared mutable more than once
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let results = Arc::new(Mutex::new(vec![0; numbers.len()]));
thread::scope(|scope| {
for (i, &num) in numbers.iter().enumerate() {
let results = Arc::clone(&results);
scope.spawn(move || {
let mut results = results.lock().unwrap();
results[i] = num * num;
println!("Thread {}: {}", i, results[i]);
});
}
}); // All scoped threads are joined here before exiting the scope
println!("Results: {:?}", results.lock().unwrap());
}
/*
Thread 0: 1
Thread 1: 4
Thread 2: 9
Thread 3: 16
Thread 4: 25
Thread 6: 49
Thread 5: 36
Thread 7: 64
Thread 8: 81
Thread 9: 100
Results: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
*/
领英推荐
Benefits of Scoped Threads
Key points of scoped threads ;
Differences between thread::spawn and thread::scope
Normal Threads (thread::spawn)
Normal threads in Rust are created using the function. This function launches a new OS thread to execute a given closure.
Here’s a basic example:thread::spawn
use std::thread;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("Hello from the spawned thread: {}", i);
}
});
for i in 1..5 {
println!("Hello from the main thread: {}", i);
}
// Wait for the spawned thread to finish
// handle.join().unwrap(); // --> Commeted here
}
When last line "handle.join().unwrap();" is commenetd below is output in my case., But it could be anything random where main thread will print but cant gurantee the print from spawned thread.
/*
Hello from the spawned thread: 1
Hello from the main thread: 1
Hello from the main thread: 2
Hello from the main thread: 3
Hello from the main thread: 4
Hello from the spawned thread: 2
Hello from the spawned thread: 3
Hello from the spawned thread:
*/
Below is the output when you have "handle.join().unwrap();" is un-commenetd
Hello from the main thread: 1
Hello from the main thread: 2
Hello from the main thread: 3
Hello from the main thread: 4
Hello from the spawned thread: 1
Hello from the spawned thread: 2
Hello from the spawned thread: 3
Hello from the spawned thread: 4
Hello from the spawned thread: 5
Hello from the spawned thread: 6
Hello from the spawned thread: 7
Hello from the spawned thread: 8
Hello from the spawned thread: 9
Then you can understand what is difference between with and without "handle.join()" in case of normal threads.
Characteristics of Normal Threads
Characteristics of Scoped Threads
Choosing between normal threads and scoped threads in Rust depends on your specific use case and requirements: