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、Send
和 Sync
能不能跨线程共享的根本
Rust 用两个 trait 控制多线程安全:
Trait | 意义 | 示例 |
---|---|---|
Send |
允许被移动到另一个线程 | Vec<T> 、i32 |
Sync |
允许多个线程同时引用 | &T if T: Sync |
举例:
i32
是Send + 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(如 tokio
或 async-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)会详细介绍。
如果觉得有用,请点个关注吧,本人公众号大鱼七成饱。