Easy Tutorial
❮ Rust Lifetime Rust Data Types ❯

Rust Concurrency Programming

Safely and efficiently handling concurrency is one of the purposes of Rust's creation, primarily addressing high server load capacity.

The concept of concurrency refers to different parts of a program executing independently, which can be easily confused with parallelism, where the emphasis is on "simultaneous execution."

Concurrency often leads to parallelism.

This chapter discusses programming concepts and details related to concurrency.

Threads

A thread is a part of a program that runs independently.

Threads differ from processes in that threads are within-program concepts, and programs usually execute within a process.

In an environment with an operating system, processes are often scheduled alternately for execution, while threads are scheduled within the process by the program.

Since thread concurrency can likely result in parallelism, issues like deadlocks and stalling errors commonly occur in programs with concurrency mechanisms.

To address these issues, many other languages (such as Java, C#) use special runtime software to coordinate resources, but this undoubtedly significantly reduces program execution efficiency.

C/C++ also supports multi-threading at the lowest level of the operating system, and neither the language itself nor its compilers have the ability to detect and avoid parallel errors, which puts a lot of pressure on developers, who need to spend a lot of effort to avoid errors.

Rust, like C/C++, does not rely on a runtime environment.

However, Rust is designed within the language to eliminate the most common errors at compile time through mechanisms including ownership, something other languages do not have.

This does not mean we can be careless in programming; so far, issues caused by concurrency have not been completely resolved in the public domain, and errors can still occur. Be cautious when programming with concurrency!

In Rust, a new thread is created using the std::thread::spawn function:

Example

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));
    }
}

Running result:

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 result may vary in some cases, but it generally prints out like this.

This program has a child thread intended to print 5 lines of text, and the main thread prints 3 lines of text. However, it is clear that as the main thread ends, the spawned thread also ends, and does not complete all printing.

The std::thread::spawn function takes a parameterless function, but the above writing style is not recommended. We can use closures to pass functions as parameters:

Example

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 stored in variables or passed as parameters to other functions. Closures are akin to Lambda expressions in Rust and have the following format:

|param1, param2, ...| -> return_type {
    // function body
}

For example:

Example

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

Running result:

inc(5) = 6

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

Example

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

The result did not change.

join Method

Example

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();
}

Running result:

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 ensures that the program does not stop running until the child thread finishes execution.

move for Forced Ownership Transfer

This is a common scenario:

Example

use std::thread;

fn main() {
    let s = "hello";

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

    handle.join().unwrap();
}

Attempting to use the resources of the current function in a child thread is incorrect! The ownership mechanism prevents this dangerous situation by prohibiting it, as it would undermine the deterministic destruction of resources by the ownership mechanism. We can handle this using the move keyword for the closure:

Example

use std::thread;

fn main() {
    let s = "hello";

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

    handle.join().unwrap();
}

Message Passing

The primary tool for implementing message passing concurrency in Rust is a channel, which consists of a transmitter and a receiver.

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

Example

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);
}

Running result:

Got: hi

The child thread acquires the main thread's transmitter tx and calls its send method to send a string, which the main thread then receives via the corresponding receiver rx.

❮ Rust Lifetime Rust Data Types ❯