Rust 多线程编程入门:从 thread::spawn 步入 Rust 并发世界

Rust 多线程编程入门:从 thread::spawn 步进入 Rust 并发世界

这是Rust九九八十一难的第六篇。本篇聊下Rust的多线程。为了提高性能,多线程几乎在每种语言中都有应用。多线程思想是一致的,但是Rust有独特的所有权设计,还是有需要注意的地方。

一、简单入门

rust 复制代码
use std::thread;
use std::time::Duration;

fn main() {
    let handles: Vec<_> = (0..4)
        .map(|i| {
            thread::spawn(move || {
                println!("Thread {} start", i);
                thread::sleep(Duration::from_secs(1));
                println!("Thread {} end", i);
            })
        })
        .collect();

    for h in handles {
        h.join().unwrap();
    }

    println!("All done");
}

1、基本使用:

  • thread::spawn启动一个新线程,代码中开启4个线程

  • move: 把 data 的所有权移动到新线程中;

  • join: 等待线程结束并返回结果(如果线程 panic,会返回 Err)。

2、函数签名

rust 复制代码
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,
参数/返回值 含义
F 线程要执行的函数或闭包
FnOnce() -> T 线程的执行逻辑,只能调用一次(所有权会被转移进线程),底层有cas控制的计数保证一次。
Send 闭包及返回值都必须是可跨线程安全传递的类型,没有实现这个协议会报错,好多默认实现了
'static 保证闭包中捕获的数据在整个线程生命周期内都有效
JoinHandle<T> 用于等待线程结束、获取返回值的句柄

3、并发还是并行

Rust 的 std::thread底层是这样的:

  • 封装操作系统级线程(OS thread),比如在linux使用pthread_create,window平台调用CreateThread/_beginthreadex;
  • 每个 thread::spawn 调用都会创建一个 真实的内核线程
  • 线程调度由操作系统控制,而非 Rust 自身。

这意味着:

  • 如果有多核 CPU(如 8 核),多个线程可能 真正同时执行(即并行);
  • 如果是单核 CPU,线程会轮流切换执行(即并发)。

二、Rust并发设计理念

在 C++、Java、Go 里,数据竞争是不是延后到了运行时检查或者开发者自律。而 Rust 想实现的是:"在编译期就证明不会产生数据竞争。","与其让程序在运行时崩溃,不如在编译期拒绝编译",这是Rust与其他语言并发模型的核心区别。在编译期防御数据竞争这块,Rust有三个核心概念:

核心机制 作用
所有权 (Ownership) 确保资源只有一个拥有者,防止悬垂引用。
借用规则 (Borrowing) 限制并发访问方式(不可变共享 or 独占修改)
Send / Sync Trait 告诉编译器:这个类型是否能安全地跨线程

靠「类型系统 + 编译期检查」来保证安全,而不是靠运行时锁定或 GC。

1、所有权与借用规则下,根本不允许数据竞争

Rust 默认规则:

  • 同一时刻只能有一个可变引用 (&mut T)
  • 或多个不可变引用 (&T)
  • 二者不能共存。

所以在单线程内:

ini 复制代码
let mut x = 10;
let y = &x;      // 不可变借用
let z = &mut x;  // ❌ 错误:不能同时有 & 和 &mut

编译器直接报错。

2、SendSync能不能跨线程共享的根本

Rust 用两个 trait 控制多线程安全:

Trait 意义 示例
Send 允许被移动到另一个线程 Vec<T>i32
Sync 允许多个线程同时引用 &T if T: Sync

举例:

  • i32Send + Sync → 安全跨线程共享
  • Rc<T> 不是 Send(不是线程安全的)
  • Arc<T>Send + Sync(内部原子计数)

编译器据此拒绝潜在的线程安全错误。所以根本无法写出数据竞争代码 (除非用 unsafe)。下面介绍下具体实现。

三、通信和共享方法

1、线程通信:mpsc 通道(std::sync::mpsc)

"Fearless Concurrency = Ownership + Message Passing " ------ Rust encourages message passing over shared memory.

Rust设计者提供了**(mpsc)** 来进行线程通信。mpsc中Sender 负责发送,Receiver 负责接收。内部通过队列实现(阻塞或非阻塞)。下面是单生产者多消费者代码示例:

rust 复制代码
use std::sync::mpsc;
use std::thread;

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

    for i in 0..3 {
        let tx = tx.clone();
        thread::spawn(move || {
            tx.send(format!("Task {} done", i)).unwrap();
        });
    }

    for msg in rx {
        println!("Got: {}", msg);// Got: Task 2 done \n Got: Task 0 done \n Got: Task 1 done
    }
}

