首页
看点啥
插画图片
首页 科技看点 Rust并发编程入门教程:多线程与通道通信怎么用实用指南

Rust并发编程入门教程:多线程与通道通信怎么用实用指南

2026-10-19 0

平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“Rust并发编程入门教程:多线程与通道通信怎么用”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。

Rust的并发编程

在这个场景下,同时发,是指在宏观意义上同一时间处理多个任务。并发的方式一般包含为三种:多进程、多线程以及最近几年刚刚火起来的协程。

多线程并发

线程(thread)是操作系统能够进行运算调度的最小单位。

它被包含在进程之中,是进程中的实际运作单位。

实际处理时,一条线程指的是进程中一个单一顺序的控制流,一个进程中能够同时发多个线程,每条线程并行执行不同的任务。

新建子线程

Rust采用标准库中的thread模块新建线程:

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

// 线程函数
fn thread_fn() {
    for i in 0..10 {
        println!("hi number {} from the spawned thread!", i);
        thread::sleep(Duration::from_secs(1));
    }
}

fn main() {
    // 创建子线程
    let t = thread::spawn(thread_fn);

    // 等待子线程结束
    t.join();
}

从实现思路看,与新建进程不同,thread::spawn得到的不再是Result,不需unwrap。难道新建线程不会失败吗?

理解这一步时,上面的例子中,线程函数每秒打印一次,循环10次后线程结束。主线程等待子线程结束后退出。

在这个场景下,这时候,若想让循环10次变成参数传入,从而控制循环次数,应该怎么做呢?很遗憾,thread::spawn没有办法给线程函数传递参数。不过,有一种特殊的函数能够解决这个问题,那就是闭包。

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

fn main() {
    let count = 5;

    // 使用闭包创建线程
    let t = thread::spawn(move || {
        // 闭包捕获环境变量count
        for i in 0..count {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_secs(1));
        }
    });

    // 等待线程结束
    t.join();
}

落到代码里,闭包能够捕获环境中的变量count,细心的朋友可能发现闭包采用了move关键字,这是因为多线程同时发有着许多不确定性。如果以借用的方法采用count,那主线程中count被销毁了怎么办?所以,需添加move关键字,让Rust将count复制一份到闭包中,这样就不会引发问题。

若线程函数很长,这么写同时不优雅,能够把上面两种方式结合起来:

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

// 线程函数
fn thread_fn(count: i32) {
    for i in 1..count {
        println!("hi number {} from the spawned thread!", i);
        thread::sleep(Duration::from_secs(1));
    }
}

fn main() {
    let count = 5;
    // 创建线程
    let t = thread::spawn(move || { thread_fn(count) });

    // 等待线程结束
    t.join();
}

线程间通信

采用通道传递数据

理解这一步时,通道(channel)是个很时髦的词汇, Go和Rust都用通道来解决线程间通信的问题。与进程的管道类似,都是分为发送端和接收端,不同的是,通道的发送端能够不只一个,但接收端只能是一个。这在Rust中被称为mpsc(multiple producer, single consumer)。

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

fn main() {
    // 创建通道
    let (tx, rx) = mpsc::channel();

    // 启动子线程
    let t = thread::spawn(move || {
        // 阻塞的接收消息
        let msg = rx.recv().unwrap();
        println!("sub thread recv: {}", msg);
    });

    // 向通道发送消息
    tx.send(String::from("hello")).unwrap();

    // 等待子线程结束
    t.join().unwrap();
}

recv()是阻塞的,与之相对的是非阻塞的try_recv:

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    // 创建通道
    let (tx, rx) = mpsc::channel();

    // 启动子线程
    let t = thread::spawn(move || {
        // 阻塞的接收消息
        loop {
            match rx.try_recv() {
                Ok(msg) => println!("sub thread recv: {}", msg),
                _ => {
                    println!("sub thread recv nothing");
                    thread::sleep(Duration::from_secs_f32(0.5))
                }
            }
        }
    });

    thread::sleep(Duration::from_secs_f32(3.5));

    // 向通道发送消息
    tx.send(String::from("hello")).unwrap();

    // 等待子线程结束
    t.join().unwrap();
}

