rust programming

Concurrent Programming in Rust

Safe and efficient handling of concurrency was one of the primary goals behind Rust's creation, mainly to solve server high-load capacity issues.

The concept of concurrency refers to different parts of a program executing independently. This is often confused with parallelism, which emphasizes "simultaneous execution."

Concurrency often leads to parallelism.

This chapter covers programming concepts and details related to concurrency.

Threads

A thread is an independently running part of a program.

Threads differ from processes in that threads are a concept within a program, while programs typically execute within a process.

In environments with operating systems, processes are typically scheduled alternately for execution, while threads are scheduled by the program within the process.

Since thread concurrency can likely result in parallelism, common parallel computing issues like deadlocks and race conditions frequently occur in programs with concurrent mechanisms.

To solve these problems, many other languages (like Java and C#) use special runtime software to coordinate resources, but this significantly reduces program execution efficiency.

C/C++ languages support multi-threading at the lowest level of the operating system, but neither the language itself nor its compiler has the ability to detect and avoid parallel errors, which puts great pressure on developers who need to spend considerable effort avoiding errors.

Rust doesn't rely on a runtime environment, similar to C/C++.

However, Rust is designed with mechanisms including ownership to eliminate the most common errors at compile time, which other languages don't have.

But this doesn't mean we can be careless when programming. To date, problems caused by concurrency haven't been completely solved in the public domain, and errors can still occur. Be extremely careful when writing concurrent programs!

In Rust, new threads are created using the std::thread::spawn function:

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

fn spawn_function() {
    for i in 0..5 {
        println!("spawned thread print {}", i);
        thread::sleep(Duration::from_millis(1));
    }
}

fn main() {
    thread::spawn(spawn_function);

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

Output:

main thread print 0
spawned thread print 0
main thread print 1
spawned thread print 1
main thread print 2
spawned thread print 2

The order of this output might vary in some cases, but it generally prints like this.

This program has one child thread that's meant to print 5 lines of text, while the main thread prints three lines. However, it's clear that as the main thread ends, the spawned thread also ends without completing all its prints.

The std::thread::spawn function takes a function without parameters as its argument, but the above approach isn't recommended. We can use closures to pass functions as parameters:

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

fn main() {
    thread::spawn(|| {
        for i in 0..5 {
            println!("spawned thread print {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

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

Closures are anonymous functions that can be saved into variables or passed as arguments to other functions. Closures are equivalent to Lambda expressions in Rust, with the following format:

|parameter1, parameter2, ...| -> return_type {
    // function body
}

For example:

fn main() {
    let inc = |num: i32| -> i32 {
        num + 1
    };
    println!("inc(5) = {}", inc(5));
}

Output:

inc(5) = 6

Closures can omit type declarations and use Rust's automatic type inference:

fn main() {
    let inc = |num| {
        num + 1
    };
    println!("inc(5) = {}", inc(5));
}

The result remains the same.

The join Method

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

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

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

    handle.join().unwrap();
}

Output:

main thread print 0 
spawned thread print 0 
spawned thread print 1 
main thread print 1 
spawned thread print 2 
main thread print 2 
spawned thread print 3 
spawned thread print 4

The join method allows the program to wait for the child thread to finish before stopping.

move Keyword for Forced Ownership Transfer

This is a common situation:

use std::thread;

fn main() {
    let s = "hello";
    
    let handle = thread::spawn(|| {
        println!("{}", s);
    });

    handle.join().unwrap();
}

Attempting to use the current function's resources in a child thread is definitely wrong! The ownership mechanism prohibits such dangerous situations as they would violate the deterministic nature of resource destruction under the ownership system. We can handle this using the move keyword with closures:

use std::thread;

fn main() {
    let s = "hello";
    
    let handle = thread::spawn(move || {
        println!("{}", s);
    });

    handle.join().unwrap();
}

Message Passing

The main tool for implementing message-passing concurrency in Rust is channels. A channel consists of two parts: a transmitter and a receiver.

The std::sync::mpsc module contains methods for message passing:

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

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

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

Output:

Got: hi

The child thread receives the transmitter tx from the main thread and calls its send method to send a string, which the main thread then receives through the corresponding receiver rx.