Rust无动态内存分配的资源共享所有权实现方法
现有实现情况
标准库没有提供专门适配句柄型资源的共享所有权智能指针。第三方生态中仅在面向无堆嵌入式的no_std场景下有少量同类实现,通用开发场景下几乎不会有人专门使用这类组件——毕竟对于i32这类小尺寸句柄,标准库Rc/Arc的堆分配开销已经微乎其微,完全没有做特殊替换的必要。
这里先澄清一个常见认知误区:不存在所有数据全存在栈上、不需要任何共享存储的共享所有权引用计数指针。多个独立所有者要同步同一个资源的引用计数,必须有一块所有持有者都能访问到的公共内存存放计数值,这块公共存储要么分配在堆上,要么放在全局静态内存区,不存在其他可行方案。
实际上你直接使用Arc<Resource>时,因为Resource内仅包含一个i32字段,堆上存储的内容就是2个usize长度的强/弱引用计数加一个i32句柄,64位系统下算上内存对齐总共仅占24字节,堆分配的开销极小。
最简实现方案
按照你描述的「仅维护引用计数和资源句柄」的需求,有两种逻辑非常简单的实现思路:
方案1:栈上存句柄,堆上存共享计数
这种实现和标准库Arc的逻辑几乎完全一致,只是把句柄从堆上挪到每个指针实例的栈空间中,堆上仅存放引用计数:
use std::ptr::NonNull; // 堆上分配的共享计数块 struct CountBlock { strong: usize, weak: usize, } struct SharedResource { handle: i32, counts: NonNull<CountBlock>, } impl SharedResource { fn new() -> Self { let handle = unsafe { create_resource() }; // 堆上分配计数块,初始强引用计数为1 let counts = Box::new(CountBlock { strong: 1, weak: 1 }); Self { handle, counts: NonNull::new(Box::into_raw(counts)).unwrap(), } } } impl Clone for SharedResource { fn clone(&self) -> Self { // 克隆时强引用计数加1 unsafe { self.counts.as_ref().strong += 1; } Self { handle: self.handle, counts: self.counts, } } } impl Drop for SharedResource { fn drop(&mut self) { unsafe { let counts = self.counts.as_mut(); counts.strong -= 1; if counts.strong == 0 { // 强计数归零时销毁资源 destroy_resource(self.handle); // 简化实现省略弱引用逻辑,直接释放计数块 drop(Box::from_raw(self.counts.as_ptr())); } } } } // 跨线程安全实现,单线程场景可去掉这两个trait实现 unsafe impl Send for SharedResource {} unsafe impl Sync for SharedResource {}
这种方案没有全局状态,逻辑和标准库Arc完全对齐,缺点是每个资源仍需要一次极小的堆分配来存放计数块,相比直接用Arc<Resource>仅省掉了访问句柄时的一次指针解引用,实际性能差异几乎无法感知。
方案2:全局句柄表存计数,栈上仅存句柄
这种方案更贴合你描述的设计,不需要为单个资源做单独堆分配,仅用一个全局映射表维护所有句柄对应的引用计数,每个智能指针实例在栈上仅占一个i32的大小:
use std::collections::HashMap; use std::sync::Mutex; // 全局句柄表:key为资源句柄,value为对应强引用计数 static HANDLE_COUNTS: Mutex<HashMap<i32, usize>> = Mutex::new(HashMap::new()); struct SharedResource { handle: i32, } impl SharedResource { fn new() -> Self { let handle = unsafe { create_resource() }; // 新资源初始引用计数为1 HANDLE_COUNTS.lock().unwrap().insert(handle, 1); Self { handle } } } impl Clone for SharedResource { fn clone(&self) -> Self { let mut table = HANDLE_COUNTS.lock().unwrap(); *table.get_mut(&self.handle).unwrap() += 1; Self { handle: self.handle } } } impl Drop for SharedResource { fn drop(&mut self) { let mut table = HANDLE_COUNTS.lock().unwrap(); let count = table.get_mut(&self.handle).unwrap(); *count -= 1; if *count == 0 { // 计数归零时移除表项、销毁资源 table.remove(&self.handle); // 提前释放锁避免死锁 drop(table); unsafe { destroy_resource(self.handle) }; } } } unsafe impl Send for SharedResource {} unsafe impl Sync for SharedResource {}
这种方案实现逻辑极简,栈上体积最小,但依赖全局可变状态,全局锁在高并发场景下的开销反而可能高于Arc的堆分配开销,仅适合句柄由底层API保证全局唯一的场景。
注:以上两种实现均为原理演示的简化版本,省略了弱引用支持、空值校验、panic安全等边界处理,生产环境直接使用标准库
Arc<Resource>是更稳妥的选择。
内容的提问来源于stack exchange,提问作者Debaug

