English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Rust Concurrency Programming

Safe and efficient handling of concurrency is one of the purposes of Rust's birth, mainly solving the high load bearing capacity of servers.

The concept of concurrency (concurrent) is that different parts of the program execute independently, which is easy to confuse with the concept of parallelism (parallel), which emphasizes "simultaneous execution".

Concurrency often leads to parallelism.

This chapter discusses programming concepts and details related to concurrency.

Thread

A thread is an independent running part of a program.

The difference between threads and processes is that threads are a concept within the program, and programs are often executed within a process.

In environments with operating systems, processes are often scheduled alternately to be executed, while threads are scheduled within processes by the program.

Since thread concurrency is likely to occur in parallel situations, deadlock and delay errors often occur in programs with concurrent mechanisms.

To solve these problems, many other languages (such as Java, C#) use special runtime software to coordinate resources, but this undoubtedly greatly reduces the execution efficiency of the program.

C/C++ Language at the lowest level of the operating system supports multithreading, and the language itself and its compiler do not have the ability to investigate and avoid parallel errors, which puts a lot of pressure on developers, who need to spend a lot of effort to avoid errors.

Rust does not rely on the runtime environment, which is similar to C./C++ Likewise.

But Rust has designed means including ownership mechanisms in the language itself to eliminate the most common errors at the compilation stage, which other languages do not have.

But this does not mean that we can be careless in programming. So far, due to concurrency, the problems caused have not been completely solved in the public domain, and errors are still possible. Be careful when programming with concurrency!

In Rust, a new process is created through 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));
    }
}

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

This result may change in sequence in some cases, but in general, it is printed in this way.

This program has a child thread, the purpose of which is to print 5 Line text, the main thread prints three lines of text, but as obviously as the end of the main thread, the spawned thread also ended and did not complete all the printing.

The std::thread::spawn function's parameter is an unparameterized function, but the above syntax is not recommended. We can use closures (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 parameters to other functions. Closures are equivalent to Lambda expressions in Rust, with the format as follows:

|parameter1, parameter2, ...| -) > return value type {
    // function body
}

For 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:

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

The result did not change.

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

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 can make the program stop running after the child thread has finished running.

Move 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 resources of the current function in a child thread is definitely wrong! Because the ownership mechanism prevents the occurrence of this dangerous situation, which will destroy the determinacy of the ownership mechanism and destroy the resource. We can use the move keyword of the closure to handle it:

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

Message Passing

In Rust, a primary tool for implementing message passing concurrency is the channel (channel), which consists of two parts: a transmitter (transmitter) and a receiver (receiver).

The std::sync::mpsc module includes message passing methods:

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 obtained the sender from the main thread, called its send method to send a string, and then the main thread received it through the corresponding receiver rx.