特点

  • ✅ 无需共享内存(避免锁),天然线程安全
  • 零拷贝:多个线程引用同一份数据, 自动计数,安全释放
  • ⚠️ 通信开销更大, 不适合共享复杂结构,需要频繁修改的状态。原因是send 都会 复制(或移动)整个结构体 ,如果结构体很大(如几 MB 的缓存数据),就会产生大量的 内存拷贝。线程之间并不是"共享",而是不断地"发送 + 拷贝 + 接收。如果共享,适合后面的几个共享方案Arc等。

适用场景:一组任务把结果发给中心线程(日志、聚合等)。

2、Arc<T> --- 多线程只读共享

Arc(Atomic Reference Counted)是线程安全的引用计数智能指针。和Rc类似,但支持多线程。从原理看,Arc内部使用原子操作 (AtomicUsize) 管理计数; 可安全地在多线程中 clone + drop; 共享的是 不可变引用(&T)

rust 复制代码
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);

    for _ in 0..3 {
        let data = Arc::clone(&data);
        thread::spawn(move || {
            println!("{:?}", data);
        });
    }
}

特点

  • 安全:只读共享,没写操作
  • 零拷贝:多个线程引用同一份数据, 自动计数,安全释放
  • ⚠️ 不可变:Arc不允许修改 T,所以引入了Arc<Mutex>。

适用场景:动态任务(有产生和销毁)、如多线程读取共享配置、缓存、静态表。

3、Arc<Mutex<T>> ------ 多线程可变共享(独占访问)

Arc 负责跨线程共享所有权Mutex 负责内部可变性 + 线程互斥 。组合后实现"多线程可变共享数据"。

如下面示例,当线程调用 .lock() 时,会先获取互斥锁,然后返回 MutexGuard,最后离开作用域自动释放。

rust 复制代码
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));

    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap(); // 获得锁
            *num += 1;
        });
        handles.push(handle);
    }

    for h in handles {
        h.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap()); //Result: 10
}

特点

  • ✅ 可修改(通过 lock() 获取可变引用),最通用的多线程共享方案
  • ✅安全防止数据竞争
  • ⚠️ 低并发时 OK,高并发时可能锁竞争,性能损耗较大
  • ⚠️ 多个锁交叉持有时有死锁风险

适合简单共享内存模型(类似 Java 的 synchronized)。

4、Arc<RwLock>:多读单写方案

RwLock(读写锁)允许:多个线程同时读;只有一个线程写。相比Arc<Mutex>,优化了多读。它使用读写锁状态标记:read()则共享读锁计数 +1,write()则独占写锁(等待所有读锁释放)。

rust 复制代码
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(0));

    let readers: Vec<_> = (0..3).map(|_| {
        let d = Arc::clone(&data);
        thread::spawn(move || {
            let r = d.read().unwrap();
            println!("Read: {}", *r);
        })
    }).collect();

    let writer = {
        let d = Arc::clone(&data);
        thread::spawn(move || {
            let mut w = d.write().unwrap();
            *w += 1;
            println!("Write done");
        })
    };

    for r in readers { r.join().unwrap(); }
    writer.join().unwrap();
}

特点

  • ✅ 多读单写性能好,更细粒度控制
  • ⚠️ 写操作会阻塞所有读,不适合高写入频率场景

适合多线程频繁读取,偶尔写入(如配置热更新),缓存系统、内存数据库等。

5、DashMap ------ 高性能并发 HashMap(推荐)

DashMap 多分片(sharding)结构:将 HashMap 拆分成多个内部桶,每个桶一个锁;并发访问时,不同键落在不同桶中,不会相互阻塞;内部使用 parking_lot 替代标准锁,性能更高。

安装:

ini 复制代码
[dependencies]
dashmap = "5"

示例代码:

rust 复制代码
use dashmap::DashMap;
use std::thread;

