寻求支持等待时释放借用的单线程异步友好RefCell方案
实现单线程异步友好的可借用Cell
核心需求拆解
要实现的SingleThreadedAsyncCell需满足:
- 单线程环境下安全使用,无需实现
Sync - 调用
.borrow()/.borrow_mut()后,在await前持有借用,进入await时自动释放,彻底避免借用跨异步挂起点 - 支持多异步任务并发调用(比如
join!中的多个方法),因为await期间借用已释放,此时并发操作是安全的
具体实现思路
思路1:异步闭包限制借用生命周期(推荐)
无需照搬RefCell的API,通过异步闭包将借用的生命周期严格限制在同步执行阶段,await前自动释放借用。示例实现:
use std::cell::{RefCell, Ref, RefMut}; use std::future::Future; struct SingleThreadedAsyncCell<T> { inner: RefCell<T>, } impl<T> SingleThreadedAsyncCell<T> { pub fn new(value: T) -> Self { Self { inner: RefCell::new(value), } } // 共享借用:闭包内持有Ref,await前自动释放 pub async fn with_borrow<F, Fut, R>(&self, f: F) -> R where F: FnOnce(Ref<'_, T>) -> Fut, Fut: Future<Output = R>, { let r = self.inner.borrow(); f(r).await } // 可变借用:同理,闭包执行完自动释放RefMut pub async fn with_borrow_mut<F, Fut, R>(&self, f: F) -> R where F: FnOnce(RefMut<'_, T>) -> Fut, Fut: Future<Output = R>, { let mut r = self.inner.borrow_mut(); f(r).await } } // 对应原示例的使用场景 struct Inner; impl Inner { async fn really_do_something(&self) {} async fn really_do_something_else(&mut self) {} } struct Foo { inner: SingleThreadedAsyncCell<Inner>, } impl Foo { pub async fn do_something(&self) { self.inner.with_borrow(|inner| async move { inner.really_do_something().await }).await; } pub async fn do_something_else(&self) { self.inner.with_borrow_mut(|mut inner| async move { inner.really_do_something_else().await }).await; } } async fn do_many_somethings(f: &Foo) { futures::join!(f.do_something(), f.do_something_else(), f.do_something_else()); }
这个方案的核心是:借用对象(Ref/RefMut)的生命周期被严格绑定到异步闭包的同步执行阶段,当闭包返回Future并进入await时,借用已被自动Drop,完全杜绝了跨异步挂起点的借用问题,同时单线程下RefCell的借用检查足以保证安全。
思路2:模拟RefCell风格的自动释放借用
如果要贴近原示例的API形态,可以实现AsyncRef/AsyncRefMut类型,通过生命周期约束强制借用无法跨await点:
use std::cell::RefCell; use std::ops::{Deref, DerefMut}; struct SingleThreadedAsyncCell<T> { inner: RefCell<T>, } impl<T> SingleThreadedAsyncCell<T> { pub fn new(value: T) -> Self { Self { inner: RefCell::new(value) } } pub fn borrow(&self) -> AsyncRef<'_, T> { AsyncRef { inner: self.inner.borrow() } } pub fn borrow_mut(&self) -> AsyncRefMut<'_, T> { AsyncRefMut { inner: self.inner.borrow_mut() } } } // 异步共享借用,Drop时自动释放 struct AsyncRef<'a, T> { inner: std::cell::Ref<'a, T>, } impl<'a, T> Deref for AsyncRef<'a, T> { type Target = T; fn deref(&self) -> &Self::Target { &self.inner } } // 异步可变借用 struct AsyncRefMut<'a, T> { inner: std::cell::RefMut<'a, T>, } impl<'a, T> Deref for AsyncRefMut<'a, T> { type Target = T; fn deref(&self) -> &Self::Target { &self.inner } } impl<'a, T> DerefMut for AsyncRefMut<'a, T> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } }
这个方案依赖Rust的生命周期检查:如果尝试将AsyncRef/AsyncRefMut存入变量并跨await,编译器会直接报错,强制用户只能写成cell.borrow().method().await的形式,此时借用对象会在await前被自动Drop。
思路3:基于UnsafeCell的底层实现
如果需要极致的性能或自定义逻辑,可以用UnsafeCell手动管理借用状态,但这种方式需要手动保证内存安全,容易出错,仅推荐特殊场景使用:
use std::cell::{UnsafeCell, Cell}; use std::future::Future; struct SingleThreadedAsyncCell<T> { inner: UnsafeCell<T>, // 单线程下标记借用状态 is_borrowed: Cell<bool>, is_borrowed_mut: Cell<bool>, } impl<T> SingleThreadedAsyncCell<T> { pub fn new(value: T) -> Self { Self { inner: UnsafeCell::new(value), is_borrowed: Cell::new(false), is_borrowed_mut: Cell::new(false), } } // 共享借用异步方法 pub async fn borrow<F, Fut, R>(&self, f: F) -> R where F: FnOnce(&T) -> Fut, Fut: Future<Output = R>, { if self.is_borrowed_mut.get() { panic!("Already borrowed mutably"); } self.is_borrowed.set(true); // 手动获取不可变引用 let ptr = self.inner.get(); let ref_val = unsafe { &*ptr }; let res = f(ref_val).await; self.is_borrowed.set(false); res } // 可变借用异步方法 pub async fn borrow_mut<F, Fut, R>(&self, f: F) -> R where F: FnOnce(&mut T) -> Fut, Fut: Future<Output = R>, { if self.is_borrowed.get() || self.is_borrowed_mut.get() { panic!("Already borrowed"); } self.is_borrowed_mut.set(true); let ptr = self.inner.get(); let ref_val = unsafe { &mut *ptr }; let res = f(ref_val).await; self.is_borrowed_mut.set(false); res } } // 单线程下标记为Send,确保不跨线程传递 unsafe impl<T: Send> Send for SingleThreadedAsyncCell<T> {}
该方案需注意:必须保证仅在单线程环境使用,同时要处理panic时的状态恢复(可通过封装借用状态到Drop类型实现)。
总结
- 优先选择思路1,API设计更安全,编译器自动保证借用不跨异步挂起点
- 若需要贴近
RefCell的使用习惯,可选择思路2,依赖生命周期检查强制安全 - 非必要不推荐直接使用
UnsafeCell手动实现,容易引入内存安全风险
内容的提问来源于stack exchange,提问作者Vitali
相关产品推荐
相关产品推荐

