David's Blog

Rust's Concurrency and Parallelism Libraries A Guide to Writing Concurrent and Parallel Code

By David Li on 2025-03-21T00:02:41.000Z

Rust’s Concurrency and Parallelism Libraries: A Guide to Writing Concurrent and Parallel Code

Rust, a systems programming language, is known for its focus on safety and performance. One of Rust’s key features is its ability to handle concurrency and parallelism efficiently. In this article, we will explore Rust’s concurrency and parallelism libraries and provide examples of how to use them to write concurrent and parallel code.

Concurrency vs. Parallelism

Before diving into Rust’s libraries, let’s clarify the difference between concurrency and parallelism:

  • Concurrency refers to the ability of a system to manage multiple tasks at the same time, where tasks may start, run, and complete in overlapping time periods.
  • Parallelism is the ability to execute multiple tasks simultaneously, often by leveraging multiple cores or processors.

Concurrency is about structure, while parallelism is about execution. Rust provides tools for both, allowing you to write concurrent code that can be executed in parallel.

Rust’s Concurrency Model

Rust’s concurrency model is built on two key principles:

  1. Ownership: Rust’s ownership system enforces strict control over which parts of the code can access and modify a given piece of data. This helps prevent common data races that can occur in concurrent programming.

  2. Fearless concurrency: Rust provides several abstractions and libraries to write concurrent code without the need for manual synchronization using locks or other mechanisms.

Standard Library Concurrency and Parallelism Primitives

Rust’s standard library provides several concurrency and parallelism primitives, such as threads, channels, and atomic operations.

Threads

The std::thread module provides a way to create and manage threads. You can create a new thread with the spawn function, which takes a closure as an argument:

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

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("thread: {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("main: {}", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

Channels

Channels are used for communication between threads. Rust provides the std::sync::mpsc module, which stands for “multiple producer, single consumer.” This module contains the channel function, which returns a pair of sender and receiver endpoints.

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    let _ = thread::spawn(move || {
        let val = String::from("Hello, Rust!");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Received: {}", received);
}

Atomic Operations

Atomic operations are low-level synchronization primitives provided by the std::sync::atomic module. They are used for situations where higher-level abstractions like mutexes or channels are not suitable. Atomic types include AtomicBool, AtomicIsize, AtomicUsize, and others.

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

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

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

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

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

Parallelism with Rayon

Rayon is a third-party library that provides work-stealing parallelism for Rust. It allows you to easily parallelize tasks with simple changes to your existing code. Rayon’s main feature is the ParallelIterator, an extension of Rust’s standard Iterator.

To use Rayon, add it to your Cargo.toml file:

[dependencies]
rayon = "1.5.1"

Here’s an example of using Rayon to parallelize a simple computation:

use rayon::prelude::*;

fn main() {
    let input = (0..100_000).collect::<Vec<_>>();
    let output: Vec<_> = input
        .par_iter()
        .map(|&x| x * x)
        .collect();
    
    println!("output: {:?}", output);
}

In this example, the par_iter method is used to create a parallel iterator, and the map function is executed in parallel across multiplethreads. The collect function then gathers the results back into a single Vec.

Mutex and RwLock

In some scenarios, you might need to protect shared data with synchronization primitives like mutexes and read-write locks. Rust provides the Mutex and RwLock types in the std::sync module.

A Mutex ensures that only one thread can access the data inside it at a time. Here’s an example of using a Mutex to protect access to a shared counter:

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 || {
            for _ in 0..1000 {
                let mut num = counter.lock().unwrap();
                *num += 1;
            }
        });
        handles.push(handle);
    }

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

    println!("Counter: {}", *counter.lock().unwrap());
}

RwLock works similarly to Mutex, but it allows multiple readers to concurrently access the data while still providing exclusive access to a single writer:

use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(vec![0; 10]));
    let mut handles = vec![];

    for _ in 0..5 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let mut write_data = data.write().unwrap();
            for i in 0..10 {
                write_data[i] += 1;
            }
        });
        handles.push(handle);
    }

    for _ in 0..5 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let read_data = data.read().unwrap();
            println!("Read data: {:?}", *read_data);
        });
        handles.push(handle);
    }

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

In this example, the write and read methods on the RwLock provide mutable and immutable access to the data, respectively.

Conclusion

Rust’s concurrency and parallelism libraries enable developers to write concurrent code that can be executed in parallel without the need for manual synchronization. Rust’s ownership system and fearless concurrency, along with concurrency primitives like threads, channels, and atomic operations, make it easier to manage complex concurrent tasks. Additionally, third-party libraries like Rayon provide powerful abstractions for parallelism, allowing developers to parallelize tasks with minimal changes to their code.

By understanding and leveraging Rust’s concurrency and parallelism libraries, you can write safe, efficient, and highly concurrent code that takes full advantage of modern hardware.

© Copyright 2024 by FriendlyUsers Tech Blog. Built with ♥ by FriendlyUser. Last updated on 2024-04-29.