Loading...
Loading...

Concurrency in Rust

Concurrency in Rust enables executing multiple tasks simultaneously while preventing data races at compile time through its ownership system. Rust offers thread-based parallelism, async/await for cooperative multitasking, and message passing between threads.

1. Threads

Threads in Rust provide OS-level parallelism. Each thread has its own stack and runs independently, scheduled by the operating system. Rust's ownership system ensures thread safety.

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

fn main() {
    // Spawn a new thread
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("Thread: {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    // Main thread work
    for i in 1..3 {
        println!("Main: {}", i);
        thread::sleep(Duration::from_millis(1));
    }

    // Wait for thread to finish
    handle.join().unwrap();
}

Key Points: thread::spawn creates a new thread. join() waits for thread completion. The main thread exits when it finishes unless you join spawned threads. Threads are expensive to create and switch between.

Thread Quiz

What happens if you don't call join() on a thread handle?

  • The thread continues running forever
  • The thread may be terminated when main thread ends
  • It causes a compile error

2. Shared-State Concurrency

Shared memory between threads requires synchronization. Rust provides Mutex (mutual exclusion) and Arc (atomic reference counting) for thread-safe shared access.

use std::sync::{Arc, Mutex};

fn main() {
    // Arc = Atomic Reference Counting
    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!("Final count: {}", *counter.lock().unwrap());
}

Safety Mechanisms: Arc enables multiple ownership across threads. Mutex ensures only one thread accesses data at a time. The compiler prevents data races at compile time.

Shared-State Quiz

Why do we need both Arc and Mutex for shared state?

  • Arc alone is enough for thread safety
  • Arc handles ownership, Mutex handles access
  • Mutex doesn't work without Arc

3. Message Passing with Channels

Channels enable thread communication by sending messages. Rust's standard library provides multiple producer, single consumer (mpsc) channels.

use std::sync::mpsc; // Multiple Producer, Single Consumer

fn main() {
    let (tx, rx) = mpsc::channel();

    // Spawn thread to send message
    thread::spawn(move || {
        let val = String::from("hello");
        tx.send(val).unwrap();
    });

    // Receive message in main thread
    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

Channel Characteristics: Sending moves ownership of values. Channels can buffer multiple messages. Receiving blocks until a message arrives. Multiple senders can share one receiver.

Channel Quiz

What happens if all senders are dropped and the channel is empty?

  • The receiver panics
  • recv() returns Err indicating no more messages
  • The program deadlocks

4. Async Programming

Async/await provides lightweight concurrency using cooperative multitasking. Tasks yield control during I/O waits, enabling high concurrency with less overhead than threads.

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        println!("Async task completed");
    });

    println!("Main thread continues working");
    handle.await.unwrap();
}

Async Benefits: Much lighter weight than threads. Excellent for I/O-bound tasks. Built on futures that represent pending computations. Requires an async runtime like Tokio.

Async Quiz

When does an async task yield control back to the runtime?

  • After every operation
  • When encountering .await on not-ready futures
  • Only at the end of the async block

5. Synchronization Primitives

Synchronization tools coordinate thread execution. Rust provides RwLock for multiple readers, Barrier for thread rendezvous, and more.

use std::sync::{RwLock, Barrier};

fn main() {
    // Read-Write Lock
    let lock = RwLock::new(5);
    {
        let r1 = lock.read().unwrap();
        let r2 = lock.read().unwrap(); // Multiple readers allowed
    }
    {
        let mut w = lock.write().unwrap(); // Only one writer
        *w += 1;
    }

    // Barrier synchronization
    let barrier = Arc::new(Barrier::new(3));
    let mut handles = vec![];
    
    for _ in 0..3 {
        let b = Arc::clone(&barrier);
        handles.push(thread::spawn(move || {
            println!("Before wait");
            b.wait();
            println!("After wait");
        }));
    }
    
    for h in handles {
        h.join().unwrap();
    }
}

Primitive Uses: RwLock is great for read-heavy workloads. Barriers synchronize thread phases. Condvars enable waiting for conditions. Always prefer the least restrictive primitive needed.

Sync Quiz

What's the advantage of RwLock over Mutex?

  • RwLock is always faster
  • Allows multiple concurrent readers
  • Doesn't require locking

6. Atomic Types

Atomic operations provide lock-free thread-safe access for primitive types. They're implemented using processor instructions for maximum performance.

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

fn main() {
    let atomic_counter = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&atomic_counter);
        handles.push(thread::spawn(move || {
            counter.fetch_add(1, Ordering::SeqCst);
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    println!("Atomic counter: {}", atomic_counter.load(Ordering::SeqCst));
}

Atomic Characteristics: No locking overhead. Limited to simple operations. Memory ordering affects visibility guarantees. Perfect for counters and flags.

Atomic Quiz

What does Ordering::SeqCst guarantee?

  • Highest performance
  • Sequential consistency (strongest ordering)
  • No synchronization needed

7. Advanced Async Patterns

Async patterns solve common concurrency problems. Channels, semaphores, and select macros enable complex async workflows.

use tokio::sync::{Semaphore, mpsc};

#[tokio::main]
async fn main() {
    // Async channel
    let (tx, mut rx) = mpsc::channel(32);
    
    tokio::spawn(async move {
        tx.send("message").await.unwrap();
    });

    if let Some(msg) = rx.recv().await {
        println!("Received: {}", msg);
    }

    // Rate limiting with semaphore
    let sem = Arc::new(Semaphore::new(3)); // Allow 3 concurrent
    
    for i in 0..10 {
        let permit = sem.clone().acquire_owned().await.unwrap();
        tokio::spawn(async move {
            // Critical section
            println!("Task {} working", i);
            tokio::time::sleep(Duration::from_secs(1)).await;
            drop(permit); // Release semaphore
        });
    }
}

Pattern Benefits: Async channels enable communication between tasks. Semaphores limit concurrent access. Select waits on multiple operations. Buffering prevents backpressure.

Async Patterns Quiz

When would you use a semaphore in async code?

  • To replace all mutexes
  • To limit concurrent access to a resource
  • Only for thread synchronization
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home