Rust中HashMap大小未知问题:多线程传递动态函数
我用Rust写了个财务交易处理程序,数据量上来后单线程跑太慢。我熟JS、TS、Python和Shell,想靠多线程提速,尝试的代码如下:
for row in lines[1..].iter() { thread::spawn(|| { process_transaction(row, &rules) }); }
其中Rules的定义是:
type CustomRule = Box<dyn Fn(&Transaction) -> &'static str>; type RuleHashMap = HashMap<&'static str, CustomRule>;
Row是带正则匹配、大小比较方法的Transaction结构体,HashMap的键是正则字符串,值是自定义函数——我把函数放Box里是因为要存堆上?设计逻辑是遍历正则模式,运行对应逻辑处理交易,过程中不会修改Transaction或Rules,只输出结果。示例规则:
rules.insert(r"(?i)7-ELEVEN|EXXONMOBIL|CIRCLE K|SUNOCO|SHEETZ|A-PLUS|RACEWAY|SHELLSERVICE|Shell SERVICE|QUICK NEASY|QUICK N EASY|FAS MART|BP|ROYAL MART|CITG|gas|wawa", Box::new(|t:&Transaction|{ match t.less_than(15.0) { true => "expenses:convience", false => "expenses:business:gas" } }));
编译器提示要加Send trait,我修改后:
type CustomRule = dyn Fn(&Transaction) + Send + 'static;
结果出现编译错误:
= help: the trait `Sized` is not implemented for `(dyn for<'r> Fn(&'r Transaction) + Send + 'static)` note: required by a bound in `HashMap`
我是底层编程新手,想理解问题本质,不是瞎抄解决方案,尤其是多线程场景的问题。想问:把自定义规则函数放Box里错了吗?我是不是把问题复杂化了?
解答
1. 为什么出现Sized编译错误?
Rust的HashMap要求存储的键和值都必须实现Sized trait——也就是编译时能确定大小的类型。你修改后的CustomRule是dyn Fn(...) + Send + 'static,这是一个trait对象,属于动态大小类型(DST),编译时无法确定具体大小,所以HashMap不接受直接存储它。
你最初的Box<dyn Fn(...)>定义是正确的:Box是一个固定大小的指针类型(编译时大小就是指针的字节数),它本身实现了Sized,能满足HashMap的要求。
2. Box的使用完全正确
把函数放进Box的思路没问题:Fn trait对象是动态大小的,必须用指针类型(比如Box、&)包裹才能存储在HashMap这类要求Sized的容器里。Box会把函数对象分配到堆上,这是Rust中存储动态大小trait对象的标准做法,你没做错。
3. 多线程场景的正确类型定义
你需要把Send和'static约束加到Box包裹的trait对象上,而不是去掉Box。正确的类型应该是:
type CustomRule = Box<dyn Fn(&Transaction) -> &'static str + Send + 'static>; type RuleHashMap = HashMap<&'static str, CustomRule>;
Send:保证函数对象可以安全地在线程间传递'static:因为线程的生命周期可能超过创建它的作用域,需要确保函数对象的生命周期足够长,避免悬垂引用
另外,你的线程代码还有一个隐藏坑:row是从lines[1..].iter()取的引用,这个引用的生命周期可能比线程短,会导致编译错误。解决方法有两种:
- 如果
Transaction实现了Clone,克隆row后传入线程:
for row in lines[1..].iter().cloned() { let rules_clone = rules.clone(); // 若rules是Arc包裹的,用Arc::clone更高效 thread::spawn(move || { process_transaction(&row, &rules_clone); }); }
- 用
Arc共享rules(因为你不会修改它),避免全量克隆:
use std::sync::Arc; // 初始化时用Arc包裹rules let rules = Arc::new(rules); for row in lines[1..].iter().cloned() { let rules_arc = Arc::clone(&rules); // Arc克隆只是复制指针,开销极小 thread::spawn(move || { process_transaction(&row, &rules_arc); }); }
4. 你没有把问题复杂化
Rust的多线程安全是编译期强制保证的,所以必须明确标注Send这类trait约束,这和你熟悉的JS/Python不同——那些语言靠运行时处理线程安全,而Rust把问题提前到编译阶段,虽然繁琐但能彻底避免运行时崩溃。你的核心思路(多线程并行处理交易、用HashMap存储规则函数)非常合理,只是需要适配Rust的类型系统规则。
内容的提问来源于stack exchange,提问作者object Object

