如何在其他线程构造!Send对象?析编译报错原因及解决方法
我有如下代码:
trait Foo { fn do_it(&self) -> (); } struct FooImpl { make_this_not_send: *const (), } impl Foo for FooImpl { ... } fn spawn(new: impl FnOnce() -> Box<dyn Foo> + Send + 'static) { tokio::spawn(async move { let foo = new(); loop { foo.do_it(); // ... } }); }
这段代码无法编译,因为Foo并非Send。我并不理解为何需要它是Send——我只想在某个线程中创建foo并让它留在那里,本以为上述new()方式能实现,但它仍要求Foo是Send。
疑问
- 为何需要
Foo是Send? - 解决该问题的惯用方法是什么?
为什么需要Send?
Tokio的spawn函数要求传入的异步任务必须实现Send,因为Tokio运行时会在多个线程间调度任务。尽管你是在任务内部创建foo,但foo作为异步任务环境的一部分,整个任务要满足Send约束,就要求Box<dyn Foo>必须是Send。
Box<dyn Foo>要成为Send,需要两个前提:
- 实现
Foo的具体类型(这里是FooImpl)本身是Send - trait对象被明确标注为
Send(即dyn Foo + Send)
你的FooImpl包含裸指针*const (),裸指针默认是Send的,但dyn Foo没有Send约束,编译器无法确保所有Foo的实现都满足Send,因此Box<dyn Foo>不被视为Send,最终导致整个异步任务不满足Send,无法通过tokio::spawn的检查。
惯用解决方法
方法1:给trait对象添加Send约束
修改spawn函数的参数类型,明确要求返回的trait对象必须实现Send:
fn spawn(new: impl FnOnce() -> Box<dyn Foo + Send> + Send + 'static) { tokio::spawn(async move { let foo = new(); loop { foo.do_it(); // ... } }); }
这样编译器能确认返回的trait对象同时满足Foo和Send,而FooImpl本身是Send,因此可以通过编译。
方法2:将trait本身标记为Send
如果Foo的所有实现都应该是Send,可以直接在trait定义中添加Send约束:
trait Foo: Send { fn do_it(&self) -> (); }
此时dyn Foo自动等价于dyn Foo + Send,无需在返回类型中额外标注,代码更简洁。
方法3:使用LocalSet运行非Send任务
如果你的任务完全不需要跨线程调度,可以用Tokio的LocalSet来执行非Send的异步任务:
use tokio::runtime::Runtime; fn spawn(new: impl FnOnce() -> Box<dyn Foo> + 'static) { let rt = Runtime::new().unwrap(); rt.block_on(async { tokio::task::LocalSet::new().run_until(async move { let foo = new(); loop { foo.do_it(); // ... } }).await; }); }
注意LocalSet中的任务只能在当前线程执行,无法被Tokio调度到其他线程,适合确实不需要跨线程的场景。
内容的提问来源于stack exchange,提问作者milck

