Skip to content

线程与消息传递

本章把前面学到的所有权、trait 与智能指针组合起来,回答一个问题:Rust 凭什么敢说"无畏并发"。 主线是三种并发模型:线程、消息传递(channel)与共享状态(Mutex/原子类型)。 前置知识:所有权与借用trait 与 Send智能指针

本章目标

  • 能说清并发(concurrency)与并行(parallelism)的区别,以及 std::thread 为什么选择 1:1 线程模型。
  • 能准确复述"无畏并发"承诺的边界:排除了数据竞争(data race),但没有排除死锁、竞态条件(race condition)与内存泄漏。
  • 能用 thread::spawn + move 闭包、thread::scopeJoinHandle::join 完成线程的创建、借用与结果回收。
  • 能用 mpsc::channel / sync_channel 实现多生产者单消费者,并在通道关闭时正确处理 Err
  • 能写出 Arc<Mutex<T>>Arc<RwLock<T>>AtomicUsize 三种共享状态方案,并说出各自的适用边界。
  • 能解释 Send / Sync 的精确定义与 auto trait 机制,看懂 E0277 ... cannot be sent between threads safely
  • 能用 CondvarBarrierOnceLockLazyLockthread_local! 解决具体的协作问题。
  • 能区分数据竞争与竞态条件,并知道什么场景应该转向〈异步编程〉一章的异步。

概念地图:并发、并行与"无畏并发"的边界

并发不等于并行

并发(concurrency) 是"多个任务在重叠的时间段内推进",并行(parallelism) 是"多个任务在同一时刻真正同时执行"。并发是结构问题(如何组织任务), 并行是执行问题(需要多少硬件)。单核 CPU 上可以写出并发程序,但写不出并行程序。

维度并发并行
关注点任务调度与协作同时执行的吞吐
硬件要求一个核就够多核 / 多 CPU
典型手段线程切换、async、事件循环多线程、SIMD、多进程
失败模式死锁、饥饿、竞态条件缓存伪共享、扩展性瓶颈

OS 线程 vs 绿色线程:std 为什么是 1:1

OS 线程(operating system thread,内核线程) 由操作系统调度,每个线程有独立的内核栈;绿色线程(green thread) 由用户态运行时调度, M:N 地把大量轻量任务复用到少量内核线程上。

模型映射关系调度者切换成本代表
1:11 个用户线程 ↔ 1 个内核线程操作系统高(陷入内核)Rust std::thread、Java Thread、C++ std::thread
N:1N 个用户线程 → 1 个内核线程用户态运行时早期 Java 绿色线程
M:NM 个用户线程 ↔ N 个内核线程用户态运行时Go goroutine、Erlang process
异步任务在少数线程上协作式让出用户态执行器最低Rust async + tokio

🧠 原理:Rust 1.0 之前(0.1x 时代)内置过绿色线程,后来被移除。原因是:绿色线程必须绑定一个运行时(runtime),而运行时要做调度、要处理阻塞式系统调用、 要给每个任务分配栈——这些策略选择不该由语言强制。std 选择 1:1,让标准库不携带运行时;需要 M:N 时由 tokioasync-std 这类库在〈异步编程〉一章的 async 模型里提供。

代价也要说清楚:std::thread 的一个线程默认栈大小是 2 MiB(Windows 上由链接器/PE 决定,可用 Builder::stack_size 覆盖), 创建 10 万个线程会直接耗尽地址空间。所以"任务数量巨大且大多在等 I/O"时,正确的工具是异步,而不是线程。

💡 对照:Java 的 Thread 也是 1:1(映射到内核线程),所以 Java 才有 ExecutorService 线程池来摊薄创建成本; Python 的 threading.Thread 同样是 1:1,但受 GIL 限制无法真正并行执行字节码;Go 的 go f() 是 M:N,创建成本极低。

"无畏并发"到底保证了什么

Rust 的官方口号是无畏并发(fearless concurrency)。它的准确含义是:

编译器在编译期静态地排除数据竞争(data race)。

