Rust, known for its safety and performance, offers powerful tools for handling concurrency. In this blog, we will discuss concurrency and parallelism in Rust, as well as the tools and different libraries that can be used to make concurrent and parallel programming in Rust efficient and reliable.
What is concurrency?
Concurrency is the ability of a programming language to perform multiple tasks simultaneously. In Rust, we have various mechanisms that take concurrency to the next level, one of which is ownership, making concurrency exceptionally powerful
In concurrency, we have a thread that can perform multiple tasks simultaneously, While executing, the thread can switch between the different tasks to perform the execution.
To understand these let‘s suppose we have a thread ( like a chef) that can perform different tasks simultaneously that is chop and stir a thread can not perform different tasks at the same time so it can switch between different tasks that is chop and stir. This is how the concurrency actually works a thread can switch between different tasks to perform the operations.
What is Parallelism?
Parallelism is the ability of Rust to execute multiple tasks concurrently to utilize the multiple processor cores to make the program more efficient and fast.
In parallelism, we have different threads that can execute in a parallel manner and there is no switching between threads.
Suppose we have two threads(Chefs) one is chopping and the other is stirring, they both don’t interfere with each other’s work they both can work independently. And perform their task efficiently
Rust Tools for Concurrency and Parallelism:
Rust provides a range of tools and libraries for making concurrency and parallelism.
Rayon
Rayon is the most popular crate in Rust for data parallelization. Rayon is popularly used when we want to parallelize operations like filtering, and mapping on data. Rayon provides different iterators to parallelize the data, these iterators can work with the Rust Standard collections (e.g., vector). In Rayon iterators each iteration can execute independently and in different CPU cores.
We have different iterators in Rayon:
- par_iter()
- par_bridge()
use rayon::prelude::*;
fn main() {
let mut vec = vec![1, 2, 3, 4, 5, 6, 7, 8];
vec.par_iter_mut().for_each(|x| *x += 2);
println!("{:?}", vec);
}
Rayon is useful in a situation where you need to parallelize operations on collections of data, Here are some scenarios where we use Rayon :
- Data processing
- Multicore CPUs
- Performance Optimization
Thread
A thread can be used in a program to execute a chunk of code simultaneously. To create and manage threads we use std::thread modules from Rust standard library.
use std::thread;
fn main() {
let new_thread= thread::spawn( || {
println!("This is the new thread which is created");
} ) ;
new_thread.join().unwrap();
}
We use the join () method to join the main thread and the newly created thread. after joining, the new thread can be executed via the main thread. In Rust every new thread can be used to run via the main thread, we can also use ownership ability to make a single owner of every resource to avoid race conditions. By using the move keyword we can move the ownership of a resource from one thread to another thread.
We can use threads when we need to manage and create concurrent or parallel execution of tasks
Message Passing (Channels)
Message passing is a way to transfer a message from one side to another side by creating a channel that has a sender and receiver to transmit the message from one side to the other, this mechanism can be done by using the threads, we create a thread which can send the message and another thread can receive the message, this can also be called thread communication via the channel.
use std::sync::mpsc;
fn main() {
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
sender.send(val).unwrap();
});
let received = receiver.recv().unwrap();
println!("Got: {}", received);
}
We use mpsc(multiple producer single consumer) crates to create a channel. While passing messages from one thread to another we also transfer the ownership of the message to another thread.
We use message passing (Channels ) to establish communication and coordination between different threads and concurrent processes.
- Inter-thread communication
- Synchronization
- Data Sharing
Async/await
Async/await are the rust features that can help to write asynchronous code that is more readable and structured. They are built on top of future trait and tokio runtime.
To define the async function we use async, before the function fn to make the function async.
async fn asynchronous_function () -> u32 {
//your code goes here
};
Await: await is used to wait until the async function completes its execution.
async fn do_some_thing_async () {
let result = asynchronous_function().await;
println!("result {:?} ",result )
}
In the above code, we have a do_some_thing_async() function, we use the await keyword to wait until another async function completes its execution so that the do_something_async () function executes
To execute these asynchronous functions we need runtime on which these asynchronous functions can execute, Rust has several runtime options to handle the execution of asynchronous functions like tokio, these runtimes provide infrastructure for executing asynchronous tasks and managing concurrency.
#[tokio::main]
async fn main() {
do_some_thing_async().await
}
async fn asynchronous_function() -> i32 {
23
}
async fn do_some_thing_async () {
let result = asynchronous_function().await;
println!("result -> {}",result )
}
We use async/await when we need to handle asynchronous operations. Here are some scenarios where we use async/await:
- I/O operations
- Concurrency
- Web Development
- Parallelism
Actor Model
In Rust we have an actix crate to implement the actor, Actors can be used to build the concurrent application, each actor has its own state and behavior, they can execute independently, and the actors can communicate with other actors via the message-passing to each other. the actors need execution context to execute, each actor needs its own execution context to execute.
To create an actor we need to implement the actor trait
#[derive(Debug)]
struct MyActor {
count: String,
}
impl Actor for MyActor {
type Context = Context;
}
While implementing the actor trait we need to define the execution context for the actor, where an actor can execute.
#[derive(Message)]
#[rtype(result = "String")]
struct Ping(String);
Here we define a message that can be passed to the actor. the above code implements the Message trait and the return type of the message is String type.
impl Handler for MyActor {
type Result = String;
fn handle(&mut self, msg: Ping, _ctx: &mut Context) -> Self::Result {
self.count = msg.0 + &" world".to_string();
self.count.clone()
}
}
Here we define a handler for the Message, the handler can respond when the actor gets any message.
We can define different handlers for the different types of Messages.
#[actix_rt::main]
async fn main() {
// start new actor
let addr = MyActor { count: "".to_string() }.start();
// send message and get future for result
let res = addr.send(Ping("hello".to_string())).await;
Here #[actix_rt::main] is the runtime system for the actix framework. To start a new actor we use the start() method to start or initialize an actor, System::current().stop() can be used to stop and exit the current actor. The send method can be used to send the message to the actor and get the future response from the actor.
We use Actor model to build concurrent and distributed systems with a high degree of isolation and encapsulation . Here are some scenarios where we can use the Actor model:
- Concurrency and parallelism
- Distributed system
- Iot(Internte of Things)