在 Linux 开发里,并发几乎绕不开:要么是多线程任务调度,要么是网络 I/O,要么是多个执行单元同时操作同一份数据。Rust 之所以在这类场景里经常被拿来讨论,不只是因为它快,更关键的是它把所有权、借用和类型约束直接嵌进了并发模型,让很多传统语言里要靠经验规避的问题,在写代码时就能被看见。
下面这篇文章按实际开发最常见的几种方式来拆解:什么时候直接开线程,什么时候优先用消息通道,什么时候必须上 Arc + Mutex,以及 tokio 异步任务和社区并发容器各自适合什么场景。看完之后,你可以更清楚地判断 Rust 在 Linux 下的并发能力到底强在哪,项目里又该怎么选型。
Rust 并发为什么能兼顾安全与效率
很多语言在并发编程上都面临同一组老问题:数据竞争、悬垂引用、锁使用不当、线程间状态难以追踪。Rust 的特别之处在于,它不是在运行时“尽量帮你避免”,而是通过类型系统和所有权模型,把大量错误前移到编译阶段。
这意味着开发者在 Linux 上写并发程序时,既可以使用操作系统级线程,也可以使用通道、锁、异步运行时或并发容器,而且这些工具之间的边界相对清晰。实际编码时,关键不是“会不会用”,而是“哪一种模型更适合当前任务”。
线程:最直接的并发起点
如果你的任务天然适合拆成几个独立执行单元,最直接的办法就是线程。Rust 标准库提供了 std::thread,可以创建和管理操作系统级线程。
常见写法是用 thread::spawn 启动一个新线程,再通过返回的句柄在主线程中调用 join() 等待它结束。这种模式适合先理解最基础的线程协作关系:谁创建任务、谁等待结果、什么时候退出。
use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("Hello from a thread!");
});
handle.join().unwrap();
}
什么时候适合直接用线程
线程适合计算任务比较明确、生命周期清晰、并发数量不算特别夸张的场景。它的优点是模型直观,和 Linux 下的系统线程对应关系也容易理解;缺点是线程创建和切换都有成本,任务规模一大时就不如异步模型轻量。
消息传递:尽量少共享数据
并发代码里最容易出问题的,往往不是“同时跑起来”,而是“大家一起改同一份数据”。Rust 给出的第一类更稳妥方案,是让线程之间通过消息传递来协作,而不是默认共享状态。
std::sync::mpsc 提供了多生产者单消费者(MPSC)通道。可以把它理解成一条线程间管道:发送端负责投递数据,接收端负责读取数据。这样做的好处是职责边界很清楚,数据所有权也会随着消息发送自然转移。
use std::sync::mpsc;
use std::thread;
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);
}
这种方式解决了什么问题
在这个例子里,String 被发送进通道后,所有权就转交给了接收方,避免了多个线程同时持有并修改同一对象的复杂性。对于任务分发、日志聚合、工作队列这类场景,消息传递通常比手动加锁更容易维护。
共享状态:需要共享内存时用 Arc 与 Mutex
并不是所有程序都能完全避开共享状态。有些情况下,多线程必须围绕同一份计数器、缓存或全局状态工作。这时 Rust 常见的组合是 Arc 加 Mutex。
Arc 是原子引用计数,解决“多个线程如何共同持有一份数据”的问题;Mutex 是互斥锁,解决“同一时间只允许一个线程修改数据”的问题。两者配合后,既能跨线程共享,又能控制访问顺序。
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 handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
这个组合各自负责什么
在上面的例子中,10 个线程都会拿到同一个计数器的共享引用,但真正修改数据前必须先调用 lock() 获取锁。因此,最终输出稳定为 10。这类模式适合确实需要围绕共享资源进行协调的情况,但锁带来的等待和复杂度也要一并考虑。
异步编程:I/O 密集型任务更轻量的方案
如果你的程序主要在处理大量网络连接、文件读写或其他 I/O 密集型任务,直接为每个任务创建一个操作系统线程通常并不划算。Rust 在这方面常见的做法是 async/await 配合异步运行时,比如 tokio。
异步任务和线程不是一回事。线程由操作系统调度,开销更重;而 tokio::spawn 创建的是运行时调度的轻量级任务,适合高并发连接处理。
use tokio::net::TcpListener;
use tokio::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, _) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = [0; 1024];
loop {
let bytes_read = match socket.read(&mut buf).await {
Ok(n) if n == 0 => return,
Ok(n) => n,
Err(e) => {
eprintln!("Failed to read from socket: {:}", e);
return;
}
};
if let Err(e) = socket.write_all(&buf[0..bytes_read]).await {
eprintln!("Failed to write to socket: {:}", e);
return;
}
}
});
}
}
这个 echo 服务器示例说明了什么
这个例子监听 127.0.0.1:8080,每接受一个连接,就启动一个异步任务去读取客户端数据并原样写回。入口上的 #[tokio::main] 宏负责初始化异步运行时,而 .await 则表示当前任务会在 I/O 等待期间让出执行权。对高连接数服务来说,这种模型通常比“一个连接一个线程”更省资源。
并发数据结构:标准库之外的实用补充
标准库覆盖了线程、通道和锁,但在更专业的并发场景里,社区库也很重要。原文提到的 crossbeam 和 rayon,就是 Rust 并发生态里常被实际项目采用的工具。
例如 crossbeam::queue::SegQueue 是无锁并发队列,支持多个线程同时入队和出队。它的意义不只是“能并发”,更在于它针对特定场景给出了比手工拼锁更合适的数据结构实现。
use crossbeam::queue::SegQueue;
use std::thread;
fn main() {
let q = SegQueue::new();
thread::scope(|s| {
s.spawn(|_| {
q.push(42);
});
s.spawn(|_| {
if let Some(item) = q.pop() {
println!("Got: {}", item);
}
});
});
}
为什么这里要用 thread::scope
这个例子里使用了 thread::scope,目的是把线程限制在作用域内,确保它们会在作用域结束前完成执行。这样做可以减少线程生命周期管理上的负担,也更适合演示多个线程围绕同一个并发容器协作的写法。
在 Linux 项目里该怎么选并发模型
如果任务简单且数量有限,线程是最容易理解和落地的起点;如果重点是线程之间解耦协作,消息传递通常比共享状态更稳;如果确实绕不开共享资源,再考虑 Arc + Mutex;如果面对的是高并发网络 I/O,优先看异步模型;如果已经进入高性能并发容器或任务并行处理阶段,再引入 crossbeam、rayon 这类社区方案会更合适。
整体来看,Rust 在 Linux 下的并发能力并不只是一组 API 的堆叠,而是一套从语言规则到库生态都相互配合的设计。它的价值就在于:你依然可以写底层、高性能、贴近系统的并发程序,但很多原本要靠经验和排障去兜底的问题,会更早暴露,也更容易被约束住。