数据竞争的定义需要同时满足三个条件:两个及以上的线程访问同一块内存;至少一个是写操作;没有任何同步手段。三者同时成立才叫数据竞争。 Rust 通过 Send / Sync 两个 auto trait(第 「SendSync 深入」一节)把"能跨线程传递/共享"变成类型系统里的约束,凡是不满足的一律编译失败。

但以下是不保证的,必须记牢边界:

问题Rust 编译期是否排除说明
数据竞争(data race)✅ 排除Send/Sync + 所有权,安全 Rust 中不可能出现
竞态条件(race condition)❌ 不排除逻辑上的"先检查后使用"依然可能出错,见「数据竞争 vs 竞态条件」
死锁(deadlock)❌ 不排除两个 Mutex 反向加锁,编译完全合法
活锁 / 饥饿(livelock / starvation)❌ 不排除自旋重试永不成功,或写锁永远抢不到
内存泄漏(memory leak)❌ 不排除Rc 循环引用、Box::leak、忘记 join 的线程
原子性缺失导致的不变式破坏❌ 不排除Mutex 只保护你真正放进锁里的数据,见「MutexGuard 的能力与"保护范围"边界」
unsafe 代码里的 UB❌ 不排除错误地 unsafe impl Send,见「unsafe impl Send/Sync:正确性与风险」

⚠️ 陷阱:初学者常把"无畏并发"理解成"并发一定正确"。正确的理解是:类型系统帮你消灭了一整类最难调试的 bug(数据竞争),剩下的逻辑正确性仍然要靠你自己设计


std::thread:创建、借用与回收

spawnjoin:最小的线程程序

下面这段代码演示最基本的模式:spawn 启动线程并返回 JoinHandlejoin 阻塞当前线程直到目标线程结束并取回返回值。

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

fn main() {
    // spawn 接收一个 FnOnce() -> T,返回 JoinHandle<T>
    let handle: thread::JoinHandle<u32> = thread::spawn(|| {
        for i in 1..=3 {
            println!("子线程: {i}");
            thread::sleep(Duration::from_millis(10));
        }
        6 * 7 // 闭包的最后表达式就是这个线程的返回值
    });

    println!("主线程继续做别的事");
    // join 返回 Result<T, Box<dyn Any + Send>>,正常结束是 Ok
    let answer = handle.join().unwrap();
    println!("子线程返回: {answer}");
}
text
输出(顺序可能交错,但子线程的三行一定在 join 之前打印完):
主线程继续做别的事
子线程: 1
子线程: 2
子线程: 3
子线程返回: 42

🧠 原理spawn 的签名是 pub fn spawn<F, T>(f: F) -> JoinHandle<T> where F: FnOnce() -> T + Send + 'static, T: Send + 'static。 注意三个约束:FnOnce(线程体只执行一次)、Send(闭包要跨线程搬过去)、'static(闭包不能含短命借用)。这三个约束就是「为什么必须 move」与 「SendSync 深入」一节的全部动机。

为什么必须 moveFnOnce + Send + 'static

thread::spawn 无法知道新线程会活多久,所以它要求闭包满足 'static。如果闭包按默认规则借用外部变量,借用生命周期就受限于当前函数栈帧, 必然短于 'static,编译失败:

rust
use std::thread;

fn main() {
    let v = vec![1, 2, 3];
    let h = thread::spawn(|| {          // 编译错误 E0373
        println!("{:?}", v);            // v 是借用进来的
    });
    h.join().unwrap();
}
text
error[E0373]: closure may outlive the current function, but it borrows `v`,
              which is owned by the current function
 --> src/main.rs:4:27
  |
