Concurrency in Rust is based on ownership, types, and borrowing principles, which help manage memory safety without a garbage collector.
Rust achieves thread safety by ensuring that shared data is either immutable or only accessible from one thread at a time.
Key Concepts:
- Threads: Rust uses threads to run multiple parts of your code simultaneously. The
std::threadmodule allows you to create new threads. - Message Passing: Rust’s concurrency model follows the “Do not communicate by sharing memory; instead, share memory by communicating” philosophy. Channels in Rust, provided by the
std::sync::mpscmodule, facilitate this. - Shared State Concurrency: Rust allows for shared-state concurrency using
MutexandArcfrom thestd::syncmodule to safely share and mutate data across threads. - Sync and Send Traits: These marker traits ensure that only safe types are shared between threads.
Sendallows a type to be transferred across thread boundaries, whileSyncallows a type to be safely shared through references.
Let’s now put it into practice with exercises to help you grasp or refresh these key concepts! 🦀
Exercises
Exercise 1: Spawning Threads
Create a simple program that spawns multiple threads and prints “Hello from thread!” from each thread.
use std::thread;
use std::time::Duration;
fn main() {
for i in 0..5 {
thread::spawn(move || {
println!("Hello from thread {}", i);
thread::sleep(Duration::from_millis(100));
});
}
thread::sleep(Duration::from_secs(1)); // Wait for all threads to complete
}
Exercise 2: Message Passing
Implement a program where you spawn two threads: one for sending a message using a channel, and another for receiving and printing that message.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let msg = String::from("Hello from the sender!");
tx.send(msg).unwrap();
});
let received = rx.recv().unwrap();
println!("Received: {}", received);
}
Exercise 3: Shared State with Mutex
Create a program that uses a Mutex to safely increment a counter from multiple threads.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Exercise 4: Building a Concurrent Web Server
This is a more advanced exercise. Implement a basic concurrent web server using TcpListener and threads. Accept connections, read the request, and respond with a fixed message.
use std::io::prelude::*;
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::time::Duration;
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\nHello from Rust server!";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
thread::spawn(|| {
handle_connection(stream);
});
}
}
Advanced Exercise: Implementing a Thread Pool
An effective way to manage resources in a concurrent program is by using a thread pool. A thread pool creates a fixed number of threads at program start-up and reuses these threads to execute tasks. This exercise involves implementing a simple thread pool to handle tasks in a concurrent web server.
Step 1: Define the ThreadPool Structure
Start by defining the structure of your thread pool and the initial function to create a new thread pool with a specified number of threads.
struct ThreadPool {
workers: Vec<Worker>,
}
impl ThreadPool {
fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut workers = Vec::with_capacity(size);
for _ in 0..size {
// Create and store workers
}
ThreadPool { workers }
}
}
Step 2: Implement the Worker
Each worker will be responsible for executing tasks. A worker holds a thread and listens for tasks sent through a channel.
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {
// Placeholder for the task execution logic
});
Worker {
id,
thread: Some(thread),
}
}
}
Step 3: Sending Tasks to Threads
Modify the ThreadPool to hold a sending end of a channel. Workers will listen on the receiving end for tasks to execute. Tasks can be represented as closures.
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;


