Intro to Concurrency in Rust

Internship at OpenGenus

Get FREE domain for 1st year and build your brand new site

So far, all of our code, all the examples and all the theories we've seen, have been ignoring one of the key features Rust aims to improve in programming. And that is of course, concurrency and parallelism. Today we're going to begin exploring these topics.

Table of contents:

  1. What is Concurrency?
  2. Why Rust?
  3. How to and Examples

So what is concurrency?

According to Wikipedia, concurrency is:

In computer science, concurrency is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or at the same time simultaneously partial order, without affecting the final outcome.

Out of order or independent execution is Concurrency. Executing at the same time is parallelism. But for the sake of simplicity, I'm going to use Concurrency to speak of both, but be mindful that they're different terms.

In most if not all programming languages (I do not know them all, so I will not lie to you with absolute claims), your program is executed in order, top to bottom, statement by statement, instruction by instruction, until it reaches the end.

With the arrival of multiple threads, multiple cores in our hardware, continuing to run our programs top to bottom, one instruction at a time, feels.. out of date.

Why Rust then?

What's so special about Rust and concurrency? Well, the language was designed with concurrency easier and safer as one of it's goals. Taking directly from the book:

Handling concurrent programming safely and efficiently is another of Rustโ€™s major goals. Concurrent programming, where different parts of a program execute independently, and parallel programming, where different parts of a program execute at the same time, are becoming increasingly important as more computers take advantage of their multiple processors. Historically, programming in these contexts has been difficult and error prone: Rust hopes to change that.

There are several issues when it comes to concurrency. For example..

Let's imagine for a moment that we have two independent threads running at the same time. But both have a particular variable that they both need to read and/or write. We have no guarantee as to which one will access this shared resource first. This is called a race condition.

Another possible issue, deadlocks. What if our two threads were waiting for each other to finish, before finishing themselves. They would never end.

Concurrency adds also a huge pile of possible bugs that are hard to debug and fix. So all these extra possibilities and performance come with a cost.

**As all things in programming, it all depends on the problem we want to solve. There's no point in using concurrency, complex techniques or "special" features unless it's handy for our problem. Over engineering is a thing that exists, and you should at least be aware of it. Don't kill an ant with a nuke, nor try to fix a building's stability with simple duct tape.

So. With that out of the way. Let's dive into how Rust deals with these issues, while at the same time giving some small code examples that are concurrent.

Code Time!

Rust's standard library offers us Native OS threads. We already know how to import libraries but let's refresh the knowledge. Practice makes Permanent.

use std::thread;

Taking a very similar approach to the examples in the rust book's concurrency chapter (linked below)..

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

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("Hello! I've said Hello {} times from the child thread.", i);
            thread::sleep(Duration::from_secs(1));
        }
    });

    for i in 1..5 {
        println!("Hi! I've said Hi {} times from the main thread.", i);
        thread::sleep(Duration::from_secs(2));
    }
}

In our main function, which spawns it's own main thread, we're spawning a second, child thread. We tell the child thread to print it's line every second, and the main thread every 2 seconds.

If you copy the above code and execute it, you'll see something similar to this in your console after running it. It will display one line at a time, each thread waiting for it's specified time to be over.

main-thread-rust

You may notice something interesting here. Main thread's for-loop goes from 1 to 4, (since the 5 is excluded), and the child thread's from 1 to 9. But.. there's no 9. Why's that?

When spawning threads from the main thread, their execution ends when the main thread's ends. So our Main for loop is ending and stopping the child thread before it can end.

So what do we do if we want to make sure the child thread ends before ending the main? Let's take a closer look at thread::spawn..

thread-spawn-rust

Additionally, the join handle provides a join method that can be used to join the child thread.

thread::spawn returns a JoinHandle that lets us use a few extra functions. So! Let's switch up our code a bit and use this handle. I'll also change the duration requirements to make it easier to see the fact the main thread is waiting for the child to finish. Main thread will take 5 seconds, the child thread will take 10

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

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("Hello! I've said Hello {} times from the child thread.", i);
            thread::sleep(Duration::from_secs(1));
        }
    });

    for i in 1..5 {
        println!("Hi! I've said Hi {} times from the main thread.", i);
        thread::sleep(Duration::from_secs(1));
    }

    handle.join().unwrap();
}

cargo-run-rust

As we can see, the main thread "ends" but waits until the child thread reaches 9 before actually ending. Calling "Join" from a thread, blocks that thread until the handle's thread finishes. We called join from the main thread, which blocks it until the handle's thread, the child thread, ends. Neat huh? But what happens if we call the join somewhere else? Say, before the main for loops executes?

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

fn main() {
    
    // Snip
    
    handle.join().unwrap();

    for i in 1..5 {
        println!("Hi! I've said Hi {} times from the main thread.", i);
        thread::sleep(Duration::from_secs(1));
    }
    
}

output-2-rust

Since join locks main until the child thread is done.. we see that first the child runs and ends, THEN main's for loop executes and runs.

This article at OpenGenus was a very introductory overview of threads and concurrency. In the next article, we're going to go deeper into this. There's so much more to explore, I've decided to chunk it up in a few articles so we can properly dissect and learn these things.

References:
Concurrency in The Rust Book