*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/ 栈上),并且只被同一个Spawnerspawn 出的任务用。- 出现
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的形式回来。