fn main() {
    let map = DashMap::new();

    let mut handles = vec![];
    for i in 0..4 {
        let map = map.clone();
        handles.push(thread::spawn(move || {
            map.insert(i, i * 10);
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    for r in map.iter() {
        println!("{:?}", r);
    }
}

特点

  • ✅ 高并发性能极佳,适合 CPU 密集并发
  • ⚠️ 不保证强一致性(瞬时读可能看到旧值),较重

适合缓存系统、并发计数器、异步任务共享表,多线程异步 Web 服务共享状态

6、parking_lot::Mutex / RwLock ------ 更快的同步原语

Rust 标准库的 Mutex 使用系统锁实现(例如 Linux futex), 而 parking_lot 使用无锁队列 + 自旋优化,性能提升 30%~50%。

rust 复制代码
use parking_lot::Mutex;
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..4 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            *data.lock() += 1;
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    println!("Result = {}", *data.lock());
}

适用场景

  • 替代标准锁,尤其在高并发或短临界区
  • Tokio、Actix、DashMap 内部都使用它

7、Arc<Atomic*> ------ 无锁共享方案

适用于 简单数值或标志 的无锁共享。 Rust 提供 AtomicBool, AtomicUsize, AtomicI64 等类型。

rust 复制代码
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

fn main() {
    let counter = Arc::new(AtomicUsize::new(0));

    let mut handles = vec![];
    for _ in 0..10 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            c.fetch_add(1, Ordering::SeqCst);
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    println!("Result = {}", counter.load(Ordering::SeqCst));
}

特点

  • ✅ 无锁,性能最高
  • ⚠️ 不支持复杂结构

适用计数器、标志位、任务统计,需要极致性能的低层系统代码

三、线程编排

上面介绍了数据共享,但是现实场景经常有线程先后顺序或者聚合问题,这就涉及到线程编排。Rust 线程确实可以编排(orchestrate),但不像一些高级语言(例如 Go 的 goroutine + channel、或 Java 的 ExecutorService + CompletableFuture)那样内置一整套编排机制。Rust 提供了底层的线程控制能力,而"编排"往往通过以下几种方式实现。

1、std::thread+JoinHandle

Rust 最基础的线程控制方式是用 std::thread::spawn 启动线程,然后用 join() 编排执行顺序:

rust 复制代码
use std::thread;

fn main() {
    let t1 = thread::spawn(|| {
        println!("任务 A");
    });

    let t2 = thread::spawn(|| {
        println!("任务 B");
    });

    // 等待线程结束
    t1.join().unwrap();
    t2.join().unwrap();

    println!("主线程收尾");
}

特点

  • 手动控制线程启动与结束,可以按顺序 join 实现简单编排;
  • 但 join 是阻塞的,灵活性有限。

2、通道 (Channel) + 多线程编排

Rust 的 std::sync::mpsc 提供了多生产者单消费者通道,线程之间可以用它传递任务和结果。

rust 复制代码
use std::sync::mpsc;
use std::thread;

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

    for i in 0..5 {
        let tx = tx.clone();
        thread::spawn(move || {
            let result = i * 2;
            tx.send(result).unwrap();
        });
    }

    drop(tx); // 关闭发送端
    for msg in rx {
        println!("收到结果: {}", msg);
    }
}

编排思路:

  • 主线程不需要显式 join;
  • 谁先完成谁就先发消息;
  • 可以实现类似"任务调度中心"的结构。

3、任务池 (Thread Pool)

如果要批量编排大量线程(比如并发 1000 个任务),直接 spawn 代价太大,可以用线程池。常用库:

  • threadpool
  • rayon(最强大的并行任务编排库)
  • `tokio::task::spawn_blocking`\](异步环境中安全执行同步任务)

rust 复制代码
use rayon::prelude::*;

fn main() {
    let nums: Vec<_> = (1..=10).collect();

    let squared: Vec<_> = nums.par_iter()
        .map(|x| x * x)
        .collect();

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

特点:

  • 自动调度任务到线程池,自动 load balancing;
  • 高性能且线程安全;

适合批量计算任务

4、基于 Actor / Future 模型的线程编排

在更复杂的系统中,我们通常使用:Actor 模型(如 actix 框架),Future + async runtime(如 tokioasync-std)。这类系统让"任务调度"成为核心机制。

示例:Tokio 的任务编排

rust 复制代码
use tokio::task;

#[tokio::main]
async fn main() {
    let h1 = task::spawn(async {
        println!("任务 1");
        10
    });

    let h2 = task::spawn(async {
        println!("任务 2");
        20
    });

    let res1 = h1.await.unwrap();
    let res2 = h2.await.unwrap();

    println!("总和: {}", res1 + res2);
}

特点:

  • spawn 返回 JoinHandle<T>,可以用 .await 等待;
  • runtime 负责线程调度;
  • 支持并发编排、超时、取消等复杂逻辑;
  • 类似 Java 的 CompletableFuture 或 Go 的 goroutine。

适合I/O 密集型系统

四、线程暂停和终止

java面试常问,线程的状态机,如挂起,和终止等,Rust 没有提供类似 Java 的线程状态机 API,也无法查询。这里只说下挂起和终止。

1、挂起线程的方法

Rust 不提供直接挂起线程的 API(像 Java 的 Thread.suspend() 已被废弃)。常用方法是 线程自己控制执行

方法一:thread::sleep
rust 复制代码
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        println!("Thread working");
        thread::sleep(Duration::from_secs(3)); // 暂停 3 秒
        println!("Thread resumed");
    });

    handle.join().unwrap();
}