理解这一步时,由于tx的所有权只能被一个线程拥有,在多个线程同时采用时,需clone为多分采用:

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

fn main() {
    // 创建通道
    let (tx, rx) = mpsc::channel();

    let tx1 = tx.clone();
    let tx2 = tx.clone();

    // 启动子线程
    let t1 = thread::spawn(move || {
        tx1.send(String::from("hello. I'm thread1.")).unwrap();
    });

    let t2 = thread::spawn(move || {
        tx2.clone().send(String::from("hello. I'm thread2.")).unwrap();
    });

    let msg1 = rx.recv().unwrap();
    println!("main thread recv: {}", msg1);

    let msg2 = rx.recv().unwrap();
    println!("main thread recv: {}", msg2);
}

rx实现了迭代器特性,能够直接用来for循化。上面的代码能够简化为:

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

fn main() {
    // 创建通道
    let (tx, rx) = mpsc::channel();

    let tx1 = tx.clone();
    let tx2 = tx.clone();

    // 启动子线程
    let t1 = thread::spawn(move || {
        tx1.send(String::from("hello. I'm thread1.")).unwrap();
    });

    let t2 = thread::spawn(move || {
        tx2.clone().send(String::from("hello. I'm thread2.")).unwrap();
    });

    for msg in rx {
        println!("main thread recv: {}", msg);
    }
}

理解这一步时,通道还有一个特性,如果发送的消息被保存在某变量中的话,消息发送后,这个变量不能再被采用。

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

fn main() {
    // 创建通道
    let (tx, rx) = mpsc::channel();

    // 启动子线程
    let t = thread::spawn(move || {
        // 阻塞的接收消息
        let msg = rx.recv().unwrap();
        println!("sub thread recv: {}", msg);
    });

    // 向通道发送消息
    let msg = String::from("hello");
    tx.send(msg).unwrap();
    println!("main thread send: {}", msg);

    // 等待子线程结束
    t.join().unwrap();
}

结合项目来看,这段代码看上去感觉没有问题,但事实上同时非如此。因为并发,Rust担心子线程接收到以后会修改甚至drop掉msg变量,这时候主线程再采用它就会引发错误。因此,发送以后,msg的所有权被移动给了接收者,发送者不能再次采用它。

共享内存

结合项目来看,在没有通道之前,大多数语言都是借助共享内存的方式实现线程间的数据共享。共享内存固然好,但采用过程中有着诸多的问题,多个线程需互斥的访问内存数据,需与信号量结合进行加锁。加锁后又会出现新的问题,比如列锁等。Go语言直接明确的表示“不要借助共享内存来通讯!”。

从实现思路看,Rust依然保留了共享内存的实现方法,在允许采用共享内存的同时,将共享内存与信号量紧密的结合在了一起。如果你不采用信号量加锁,就得不到共享内存的所有权,共享内存采用完后会自动解锁等一系统的措施来保证共享内存的安全。

实际处理时,现在很多班级有问题都需在群里进行接龙,接龙的过程是很多家长同时发的过程,经常出现家长刚编辑完接龙的文字发出去就被其他家长覆盖的情况。我们采用共享内存来帮助家长解决一下这个头疼的问题。

首先,进行第一步尝试,在单线程中采用共享内存:

use std::sync::Mutex;

fn main() {
    // 创建信号量,它的内部是一个动态数组
    let m = Mutex::new(vec![]);

    // 从信号量中取得共享内存
    let mut names = m.lock().unwrap();

    // 修改共享内存
    names.push("main");

    println!("{:?}", names);
}

落到代码里,m.lock不仅仅完成了对信号量的加锁,它还完成了从信号量中提取共享内存的的作用,所以,不用担心没有对共享内存加锁就采用它,因为不加锁你就得不到共享内存。虽然信号量是不可变的,但依然能够对其进行加锁,得到的共享内存也能够是可变的。

下面,在循环中采用共享内存:

use std::sync::Mutex;

