Rust Concurrency Explained: A Beginner's Guide to Arc and Mutex
Tags: rust, programming, tutorial, concurrency
Published: 2023-11-17 19:47:32

## Introduction

Jumping into Rust programming has been quite a ride. At first, it felt a bit like trying to solve a puzzle, especially when it came to understanding how Rust handles data. The borrowing concept? Got it, no big deal. But things got trickier when I started wondering how you could let two parts of your program share and change the same data without stepping on each other's toes. That's where `Arc` and `Mutex` come into play.

So, I thought, why not make an example to figure this out? The idea was to set up two threads in a Rust program, with both threads messing around with the same bunch of data. They would each bump up a couple of counters - one that they both share and another that's just for themselves. Then, right after each bump, we'd take a look at where the counters stand.

This wasn't just about writing some code; it was more about getting my head around Rust's way of handling data when multiple things are happening at once. Here's the story of what I did, what I learned, and how it all works.

## The Application Idea: Playing with Counters

So, what's this example all about? Well, it's pretty straightforward:

- **Two Threads, Two Tasks**: Imagine two little workers (threads) in our program. They're both doing a similar job but in their own way. One is labeled `thread1` and the other `thread2`.
- **What They Do**: Each time they get to work (`thread1` and `thread2` doing their thing), they do a couple of things:
  - `thread1` ups the numbers on two counters: a shared one (`count`) and its own (`count_thread1`). Then it shouts out (prints) the current score.
  - `thread2` does pretty much the same, but with `count` (the shared one) and `count_thread2` (its own personal counter).
