☰
Dioxus 中持有 Signal 读取时再写入触发 panic 怎么排查?
2026/9/28 6:47:11 网站建设 项目流程

Dioxus 中持有 Signal 读取时再写入触发 panic 怎么排查?

【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus

在 Dioxus 里用Signal做状态管理时,一个常见崩溃是:对同一个 Signal 的读取(read)还没有结束,代码就对它发起写入(write),程序直接 panic。官方文档明确说明这一行为:Signal 和RefCell<T>一样在运行时检查借用,"If you read and write to the signal at the same time, it will panic"(见 packages/signals/docs/signals.md)。这篇文章围绕这个 panic 展开:先识别 panic 文案,再判断是同步还是异步场景下的借用重叠,最后按文档给出的方式修复并验证。

先确认 panic 是不是借用重叠导致的

Signal底层由 generational-box 实现(signals.md 的 "Signals lifecycle" 一节),panic 文案来自 error.rs。两类借用冲突对应两条不同的消息:

  • 已有可变借用时再读取:Failed to borrow because the value was already borrowed mutably.
  • 已有不可变借用时再写入:Failed to borrow mutably because the value was already borrowed immutably.

"持有读取时再写入"属于第二类,即写操作撞上未结束的读。另外,在 debug 构建(debug_assertions)或开启debug_borrowsfeature 时,错误信息会额外附带borrowed_mut_at:/borrowed_at:及对应的源码位置,可以直接指向仍持有借用的那行代码——这是定位问题最快的一步。如果 panic 消息里没有位置信息,只能回到自己的代码里按下面两个典型模式排查。

场景一:同步代码里跨作用域持有读取

signals.md 给出的会 panic 的最小示例:

# use dioxus::prelude::*; let mut signal = use_signal(|| 0); // If you create a read and hold it while you write to the signal, it will panic let read = signal.read_unchecked(); // This will panic signal += 1; println!("{}", read);

read_unchecked()返回的读取值被一直持有到作用域结束,中间的signal += 1就触发了 panic。文档给出两种等价修复方式:

  1. 把读取放进一个在写入之前结束的代码块,让读取先被 drop:
# use dioxus::prelude::*; let mut signal = use_signal(|| 0); { // Since this read is inside a block that ends before we write to the signal, the signal will be dropped before the write and it will not panic let read = signal.read(); println!("{}", read); } signal += 1;
  1. 使用with/with_mut变体,借用只存活在闭包内部:
# use dioxus::prelude::*; let mut signal = use_signal(|| 0); // Or you can use the with and with_write methods which only read or write to the signal inside the closure signal.with(|read| println!("{}", read)); // Since the read only lasts as long as the closure, this will not panic signal.with_mut(|write| *write += 1);

排查时对照检查:出 panic 的函数里是否存在一个先获取的 read(包括read_unchecked()的返回值被存进长生命周期变量),并且它在写入发生前没有被 drop。

场景二:异步代码里跨 await 点持有借用

这是文档特别强调的场景。如果你在use_future/use_resource等异步代码里持有读取或写入跨过.await,借用会在异步等待期间一直开着,此间 UI 或其他代码读写同一个 Signal 就可能 panic:

# use dioxus::prelude::*; # async fn sleep(delay: u32) {} async fn double_me_async(value: &mut u32) { sleep(100).await; *value *= 2; } let mut signal = use_signal(|| 0); use_future(move || async move { // Don't hold reads or writes over await points let mut write = signal.write(); // While the future is waiting for the async work to finish, the write will be open double_me_async(&mut write).await; }); rsx!{ // This read may panic because the write is still active while the future is waiting for the async work to finish "{signal}" };

文档给出的修复方式是:不要把借用传进 await,而是先把需要的值 clone 出来,异步完成后再一次性写回:

# use dioxus::prelude::*; # async fn sleep(delay: u32) {} async fn double_me_async(value: u32) -> u32 { sleep(100).await; value * 2 } let mut signal = use_signal(|| 0); use_future(move || async move { // Clone the value out of the signal let current_value = signal(); // Run the async work let new_value = double_me_async(current_value).await; // Set the signal to the new value signal.set(new_value); }); rsx! { // This read will not panic because the write is never held over an await point "{signal}" };

use_memo的派生值同样在运行时检查借用:如果持有 memo 的读取跨过 await 点,而期间写入 Signal 导致 memo 重跑,也会 panic(见 memo.md 的 "Memos with Async" 一节)。修复思路相同——调用halved()克隆出值再跨 await,而不是把halved.read()的引用传进异步函数。如果你的 panic 堆栈出现在按钮点击(写入 Signal 触发 memo 重跑)时,优先按这个模式排查。

如何验证修复生效

文档对上面每个修复后的示例都标注了 "will not panic"。实际验证方法是重新走一遍原触发路径:

  1. 在 debug 构建下复现原操作(点击触发写入、UI 渲染读取该 Signal 等),确认不再出现Failed to borrow mutably because the value was already borrowed immutably.一类消息;
  2. 如果修复前 debug 构建里 panic 消息带有borrowed_at:位置,确认该位置对应的读取已改为块作用域 /with/ 克隆后写入的形式;
  3. 对最小示例,可以直接把 signals.md 中修复前后的两段代码放进自己的项目跑一遍,先确认会 panic 的形态、再确认修复后不 panic。

局限

  • 借用检查发生在运行时,编译器不会在编译期报出这类重叠读写,只能靠 panic 消息(以及 debug 构建中的位置信息)定位;
  • 本文覆盖的是读写重叠导致的 panic。read_unchecked()等 API 在 Signal 被 drop 后使用同样会 panic(见 read.rs 中的文档说明),那是值生命周期问题,不是借用冲突,排查路径不同。

【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询