fn main() {
    // 创建信号量,它的内部是一个动态数组
    let m = Mutex::new(vec![]);

    for i in 1..10 {
        // 从信号量中取得共享内存
        let mut names = m.lock().unwrap();

        // 修改共享内存
        names.push(format!("{}", i));
    }
    
    let names = m.lock().unwrap();
    println!("{:?}", names);
}

实际处理时,这段代码也能够正常运行,为什么每次对信号量的加锁都能成功呢?因为在for循环的循环体内取得了共享内存,在共享内存的生命周期结束时(离开循环体时),信号量被自动解锁了。所以,不用担心忘记解锁,后面加锁时永远得不锁造成死锁的情况。

下面,将共享内存放到多线程的并发环境中:

use std::thread;
use std::sync::Mutex;

fn main() {
    // 创建信号量,它的内部是一个动态数组
    let m = Mutex::new(vec![]);
    
    // 循环中创建多个线程
    let mut threads = vec![];
    for i in 1..10 {
        let t = thread::spawn(move ||{
            // 从信号量中取得共享内存
            let mut names = m.lock().unwrap();

            // 修改共享内存
            names.push(format!("{}", i));
        });
        
        threads.push(t);
    }
    
    let names = m.lock().unwrap();
    println!("{:?}", names);
}

然而,意想不到的是,这段代码是不能借助编译的,编译器提示:

error[E0382]: use of moved value: `m`
  --> src\main.rs:11:31
   |
6 | let m = Mutex::new(vec![]);
   | - move occurs because `m` has type `std::sync::Mutex>`, which does not i
mplement the `Copy` trait
...
11 | let t = thread::spawn(move ||{
   | ^^^^^^^ value moved into closure here, in previous iteration of loop
12 | // 从信号量中取得共享内存
13 | let mut names = m.lock().unwrap();
   | - use occurs due to use in closure

error: aborting due to previous error

实际处理时,信号量没有实现Copy特性,同一个信号量是不能被多个线程采用。尝试对m进行clone操作,但发现m也没提供clone方法。这该如何是好。

结合项目来看,事实上,Rust是允许多所有权的,这在Rust中被称为Rc(引用计数:Reference counted),Rc是一种智能指针,采用它,能够使变量被多个地方采用。在同时发的环境中,Rust还提供了一种原子引用计数:Arc(Atomically reference counted)的类型,采用它来操作变量是具有原子性的,因此,它能够保证并发的安全性。

use std::thread;
use std::sync::{Mutex, Arc};

fn main() {
    // Arc,它的内部为之前的信号量
    let m = Arc::new(Mutex::new(vec![]));
    
    // 循环中创建多个线程
    let mut threads = vec![];
    for i in 1..10 {
        // m具有了clone方法
        let m = m.clone();

        // 创建线程
        let t = thread::spawn(move ||{
            // Arc类型可以直接使用内部的值,从信号量中取得共享内存的方法与不使用Arc完全一致
            let mut names = m.lock().unwrap();

            // 修改共享内存
            names.push(format!("thread{}", i));
        });
        
        threads.push(t);
    }

    // 等待所有线程结束
    for t in threads {
        t.join().unwrap();
    }
    
    // 打印接龙名单
    let names = m.lock().unwrap();
    println!("{:?}", names);
}

在这个场景下,这个就能够正常运行了,最后结果的名单同时不是按顺序的,也证明了并发的有效性。

实际处理时,虽然Rust对共享内存的安全进行了大量的工作,但是,它仍不能100%的避免死锁的问题,比如,有两个信号量,当同时拥有这两个信号量时才能进行某些工作,而这两个信号量却分别被两个线程加锁。

落到代码里,这样的问题在于同时发逻辑设计的不好,只能从逻辑上进行避免,无法在语言层面上根除,在进行并发编程中,需小心又小心,谨慎再谨慎。

总结

落到代码里,总的来说,Rust多线程适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。

喜欢(0)

上一篇

基于Vue3+DeepSeek实现方式流式打字机效果实用指南

下一篇

OpenClaw安装部署及应用场景教程(2026最新完整版)实用指南

OpenClaw安装部署及应用场景教程(2026最新完整版)实用指南
猜你喜欢