You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在其他线程构造!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。

疑问

  1. 为何需要Foo是Send?
  2. 解决该问题的惯用方法是什么?

回答

为什么需要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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 16:40:14