Rust in Everything
基于 embassy-sync 0.8.0

*mut () cannot be shared between threads safely

主线位置:跨任务主线的「RawMutex 保护内部状态」。三种 RawMutex 各自是不是 Sync、为什么,见原始互斥;本页讲编译器拦下来的两种用法和各自的改法。

flowchart TB
  ch["Channel NoopRawMutex"] --> u{"怎么共享"}
  u -->|"static"| e1["E0277 不是 Sync"]
  u -->|"引用给别的执行器"| e2["E0277 引用不是 Send"]
  u -->|"StaticCell 同一执行器"| ok1["可以"]
  cs["CriticalSectionRawMutex"] --> ok2["static 跨执行器 中断"]

实验 crate exp-embassy/pita-std,bin noop_static、noop_send、noop_fixed(platform-std,debug);M2 Max、macOS 27、rustc 1.99.0。

现象

放进 static。 例子里的 channel 大多是全局的,类型参数照着「同一个执行器用 Noop 最便宜」选了 NoopRawMutex:

static CH: Channel<NoopRawMutex, u32, 4> = Channel::new();
error[E0277]: `*mut ()` cannot be shared between threads safely
   --> src/bin/noop_static.rs:6:12
    |
  6 | static CH: Channel<NoopRawMutex, u32, 4> = Channel::new();
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `*mut ()` cannot be shared between threads safely
    |
    = help: within `NoopRawMutex`, the trait `Sync` is not implemented for `*mut ()`
note: required because it appears within the type `PhantomData<*mut ()>`
...
note: required because it appears within the type `NoopRawMutex`
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-sync-0.8.0/src/blocking_mutex/raw.rs:70:12
...
    = note: required for `embassy_sync::blocking_mutex::Mutex<NoopRawMutex, RefCell<channel::ChannelState<u32, 4>>>` to implement `Sync`
note: required because it appears within the type `embassy_sync::channel::Channel<NoopRawMutex, u32, 4>`
...
    = note: shared static variables must have a type that implements `Sync`

程序里没有线程,报错却说「between threads」:Sync 是 Rust 对「多个执行上下文同时访问」的唯一说法,中断处理函数、另一个核、另一个执行器都算。

借给另一个执行器。 不放 static,改成 StaticCell 造一个,把 &'static 交给另一个线程上的执行器里的任务:

type Ch = Channel<NoopRawMutex, u32, 4>;

#[embassy_executor::task]
async fn consumer(ch: &'static Ch) { /* ... */ }

fn second_executor(spawner: SendSpawner, ch: &'static Ch) {
  spawner.spawn(consumer(ch).unwrap());
}
// main: std::thread::spawn(move || EXEC2.init(Executor::new()).run(|s| second_executor(s.make_send(), ch)));

报两处,一处在 std::thread::spawn(闭包捕获了 ch),一处在 SendSpawner::spawn:

error[E0277]: `*mut ()` cannot be shared between threads safely
   --> src/bin/noop_send.rs:17:17
    |
 17 |   spawner.spawn(consumer(ch).unwrap());
    |           ----- ^^^^^^^^^^^^^^^^^^^^^ `*mut ()` cannot be shared between threads safely
    |           |
    |           required by a bound introduced by this call
...
    = note: required for `&'static embassy_sync::channel::Channel<NoopRawMutex, u32, 4>` to implement `Send`
...
note: required by a bound in `SendSpawner::spawn`
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-executor-0.10.0/src/spawner.rs:223:21
    |
223 |     pub fn spawn<S: Send>(&self, token: SpawnToken<S>) {
    |                     ^^^^ required by this bound in `SendSpawner::spawn`

出处

NoopRawMutex 用一个 PhantomData<*mut ()> 关掉 Sync,只补回 Send:

pub struct NoopRawMutex {
    _phantom: PhantomData<*mut ()>,
}

unsafe impl Send for NoopRawMutex {}

它的 lock 什么也不做,正确性全靠「同一时刻只有一个执行上下文碰它」,而 !Sync 就是把这个前提写进类型。blocking_mutex::Mutex<R, T> 只在 R: Sync 时是 Sync,Channel 包着它,于是整条链上都不是 Sync。报错里那串 required because it appears within the type 就是这条链。

第二个错误多了一步:&T 是 Send 当且仅当 T 是 Sync。SendSpawner::spawn 要求令牌的标记类型 S: Send,对 async fn 任务来说 S 由参数决定(Spawner 的「参数是 Send 就够了」),参数是 &'static Channel<NoopRawMutex, ..>,它不是 Send。普通的 Spawner::spawn 没有这个约束,所以同一个执行器里传引用没问题。

修法

同一个执行器内:NoopRawMutex 加 StaticCell,引用传给同一个 Spawner 生出的任务。跨执行器(或要给中断处理函数用):CriticalSectionRawMutex,直接放 static(bin noop_fixed):

type Local = Channel<NoopRawMutex, u32, 4>;
static LOCAL: StaticCell<Local> = StaticCell::new();
static SHARED: Channel<CriticalSectionRawMutex, u32, 4> = Channel::new();

#[embassy_executor::task]
async fn local_consumer(ch: &'static Local) {
  println!("local got {}", ch.receive().await);
}

#[embassy_executor::task]
async fn remote_consumer() {
  println!("remote got {} on {:?}", SHARED.receive().await, std::thread::current().name());
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
  let local: &'static Local = LOCAL.init(Channel::new());
  spawner.spawn(local_consumer(local).unwrap());
  std::thread::Builder::new()
    .name("exec2".into())
    .spawn(|| EXEC2.init(Executor::new()).run(|s| second_executor(s.make_send())))
    .unwrap();
  local.send(1).await;
  SHARED.send(2).await;
  // ...
}
local got 1
remote got 2 on Some("exec2")

不要用 unsafe impl Sync 包一层来让 static 编过:那等于对编译器说「我保证只有一个上下文碰它」,然后在中断里碰它。原始互斥的实验和中断里用 Noop / ThreadMode 都是这么做的,结果是 BorrowMutError panic,而且只在中断恰好打断临界区时才出现。

怎么确认没中招

  • grep -rn 'NoopRawMutex' src/:每一处的值应当是局部的(StaticCell / 栈上),并且只被同一个 Spawner spawn 出的任务用。
  • 出现 unsafe impl Sync 或 unsafe impl Send 包着 embassy-sync 类型时,逐个追问:有没有中断处理函数、InterruptExecutor 的任务、另一个执行器碰它。
  • 拿不准时用 CriticalSectionRawMutex:在单核 Cortex-M 上它是存 PRIMASK、cpsid i、用完写回(原始互斥),代价是临界区里中断要等一会儿;一个 CriticalSectionRawMutex 的 Channel 整个程序只多 628 字节 .text(Channel 的「代码体积」)。

小结

  • NoopRawMutex 故意 !Sync,用它的 Channel 不能进 static,引用也不能交给 SendSpawner:*mut () cannot be shared between threads safely。
  • 同一执行器内:StaticCell 加引用;跨执行器、跨中断:CriticalSectionRawMutex 放 static。
  • 用 unsafe impl Sync 绕过去,问题会以运行期 BorrowMutError 的形式回来。

💬 关联讨论

发起讨论
加载中...