4 |     let h = thread::spawn(|| {
  |                           ^^ may outlive borrowed value `v`
5 |         println!("{:?}", v);
  |                          - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
help: to force the closure to take ownership of `v` (and any other referenced variables),
      use the `move` keyword
  |
4 |     let h = thread::spawn(move || {
  |                           ++++

加上 move 后,v所有权被搬进闭包,闭包类型因此满足 'static

rust
use std::thread;

fn main() {
    let v = vec![1, 2, 3];
    // move 把 v 的所有权交给新线程;主线程从此不能再使用 v
    let h = thread::spawn(move || v.iter().sum::<i32>());
    println!("sum = {}", h.join().unwrap());
    // println!("{:?}", v); // 取消注释会报 E0382:v 已被移走
}
text
输出:sum = 6

⚠️ 陷阱move 是"按值捕获所有被用到的变量",而不是"只对需要移动的变量生效"。循环里连续 spawn 时,如果每个闭包都 move 同一个 Arc, 第一次之后原变量就没了——这正是 「Arc<Mutex<T>>:完整的并发计数器」一节 Arc::clone 惯例存在的原因。

thread::scope:借用非 'static 数据(1.63+)

move 的代价是所有权被搬走,主线程再也用不了那些数据。作用域线程(scoped thread) std::thread::scope(最低稳定版本 1.63) 解决了这个矛盾:它保证作用域结束前所有子线程都已结束,因此编译器允许子线程借用作用域内的局部变量。

rust
use std::thread;

fn main() {
    let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
    let (left, right) = data.split_at(4); // 两个 &[i32],生命周期短于 'static

    // scope 会阻塞到闭包内所有 spawn 的线程结束
    let total = thread::scope(|s| {
        let a = s.spawn(|| left.iter().sum::<i32>());  // 借用 left,无需 move
        let b = s.spawn(|| right.iter().sum::<i32>()); // 借用 right
        a.join().unwrap() + b.join().unwrap()
    });

    println!("total = {total}");
    println!("data 仍然可用: {data:?}"); // 所有权从未离开主线程
}
text
输出:
total = 36
data 仍然可用: [1, 2, 3, 4, 5, 6, 7, 8]

scope 的返回值就是闭包的返回值;如果作用域内任何线程 panic,scope 本身会在退出时 panic,消息为 a scoped thread panicked。 它内部帮你做了所有 join,所以闭包里不需要手动 join(当然手动 join 以便取回结果是可以的)。

⚠️ 陷阱scope 只放宽了 'static没有放宽 Send,也没有放宽借用检查。 下面这种想用两个线程分别修改同一个 String 的写法仍然报 E0499(同一时刻只能有一个可变借用):

rust
use std::thread;

fn main() {
    let mut n = 0;
    thread::scope(|s| {
        s.spawn(|| { n += 1; }); // error[E0499]: cannot borrow `n` as mutable more than once
        s.spawn(|| { n += 1; });
    });
}

正确做法是换成 AtomicUsize(「原子类型与内存序」一节)或 Mutex(「共享状态:MutexRwLockArc」一节)把可变性收进同步原语内部。

线程标识、可用并行度与 sleep

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

fn main() {
    // ThreadId 是 Copy + Eq + Hash,可比较但不保证从 0 连续
    println!("主线程 id = {:?}", thread::current().id());

    let ids = thread::scope(|s| {
        let handles: Vec<_> = (0..3)
            .map(|_| s.spawn(|| thread::current().id()))
            .collect();
        handles.into_iter().map(|h| h.join().unwrap()).collect::<Vec<_>>()
    });
    println!("子线程 id = {ids:?}");

    // 返回 Ok(非零并行度);在受限环境(如某些容器)里可能返回 Err
    match thread::available_parallelism() {
        Ok(n) => println!("可用并行度 = {n}"),
        Err(e) => println!("无法获取并行度: {e}"),
    }

    // sleep 至少睡这么久,可能更久(受调度影响),不是精确计时器
    thread::sleep(Duration::from_millis(50));
}
text
一台 32 核机器上的实际输出(每次运行可能不同):
主线程 id = ThreadId(1)
子线程 id = [ThreadId(2), ThreadId(3), ThreadId(4)]
可用并行度 = 32

💡 对照ThreadId 类似 Java 的 Thread.getId()、Python threading.get_ident()available_parallelism() 类似 Java 的 Runtime.getRuntime().availableProcessors() 与 Python 3.13 的 os.process_cpu_count()。 注意 ThreadId 只保证在当前进程内唯一,且可被复用,不要拿它当业务主键。

panic 传播:join()Result

子线程 panic 不会杀死整个进程(这与 C++ 的 std::terminate 不同),但 join() 会返回 Err,里面装的是 panic 的负载(payload)。

rust
use std::thread;

fn main() {
    let ok = thread::spawn(|| 1 + 1);
    println!("正常: {:?}", ok.join());

    let bad = thread::spawn(|| panic!("子线程炸了: {}", 42));
    let err = bad.join().unwrap_err(); // 这就是"传播":父线程显式决定如何处理
    // panic! 的负载是 Box<dyn Any + Send>,需要 downcast 才知道具体类型
    if let Some(msg) = err.downcast_ref::<&str>() {
        println!("捕获到 &str 负载: {msg}");
    } else if let Some(msg) = err.downcast_ref::<String>() {
        println!("捕获到 String 负载: {msg}");
    } else {
        println!("捕获到未知负载(例如 panic_any 传入的自定义类型)");
    }
}
text
输出(顺序略有交错):
正常: Ok(2)
thread '<unnamed>' panicked at src/main.rs:7:31:
子线程炸了: 42
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
捕获到 &str 负载: 子线程炸了: 42

🧠 原理panic!线程边界被捕获并转成 Result,这是 Rust 并发模型的重要设计:一个线程的逻辑失败不会破坏其他线程的内存状态。 这一点与共享内存模型配合得非常好——所以 「共享状态:MutexRwLockArc」一节反复强调"临界区尽量小、不要在持锁时 panic"。

⚠️ 陷阱panic!("...") 产生 &'static str 负载,panic!("{}", x) 产生 String 负载,而且不同版本可能不同, 所以稳健的代码两个都 downcast_ref 一遍(像上面那样),或者统一用 std::panic::panic_any(MyError { .. }) 传自定义类型。

Builder:命名线程与调整栈大小

thread::Builder 用于在 spawn 之前配置属性,最常用的是 name(日志/调试器里可见)和 stack_size(深递归算法需要)。

rust
use std::thread;

fn main() {
    // name 会出现在 panic 消息与调试器中:'worker-1' panicked at ...
    let h = thread::Builder::new()
        .name("worker-1".to_string())
        .stack_size(4 * 1024 * 1024) // 4 MiB,默认通常为 2 MiB
        .spawn(|| {
            println!("我的名字是 {:?}", thread::current().name());
            42
        })
        .expect("线程创建失败"); // Builder::spawn 返回 io::Result<JoinHandle<T>>

    println!("结果 {}", h.join().unwrap());
}
text
输出:
我的名字是 Some("worker-1")
结果 42

注意 Builder::spawn 返回的是 io::Result<JoinHandle<T>>——系统资源有限, 创建线程可能失败(Windows 上是 ERROR_NOT_ENOUGH_MEMORY 之类)。生产代码里不要无条件 unwrap,至少用 expect 并说明原因, 或者向上传播 ?


消息传递:mpsc 通道

Rust 官方文档有一句名言,来自 Go 社区:

Do not communicate by sharing memory; instead, share memory by communicating. (不要通过共享内存来通信,而要通过通信来共享内存。)

std::sync::mpscmulti-producer single-consumer(多生产者单消费者)通道: Sender<T> 可以克隆出多个,Receiver<T> 只有一个。

基本 API

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

fn main() {
    let (tx, rx) = mpsc::channel::<String>(); // 类型可推断时也可写成 mpsc::channel()

    thread::spawn(move || {
        for i in 1..=3 {
            // send 返回 Result<(), SendError<T>>:接收端全部消失时失败
            tx.send(format!("消息 {i}")).unwrap();
        }
        // 这里 tx 被 drop,通道的最后一个生产者消失
    });

    // recv 阻塞直到拿到一条消息或所有发送端关闭
    // 关闭时返回 Err(RecvError),循环因此自然终止
    while let Ok(msg) = rx.recv() {
        println!("收到: {msg}");
    }
    println!("通道已关闭,循环退出");
}
text
输出:
收到: 消息 1
收到: 消息 2
收到: 消息 3
通道已关闭,循环退出

关键语义速记:

调用返回值阻塞语义
tx.send(v)Result<(), SendError<T>>无界通道不阻塞失败意味着所有 Receiver 都没了,SendError 里能取回 v
rx.recv()Result<T, RecvError>阻塞Err 表示所有 Sender 已 drop 且队列已空
rx.try_recv()Result<T, TryRecvError>不阻塞Empty(暂时没数据)与 Disconnected(发送端全没了)要分开处理
rx.recv_timeout(d)Result<T, RecvTimeoutError>最多阻塞 d超时是 Timeout,与 Disconnected 区分
rx.iter()Iterator<Item = T>阻塞等价于反复 recv().unwrap()通道关闭会 panic
rx.into_iter()Iterator<Item = T>阻塞iter() 相同,for msg in rx 用的就是这个

⚠️ 陷阱rx.iter() 内部对 recv() 的结果做了 unwrap,所以通道关闭时它会 panic,而不是优雅结束。 要优雅结束请手写 while let Ok(m) = rx.recv()。只有在"通道不该关闭"的确定性场景下才用 iter()

多生产者:Sender::clone

Sender 实现了 CloneReceiver 没有),所以可以复制任意多份分发给工作线程:

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

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

    let mut handles = Vec::new();
    for worker in 0..4u64 {
        let tx = tx.clone(); // 每个线程一份 Sender
        handles.push(thread::spawn(move || {
            tx.send(worker * 10).unwrap();
        }));
    }
    // 关键:主线程也要放弃自己那份,否则 rx.recv() 永远等不到"全部关闭"
    drop(tx);

    let mut sum = 0u64;
    while let Ok(v) = rx.recv() {
        println!("收到 {v}");
        sum += v;
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("sum = {sum}");
}
text
输出(顺序不确定):
收到 0
收到 10
收到 20
收到 30
sum = 60

⚠️ 陷阱:忘记 drop(tx)mpsc 最常见的死锁原因。只要还有一个 Sender 活着,recv() 就会一直阻塞等待"可能的未来消息"。

有界队列与背压:sync_channel

无界通道(channel)在生产快于消费时会无限增长,最终吃光内存。mpsc::sync_channel(n) 创建一个容量为 n 的有界通道, send 在队列满时阻塞,从而向上游施加背压(backpressure)

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

fn main() {
    let (tx, rx) = mpsc::sync_channel::<u32>(2); // 缓冲区上限 2

    let producer = thread::spawn(move || {
        for i in 0..6 {
            // 队列满时这里会阻塞,生产者被动降速
            tx.send(i).unwrap();
            println!("已发送 {i}(队列可能已满)");
        }
    });

    thread::sleep(Duration::from_millis(30)); // 故意让生产者先跑满缓冲区
    for _ in 0..6 {
        println!("  消费 {:?}", rx.recv());
        thread::sleep(Duration::from_millis(5)); // 消费者慢,反压回去
    }
    producer.join().unwrap();
}
text
输出特征:生产者会打印到 "已发送 1" 后停住,等消费者取走数据才继续
(具体交错依赖调度,但永远不会有超过 2 条消息滞留在通道里)。

sync_channel(0)会合通道(rendezvous channel):容量为 0,send 必须等到 recv 配对成功才返回,常用于"交接棒"式的严格同步。

💡 对照sync_channel(n) 相当于 Java 的 ArrayBlockingQueue(n)sync_channel(0) 相当于 SynchronousQueuechannel() 相当于 LinkedBlockingQueue(无界)。 Go 的 make(chan T, n) 也是这个含义。

Receiver 不是 Sync:多消费者怎么办

Receiver<T>Send不是 Sync。原因很直接:recv(&self) 需要串行化"从队列取走一条消息", 如果多个线程能同时持有 &Receiver 并调用 recv,就必须在标准库内部实现一套锁与唤醒逻辑——std 选择不付这个成本,而是把这个选择交给使用者。

因此下面这段代码编译失败:

rust
use std::sync::{mpsc, Arc};
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel::<i32>();
    let rx = Arc::new(rx);
    for _ in 0..3 {
        let rx = Arc::clone(&rx);
        thread::spawn(move || rx.recv().unwrap()); // 编译错误
    }
    tx.send(1).unwrap();
}
text
error[E0277]: `std::sync::mpsc::Receiver<i32>` cannot be shared between threads safely
   --> src/main.rs:9:31
    |
9   |         thread::spawn(move || rx.recv().unwrap());
    |                 ------------- ^^^^^^^^^^^^^^^^^^ cannot be shared between threads safely
    |
    = help: the trait `Sync` is not implemented for `std::sync::mpsc::Receiver<i32>`
    = note: required for `Arc<std::sync::mpsc::Receiver<i32>>` to implement `Send`
note: required because it's used within this closure
note: required by a bound in `spawn`
    F: Send + 'static,
       ^^^^ required by this bound in `spawn`

错误链条要会读: spawn 要求闭包 Send → 闭包捕获了 Arc<Receiver<i32>>Arc<T>: Send 的前提是 T: Send + SyncReceiver 满足 Send 但不满足 Sync → 整体不 Send

三种正确模式:

模式 A(推荐):单消费者分发。 一个专门的"分发线程"持有 Receiver,把任务再通过其它机制(比如另一个 mpscMutex 保护的队列、或原子索引)发给工作线程。

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

fn main() {
    // 外部任务来源(本例未使用,只用来演示"主通道"的存在)
    let (job_tx, job_rx) = mpsc::channel::<u32>();

    // 每个工作线程一个私有通道:通道天然是单消费者的,所以完全不需要锁
    let mut senders = Vec::new();
    let mut handles = Vec::new();
    for id in 0..3 {
        let (tx, rx) = mpsc::channel::<u32>();
        senders.push(tx);
        handles.push(thread::spawn(move || {
            let mut done = 0;
            while let Ok(job) = rx.recv() {
                println!("worker {id} 处理任务 {job}");
                done += 1;
            }
            done // 通道关闭后返回处理条数
        }));
    }

    // 把 Sender 与 JoinHandle 分开存放:前者交给分发者,后者留给主线程收尾
    let dispatcher = thread::spawn(move || {
        for job in 0..7u32 {
            // 简单的轮询分发(真实项目里常用任务队列或 work-stealing 库)
            senders[job as usize % senders.len()].send(job).unwrap();
        }
        senders.len() // 返回持有过的通道数,便于断言
    });

    // senders 已被移入 dispatcher;它返回时全部 drop → 工作线程的 recv 得到 Err
    assert_eq!(dispatcher.join().unwrap(), 3);
    let total: i32 = handles.into_iter().map(|h| h.join().unwrap()).sum();
    assert_eq!(total, 7, "7 个任务必须全部被处理");

    // 主通道从未被投递,接收端此刻应为 Empty 而不是 Disconnected
    let empty = matches!(job_rx.try_recv(), Err(mpsc::TryRecvError::Empty));
    println!("主通道为空: {empty}");
    drop(job_tx);
    println!("共处理 {total} 个任务");
}
text
输出(worker 编号与顺序不定):
worker 0 处理任务 0
worker 1 处理任务 1
...
主通道为空: true
共处理 7 个任务

模式 B:Mutex<Receiver> 用锁把 recv 串行化,简单但所有消费者争同一把锁,扩展性差:

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

fn main() {
    let (tx, rx) = mpsc::channel::<u32>();
    let rx = Arc::new(Mutex::new(rx));

    let handles: Vec<_> = (0..3)
        .map(|_| {
            let rx = Arc::clone(&rx);
            thread::spawn(move || {
                // 锁只护住 recv 这一下,取到值立刻释放
                let msg = rx.lock().unwrap().recv();
                msg.ok()
            })
        })
        .collect();

    for i in 0..3 {
        tx.send(i).unwrap();
    }
    drop(tx);
    for h in handles {
        println!("得到 {:?}", h.join().unwrap());
    }
}

模式 C:crossbeam-channel 如果想直接获得多消费者(MPMC)语义,用成熟的三方库:cargo add crossbeam-channel, 它的 ReceiverSync,可以直接 clone() 分发给多个线程,另外还提供 select!、无界/有界统一 API 与更高的吞吐。本章不展开, 知道"std 的 mpsc 不够用时有这条路"即可。

🚀 进阶std::sync::mpsc 在 1.67 之后换成了基于 crossbeam 的更快实现, 但公开 API 与 Receiver: !Sync 的约束没有变。 性能敏感的多消费者场景请直接使用 crossbeam-channel


延伸阅读


内容以 rustc 1.98.1 · Rust 2024 edition 为基准