sleep 让线程阻塞一段时间,模拟暂停

CPU 不会消耗,但线程仍占用栈和调度资源

方法二:条件变量控制暂停/恢复

下面用到了Condvar,简单介绍下。Condvar(Condition Variable)是一种线程同步原语,用于 线程间等待和通知 ,通常和 互斥锁(Mutex) 一起使用,条件状态保存在 Mutex 内部(这里是 bool)。当线程调用 cvar.wait() 或wait_timeout():

  • 线程进入阻塞状态(等待条件)
  • CPU 不占用
  • 等待期间释放 Mutex

当被通知或超时:线程重新获取锁,继续执行后续逻辑。Condvar 是实现"挂起-恢复"模式的安全工具。

rust 复制代码
use std::sync::{Arc, Mutex, Condvar};
use std::thread;

fn main() {
    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    let pair_clone = pair.clone();

    let handle = thread::spawn(move || {
        let (lock, cvar) = &*pair_clone;
        let mut started = lock.lock().unwrap();
        while !*started {
            started = cvar.wait(started).unwrap(); // 阻塞等待信号
        }
        println!("Thread resumed");
    });

    thread::sleep(std::time::Duration::from_secs(2));
    {
        let (lock, cvar) = &*pair;
        let mut started = lock.lock().unwrap();
        *started = true; // 发送信号
        cvar.notify_one();
    }

    handle.join().unwrap();
}

特点:

  • 可实现线程暂停/恢复
  • 安全,不破坏内存或锁

2、终止线程的方法

Rust 不允许强制杀死线程 (没有 thread.kill()),因为强制杀线程可能破坏内存安全,可能导致 Mutex 锁未释放,堆资源泄漏。如果退出,可以使用以下方法。

方法一:使用 共享标志
rust 复制代码
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use std::thread;
use std::time::Duration;

fn main() {
    let stop_flag = Arc::new(AtomicBool::new(false));
    let flag_clone = stop_flag.clone();

    let handle = thread::spawn(move || {
        let mut i = 0;
        while !flag_clone.load(Ordering::Relaxed) {
            println!("Working {}", i);
            i += 1;
            thread::sleep(Duration::from_millis(500));
        }
        println!("Thread exiting safely");
    });

    thread::sleep(Duration::from_secs(2));
    stop_flag.store(true, Ordering::Relaxed); // 主动通知线程退出
    handle.join().unwrap();
}
方法二:使用 通道通知
rust 复制代码
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

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

    let handle = thread::spawn(move || {
        loop {
            if rx.try_recv().is_ok() { // 收到停止信号
                println!("Thread stopping");
                break;
            }
            println!("Working...");
            thread::sleep(Duration::from_millis(500));
        }
    });

    thread::sleep(Duration::from_secs(2));
    tx.send(()).unwrap(); // 发停止信号
    handle.join().unwrap();
}

特点:

  • 安全退出
  • 资源会自动释放(堆、锁、文件)

五、小结

Rust多线程内容较多,本次介绍了thread的使用,做个入门。内容包含共享变量和通信等,挂起和终止,有部分原理和示例代码。跟其他语言一样,线程也存在耗时、资源占用等问题,一些第三方框架有优化方案,这个后续篇章(如 async/await、Tokio)会详细介绍。

如果觉得有用,请点个关注吧,本人公众号大鱼七成饱

相关推荐
码事漫谈3 小时前
深入剖析:C++、C 和 C# 中的 static
后端
码事漫谈3 小时前
AI智能体全球应用调查报告:从“对话”到“做事”的变革
后端
绝无仅有3 小时前
某大厂跳动Java面试真题之问题与解答总结(二)
后端·面试·github
绝无仅有3 小时前
某大厂跳动Java面试真题之问题与解答总结(三)
后端·面试·架构
野犬寒鸦4 小时前
从零起步学习Redis || 第十章:主从复制的实现流程与常见问题处理方案深层解析
java·服务器·数据库·redis·后端·缓存
江上月5136 小时前
django与vue3的对接流程详解(上)
后端·python·django
秦禹辰7 小时前
轻量级开源文件共享系统PicoShare本地部署并实现公网环境文件共享
开发语言·后端·golang
Emrys_7 小时前
Redis 为什么这么快?一次彻底搞懂背-后的秘密 🚀
后端·面试
程序员小假7 小时前
我们来说一说 Java 自动装箱与拆箱是什么?
java·后端