- **Keep It DRY (Don't Repeat Yourself)**: I wanted to be smart about this and not write the same piece of code twice for showing the score. So, both threads use the same function to print the status. It's like they're using the same megaphone to announce their results.
- **Playing Nice with Sharing**: Here's the catch, though. When one thread is updating the counters and about to announce the score, we don't want the other one to barge in and mess things up. It's like saying, "Hold on, let me finish talking before you jump in." This means we need to be a bit clever about locking things up.

That's the gist of our little Rust adventure. Two threads, a few counters, and making sure they don't trip over each other while they're at it.

## Explaining the Service Code: Mutex Magic in Rust

Alright, let's dive into the heart of our Rust code - the `Service` struct. This is where the magic happens, and by magic, I mean carefully managing access to shared data with mutexes. Here's the code:

```rust
pub struct Service {
    count: std::sync::RwLock<u64>,
    count_thread1: std::sync::RwLock<u64>,
    count_thread2: std::sync::RwLock<u64>,
    write_lock: std::sync::RwLock<u8>,
}

impl Service {
    pub fn new() -> Self {
        Service {
            count: std::sync::RwLock::new(0),
            count_thread1: std::sync::RwLock::new(0),
            count_thread2: std::sync::RwLock::new(0),
            write_lock: std::sync::RwLock::new(0),
        }
    }

    pub fn get_counts(&self) -> Result<(u64, u64, u64), String> {
        let count = *self
            .count
            .read()
            .map_err(|e| format!("Failed to read-lock count: {}", e))?;
        let count_thread1 = *self
            .count_thread1
            .read()
            .map_err(|e| format!("Failed to read-lock count_thread1: {}", e))?;
        let count_thread2 = *self
            .count_thread2
            .read()
            .map_err(|e| format!("Failed to read-lock write_lock: {}", e))?;
        Ok((count, count_thread1, count_thread2))
    }

    pub fn increment_counts_thread1(&self) -> Result<(u64, u64, u64), String> {
        let mut count = self
            .count
            .write()
            .map_err(|e| format!("Failed to write-lock count: {}", e))?;
        let mut count_thread1 = self
            .count_thread1
            .write()
            .map_err(|e| format!("Failed to write-lock count_thread1: {}", e))?;
        let mut write_lock = self
            .write_lock
            .write()
            .map_err(|e| format!("Failed to write-lock write_lock: {}", e))?;
        *count += 1;
        *count_thread1 += 1;
        *write_lock = 1;
        drop(count);
        drop(count_thread1);
        self.get_counts()
    }

    pub fn increment_counts_thread2(&self) -> Result<(u64, u64, u64), String> {
        let mut count = self
            .count
            .write()
            .map_err(|e| format!("Failed to write-lock count: {}", e))?;
        let mut count_thread2 = self
            .count_thread2
            .write()
            .map_err(|e| format!("Failed to write-lock count_thread2: {}", e))?;
        let mut write_lock = self
            .write_lock
            .write()
            .map_err(|e| format!("Failed to write-lock write_lock: {}", e))?;
        *count += 1;
        *count_thread2 += 1;
        drop(count);
        drop(count_thread2);
        *write_lock = 2;
        self.get_counts()
    }
}
```

### Breaking Down the Code

- **Counters for Each Thread**: We've got three counters here - `count`, `count_thread1`, and `count_thread2`. The first one is shared between both threads, while the other two are individual to each thread. Each counter is wrapped in a `Mutex`. Why? Because `Mutex` ensures that only one thread can mess with the data at a time.

- **The `write_lock` Mutex**: This little guy is the key to making sure our print status function doesn't get interrupted by the other thread. We're using it to lock down the entire increment operation, from start to finish, including the print part. And to keep the compiler happy (and avoid warnings about unused variables), we're assigning a value (`1` or `2`) to `write_lock`.

- **Lock Order Consistency**: Notice something about how we lock our counters? We always lock `count` first, then `count_thread1` or `count_thread2`. This is crucial. Locking in the same order every time is a simple yet effective way to dodge deadlocks. If you start locking in different orders in different parts of your code, you're setting up a classic deadlock scenario.

### Choosing RwLock Over Mutex: A Consideration of Context

In the Rust ecosystem, we often use `Mutex` for safe, exclusive access to data across multiple threads. Think of `Mutex` as a way to say, "One at a time, please," ensuring that only one thread can access the data at any given moment. It's a straightforward, foolproof approach to concurrency.

But then there's `RwLock`, a slightly more complex cousin of `Mutex`.

#### The Subtlety of RwLock

- **Reading and Writing**: `RwLock` allows multiple threads to read data at the same time, which can be a big win for performance if you have a lot of read operations. However, it still ensures that write operations get exclusive access.
- **In Our Case**: While our current example might not have simultaneous reads, we chose `RwLock` to illustrate how it could be beneficial in a broader application context. It's about understanding the tools at your disposal and choosing the right one for the right job.

#### The Practical Angle

- **Why Not Just Stick with Mutex?**: You might wonder why we didn't just use `Mutex` since our example doesn’t explicitly require the concurrent read capabilities of `RwLock`. The reason is twofold: firstly, to demonstrate the capabilities of `RwLock` for educational purposes, and secondly, to prepare the code for potential scalability where concurrent reads might become more relevant.
- **Big Picture**: In a larger application, where you might have numerous threads frequently reading data, the benefits of `RwLock` become more pronounced. By allowing concurrent reads, `RwLock` can significantly enhance performance, reducing the waiting time for threads that just want to read data.

Choosing `RwLock` in our example is a nod to these broader considerations. It’s about anticipating future needs and understanding how different concurrency tools can be leveraged in various scenarios. This is a key part of thinking like a Rustacean — not just solving the problem at hand but doing so in a way that's efficient, scalable, and idiomatic to Rust.

## Explaining the `main` Function and the Role of `Arc`

In our Rust application, the `main` function is where we see the concurrency in action. Let's dissect how it uses `Arc` to enable multiple threads to interact with the same instance of our `Service` struct.

### The `main` Function Code

Here's what our `main` function looks like:

```rust
mod service;

use service::Service;
use std::sync::Arc;
use std::thread;
use std::time::Duration;

fn main() {
    let service = Arc::new(Service::new());

    let service1 = Arc::clone(&service);
    let service2 = Arc::clone(&service);

    let thread1 = thread::spawn(move || {
        for i in 0..5 {
            match service1.increment_counts_thread1() {
                Ok(counts) => println!("Thread 1: Iteration {}: Counts = {:?}", i, counts),
                Err(e) => {
                    eprintln!("Thread 1: Iteration {}: Error = {}", i, e);
                    // Handle error, e.g., retry, log, or break
                    break;
                }
            }
            thread::sleep(Duration::from_millis(250));
        }
    });

    let thread2 = thread::spawn(move || {
        for i in 0..5 {
            match service2.increment_counts_thread2() {
                Ok(counts) => println!("Thread 2: Iteration {}: Counts = {:?}", i, counts),
                Err(e) => {
                    eprintln!("Thread 2: Iteration {}: Error = {}", i, e);
                    // Handle error, e.g., retry, log, or break
                    break;
                }
            }
            thread::sleep(Duration::from_millis(250));
        }
    });

    if let Err(e) = thread1.join() {
        eprintln!("Thread 1 panicked: {:?}", e);
    }

    if let Err(e) = thread2.join() {
        eprintln!("Thread 2 panicked: {:?}", e);
    }
}
```

### Understanding `Arc` in Rust

- **Arc: A Smart Pointer for Concurrency**: `Arc` stands for Atomic Reference Counting. It's a type of smart pointer in Rust, which means it keeps track of how many references exist to a certain piece of data. Once all references are gone, the data is automatically cleaned up. This is super handy in avoiding memory leaks.
- **Why Use `Arc`?**: In a multi-threaded context, we need a way to safely share data between threads. `Arc` allows multiple threads to own a reference to the same data, ensuring that the data stays alive as long as at least one thread is using it.
- **Thread-Safety**: `Arc` is thread-safe, meaning it can be used across multiple threads without the risk of causing data races. This is crucial in our example where multiple threads are accessing and modifying the shared `Service` instance.

### Threads in Action

- **Creating Threads**: We spawn two threads, `thread1` and `thread2`. Each thread is given a cloned reference to our `Service` instance (`service1` and `service2`). This cloning is done using `Arc::clone`, which increments the reference count rather than copying the actual data.
- **Interacting with Shared Data**: Inside each thread, we call either `increment_counts_thread1` or `increment_counts_thread2`. These methods modify the shared data in a controlled manner, thanks to our `RwLock` implementation in the `Service` struct.
- **Expected Outcomes**: As each thread performs its operations, we expect the shared counter (`count`) to be incremented by both threads, whereas `count_thread1` and `count_thread2` are exclusive to their respective threads. The threads also print out the current state of these counters after each increment.

In summary, `Arc` in our `main` function demonstrates Rust's powerful and safe approach to concurrency. By allowing multiple threads to share ownership of data, `Arc` enables concurrent access while ensuring that the data lives as long as it's needed and no longer. This, combined with the thread-safe operations on our `Service` struct, showcases a typical pattern for managing shared state in multi-threaded Rust applications.

#### Seeing It in Action: Output of the Program

After understanding the roles of `Arc`, `RwLock`, and our thread setup, let's see what happens when we actually run the program. Here's the output you can expect:

```bash
cargo run .
   Compiling service_example v0.1.0 (/home/inigo/Documents/Tutorials/RUST/service_example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/service_example .`
Thread 1: Iteration 0: Counts = (1, 1, 0)
Thread 2: Iteration 0: Counts = (2, 1, 1)
Thread 1: Iteration 1: Counts = (3, 2, 1)
Thread 2: Iteration 1: Counts = (4, 2, 2)
Thread 1: Iteration 2: Counts = (5, 3, 2)
Thread 2: Iteration 2: Counts = (6, 3, 3)
Thread 1: Iteration 3: Counts = (7, 4, 3)
Thread 2: Iteration 3: Counts = (8, 4, 4)
Thread 1: Iteration 4: Counts = (9, 5, 4)
Thread 2: Iteration 4: Counts = (10, 5, 5)
```

This output clearly shows how the counters are incremented by each thread. Notice how the shared counter (`count`) increases with each operation, regardless of which thread is executing, while the individual counters (`count_thread1` and `count_thread2`) are incremented only by their respective threads.

## Error Management in Rust: Embracing Verbosity for Clarity

When transitioning to Rust from a language like Go, one thing you might find familiar is the verbosity in error handling. Rust, much like Go, emphasizes explicit and clear error management, though the styles differ slightly. Let's delve into how this plays out in Rust using our example, and why embracing this verbosity can be beneficial.

### Rust's Explicit Error Handling

- **Explicit is Better than Implicit**: Rust enforces an explicit approach to error handling. Unlike languages that use exceptions, Rust requires you to acknowledge and handle potential errors at every step.
- **Avoiding `.unwrap()` in Production**: While `.unwrap()` is convenient for quick tests or examples, it's risky in production code because it causes the program to panic in case of an error. Rust encourages handling errors gracefully to avoid unexpected crashes.

### Verbosity in Rust vs. Go

- **Clear and Predictable Code**: Rust’s verbose error handling, akin to Go's, ensures that your code is clear about how it deals with various failure scenarios. This explicitness leads to more predictable and maintainable code.
- **Our Code's Error Handling Approach**:

```rust
match service1.increment_counts_thread1() {
    Ok(counts) => println!("Thread 1: Iteration {}: Counts = {:?}", i, counts),
    Err(e) => {
        eprintln!("Thread 1: Iteration {}: Error = {}", i, e);
        break;
    }
}
```

- **The Use of `match`**: We use `match` to handle the `Result` type returned by our functions. This way, we explicitly define the flow for both successful and error outcomes.

### Balancing Verbosity and Practicality

- **Context Matters**: There are times when using `.unwrap()` might be okay, such as in prototype code or when an error is impossible by logic. However, as a general rule, explicit error handling is preferred.
- **Familiarity Over Time**: If you're used to Go's error handling, Rust's approach will feel familiar, though it may take some time to adapt to the nuances. Eventually, you'll likely appreciate the robustness it brings to your programs.

In summary, Rust's approach to error handling, while verbose, shares similarities with Go's explicit style. This verbosity is a small price to pay for the clarity and safety it brings to your code. As you grow more accustomed to Rust's patterns, you'll find that this explicitness becomes an invaluable tool in writing reliable, bug-resistant applications.

## Conclusions: Navigating the Learning Path in Rust

As we come to the end of this post, it's clear that diving into Rust's world, especially its approach to concurrency, is both challenging and rewarding. I'm by no means an expert in Rust — just someone who's starting small and grappling with concepts that initially seemed daunting.

### Embracing Rust's Concurrency Model

- **Starting Small**: My journey in Rust began with tackling small, challenging concepts. This exploration into `Arc`, `RwLock`, and threading is a part of that journey, an attempt to understand the depths of Rust's concurrency.
- **Safety First**: One of the most significant learnings from this exercise is the importance Rust places on safety, particularly in concurrent environments. The language's design nudges you towards patterns that prevent common errors like data races.

### The Beginner's Experience

- **From Confusion to Clarity**: As a beginner, the complexity of ownership, borrowing, and concurrency in Rust can be quite overwhelming. But, as with any new skill, the confusion gradually gives way to clarity with practice and patience.
- **Community Support**: One thing that stands out in my learning process is the Rust community's support. Whether it's through forums, documentation, or open-source contributions, there's always help available.

### A Small Step in a Larger Journey

- **Continual Learning**: This example, which I found challenging initially, represents just a small step in the broader journey of mastering Rust. It’s a testament to starting with what seems tough and breaking it down into manageable pieces.
- **Encouragement for Fellow Learners**: To those who are also on their beginning stages with Rust, keep at it. The initial hurdles are part of the process, and every challenge overcome is a stride towards becoming a proficient Rustacean.

### Explore the Code

If you're curious to see how the theory translates into code, or if you want to try your hand at modifying and playing with it, check out the repository on GitHub. It's a space for learning, experimenting, and sharing insights.

[GitHub Repository Link](https://github.com/ietxaniz/rust_service_example)

## Wrapping Up

In summary, my journey with Rust is still in its early stages, filled with learning and discovery. This example is a reflection of that — a small piece of a much larger puzzle. As I continue to learn, I look forward to uncovering more such pieces, understanding them, and fitting them together in the beautiful tapestry that is Rust programming.