Rust Lang

Rust Concurrency

Concurrency refers to a feature that allows standalone parts of a program to run parallel to other sections of the code. Concurrency allows different parts of a program to run simultaneously, improving performance.

Let us stroll through the woods of concurrency programming in the Rust programming language. Keep in mind that this is article is not designed to be a complete guide to concurrency programming. It only serves as a foundation for expanding and creating more complex applications.

Processes and Threads

When we write a normal program and execute it on a target system, the host operating system executes the code in a process. A process refers to a unit of a specified executable.

However, in modern systems and applications, you have parts of the same code run simultaneously using threads.

In most cases, you will often hear the term multi-threading used where concurrency occurs. This is because we are essentially spawning multiple threads and allowing them to run parallel to each other.

Let us take a basic program to illustrate how a normal program works and how to use concurrency to improve it.

Consider a program with two loops as shown:

use std::{thread};
use std::time::Duration;

fn main() {

 for i in 0..=5 {

    println!("{}", i);

    // sleep for 1000ms
    thread::sleep(Duration::from_millis(1000));
 }

 for i in 0..=5 {

    println!("{}", i);
    thread::sleep(Duration::from_millis(1000));
 }
}

In the example code above, we have two loops that iterate from 0 to 5. However, in each iteration, we sleep for 1000 milliseconds.

The thread::sleep method allows us to put a specific thread to sleep for the specified duration.

If you run the code above, you notice that the first loop waits for the second loop to complete before it can start running.

This is because both loops are on a single thread.

If we want both loops to run simultaneously, we need to put them in different threads.

Rust Create Thread

We can create new threads using the thread module. It is part of the standard library and provides us with a suite of tools and functions for working with threads.

We can import it using the statement:

use std::thread;

We also need the Duration module from the time thread. We can import it as:

use std::time::Duration

To create a new thread in Rust, use the thread::spawn method. This method takes a closure as the argument.

The closure, in this case, defines the code to run inside the thread.

The syntax is as shown below:

thread::spawn(|| { closure })

Let us refine the previous code and put each construct into a separate thread. Example code is as shown:

use std::{thread};
use std::time::Duration;

fn main() {

 // create new thread
 std::thread::spawn(move || {

    for i in 0..=5 {

      println!("{}", i);
      thread::sleep(Duration::from_millis(1000));
    }

 });

 for i in 0..=5 {

     println!("{}", i);
     thread::sleep(Duration::from_millis(1000));

 }
}

In the example program above, we create a new thread using the thread::spawn function and pass the first loop as the closure.

In the main thread, we run the second loop. This allows both loops to run simultaneously. The code above should return output as:

0
0
1
1
2
2
3
3
4
4
5
5

What happens if the main thread exits before the “inner” thread completes? An example is as shown below:

use std::{thread};
use std::time::Duration;

fn main() {

 // inner thread
 std::thread::spawn(move || {

    for i in 0..=5 {

      println!("{}", i);
      thread::sleep(Duration::from_millis(1000));
    }

  });

 // main
 for i in 0..=5 {

   println!("{}", i);
   thread::sleep(Duration::from_millis(2));
 }
}

In this example, the main thread takes less time to sleep and hence will complete faster before the inner thread completes.

In such a case, the inner thread will only run while the main thread is running. The code above will return incomplete output as:

0
0
1
2
3
4
5

This is because the “inner” thread is terminated before completion.

Rust Join Handles

We have seen how a thread behaves if the main thread exits before it completes. We can join the two handles to resolve such a case and let the other thread wait for another.

Joining handles will allow the main thread to wait for the other threads before termination.

To join handles, we use the join method as shown in the syntax below:

let handle_name = thread::spawn({closure});

handle_name.join().unwrap();

Let us redefine our loop example, where the main thread exits early.

use std::{thread};
use std::time::Duration;

fn main() {

  let handle = std::thread::spawn(move || {

      for i in 0..=5 {

            println!("{}", i);
            thread::sleep(Duration::from_millis(1000));
      }

   });

  for i in 0..=5 {

      println!("{}", i);
      thread::sleep(Duration::from_millis(2));
  }

  // join handle
  handle.join().unwrap();
}

In the example code above, we create a handle variable that holds the thread. We then join the thread using the join() method.

The unwrap method allows us to handle errors.

Since the main thread is sleeping for a shorter time, it should be completed before the “inner” thread. However, it should wait for the other thread to exit due to the join method.

The output is as shown:

0
0
1
2
3
4
5
1
2
3
4
5

Note that the main thread outputs all the values in a short duration and waits for the other thread to complete.

Rust Thread Move Closure

You may have noticed the move keyword inside the thread closure in our previous example. The move closure is used with the thread::spawn method to allow sharing of data between threads.

Using the move keyword, we can allow a thread to transfer ownership of values to another thread.

Take an example program below:

use std::{thread};
use std::time::Duration;

fn main() {

  let arr = [1,2,3,4,5];
  let handle = std::thread::spawn(|| {

   for i in arr.iter() {

    println!("{}", i);
    thread::sleep(Duration::from_millis(100));

   }

  });

  handle.join().unwrap();
}

In the code above, we declare an array called arr in the main thread. We then spawn a new thread without the move closure.

NOTE: Since we are trying to access the array arr and make it part of the closure environment, the compilation will fail as it is not available in that thread.

We can use the move keyword to force the closure in the thread to take ownership of the array.

We can fix the code above by adding the move closure as shown:

use std::{thread};
use std::time::Duration;

fn main() {

  let arr = [1,2,3,4,5];
  let handle = std::thread::spawn(move || {

  for i in arr.iter() {

    println!("{}", i);
    thread::sleep(Duration::from_millis(100));
  }

  });

  handle.join().unwrap();
}

This allows the thread to take ownership of the array and iterate it. This should return:

1
2
3
4
5

Conclusion

That was the fundamentals of concurrent programming in the Rust programming language. Although this article serves as a concrete foundation for Rust concurrency, it does not cover advanced concepts. You can check the documentation for details.

About the author

John Otieno

My name is John and am a fellow geek like you. I am passionate about all things computers from Hardware, Operating systems to Programming. My dream is to share my knowledge with the world and help out fellow geeks. Follow my content by subscribing to LinuxHint mailing list