---
title: "Rust 手写异步 Trait:不用 async_trait,搞懂生命周期、Send/Sync 和动态分发"
slug: "rust-手写异步-trait不用-async_trait搞懂生命周期-send-sync-和动态分发"
canonical_url: "https://blog.youngoing.cn/posts/rust-%E6%89%8B%E5%86%99%E5%BC%82%E6%AD%A5-trait%E4%B8%8D%E7%94%A8-async_trait%E6%90%9E%E6%87%82%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F-send-sync-%E5%92%8C%E5%8A%A8%E6%80%81%E5%88%86%E5%8F%91/"
collection: rust
published_at: 2026-09-08T00:00:00.000Z
updated_at: 2026-09-08T00:00:00.000Z
tags: []
author: youngo
---

## Navigation Context

- Canonical URL: https://blog.youngoing.cn/posts/rust-%E6%89%8B%E5%86%99%E5%BC%82%E6%AD%A5-trait%E4%B8%8D%E7%94%A8-async_trait%E6%90%9E%E6%87%82%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F-send-sync-%E5%92%8C%E5%8A%A8%E6%80%81%E5%88%86%E5%8F%91/
- You are here: Home > Posts > rust > Rust 手写异步 Trait:不用 async_trait,搞懂生命周期、Send/Sync 和动态分发

### Useful Next Links
- [Home](https://blog.youngoing.cn/)
- [rust](https://blog.youngoing.cn/collections/rust/)
- [todo](https://blog.youngoing.cn/collections/todo/)
- [技术](https://blog.youngoing.cn/collections/%E6%8A%80%E6%9C%AF/)
- [测试](https://blog.youngoing.cn/collections/%E6%B5%8B%E8%AF%95/)

> 目标:理解为什么"trait 里的 async 方法"很麻烦,以及不用 `async_trait` 时如何手工写出正确、可跨线程的代码。

### **1\. 先看最终能跑的代码**

```
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

struct User {
    id: String,
    name: String,
    email: String,
    age: u8,
}

// 关键点 1: trait 声明返回 `Pin<Box<dyn Future + Send>>`
// 关键点 2: trait 继承 `Send + Sync`,保证 `dyn` 能跨线程
trait UserService: Send + Sync {
    fn get_user<'a>(&'a self, id: &'a str)
        -> Pin<Box<dyn Future<Output = Option<User>> + Send + 'a>>;
}

struct TestUserService;
impl UserService for TestUserService {
    fn get_user<'a>(&'a self, id: &'a str)
        -> Pin<Box<dyn Future<Output = Option<User>> + Send + 'a>> {
        let id = id.to_string();            // 提前复制,消除借用
        Box::pin(async move {               // 装箱 + 变为 'static
            Some(User {
                id,
                name: "test".to_string(),
                email: "xx@x.com".to_string(),
                age: 11,
            })
        })
    }
}

async fn user_service(service: Arc<dyn UserService>) {
    tokio::spawn(async move {
        let user = service.get_user("123").await;
    });
}

#[tokio::main]
async fn main() {
    user_service(Arc::new(TestUserService)).await;
}
```

var e=\[\];function t(t){if(t.dataset.htmlFrameAuto===\`done\`)return;t.dataset.htmlFrameAuto=\`done\`;let n=t.getAttribute(\`style\`)||\`\`;if(t.classList.contains(\`code-rendered-aspect\`)||n.includes(\`--html-frame-height\`)||n.includes(\`--html-frame-aspect-ratio\`))return;let r=t.querySelector(\`iframe.code-iframe\`);if(!r)return;let i=()=>{try{let e=r.contentDocument;if(!e||!e.body)return;let n=Math.max(e.body.scrollHeight,e.body.offsetHeight);if(!n)return;let i=Math.round(window.innerHeight\*.95),a=Math.min(Math.max(n+12,96),i);t.style.setProperty(\`--html-frame-height\`,\`${a}px\`)}catch{}};e.push(i);let a=()=>{i();try{let e=r.contentDocument;e&&e.body&&\`ResizeObserver\`in window&&new ResizeObserver(()=>i()).observe(e.body)}catch{}};r.contentDocument&&r.contentDocument.readyState===\`complete\`?a():r.addEventListener(\`load\`,a)}function n(){let e=document.querySelectorAll(\`\[data-html-frame\]\`);if(!e.length)return;if(!(\`IntersectionObserver\`in window)){e.forEach(t);return}let n=new IntersectionObserver((e,n)=>{for(let r of e)r.isIntersecting&&(n.unobserve(r.target),t(r.target))},{rootMargin:\`250px 0px\`});e.forEach(e=>n.observe(e))}var r=()=>\`requestIdleCallback\`in window?window.requestIdleCallback(n,{timeout:2e3}):setTimeout(n,200);document.readyState===\`complete\`?r():window.addEventListener(\`load\`,r,{once:!0});var i=0;window.addEventListener(\`resize\`,()=>{i&&cancelAnimationFrame(i),i=requestAnimationFrame(()=>e.forEach(e=>e()))});

### **2\. 背景:为什么 async 方法不能直接写在 trait 里?**

原生 `async fn` 是语法糖,脱糖成返回 `impl Future`(RPITIT):

```
trait UserService {
    // 这只是写法,真正签名是:
    fn get_user<'a>(&'a self, id: &'a str)
        -> impl Future<Output = Option<User>> + 'a;   // 匿名类型,无法命名
}
```

问题在于:**trait 要能被当作** `**dyn UserService**`**(动态分发)用,每个方法的返回类型必须可命名、可写进 vtable。** 而 `impl Future` 是匿名的、大小未知,违反了"对象安全",于是 `Box<dyn UserService>` 直接报 `E0038`。

`async_trait` 的解决办法:把返回类型改写成**可命名、大小已知**的 `Pin<Box<dyn Future>>`,这样 trait 就能对象化。本文就是手动做这件事。

### **3\. 三个核心概念**

#### **3.1 Future 是一台"只能被问进度"的机器**

```
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<T>
```

-   `Poll::Ready(v)` → 完成,给结果
-   `Poll::Pending` → 未完成,过会儿再来问

Future **不会自己跑**,必须由 executor 反复调用 `poll`。所有 `async`/`await` 代码都需要有人驱动,这就是为什么 `main` 里必须有 runtime(如 `#[tokio::main]`)。

#### **3.2 Box::pin 让"未知大小 + 需要固定"的 future 可用**

`dyn Future` 是胖指针,大小未知,不能直接放栈上、不能直接返回。`Box<dyn Future>` 把它放到堆上,大小就固定了。`Pin` 保证它在内存中的地址不被移动——这是 async 状态机的安全前提。

所以 `async fn` 的返回值一律是 `Pin<Box<dyn Future>>`(或 async\_trait 里的变体)。

#### **3.3 Send + Sync 是跨线程的前提**

-   `Send`:该值可以**移动**到另一个线程。
-   `Sync`:该值可以被多个线程**同时共享引用**。

`tokio::spawn` 要求传入的 future 是 `Send`。而 future 会借用 `&self`(`&dyn UserService`),`&T` 要 Send 要求 `T: Sync`。所以:

```
trait UserService: Send + Sync { ... }
```

### **4\. 逐行拆解签名**

#### **4.1 trait 签名**

```
fn get_user<'a>(&'a self, id: &'a str)
    -> Pin<Box<dyn Future<Output = Option<User>> + Send + 'a>>;
```

-   `'a` 是 `&self` 的生命周期(这里也顺手让 `&str` 用同一个 `'a`)。
-   返回的 future 带 `+'a`,意思是:**这个 future 借用了** `**&self**`**,只要** `**&self**` **还活着,它就能被 poll。** 这是"非 static"的生命周期版本。
-   `+ Send` 让 future 能跨线程传给 `tokio::spawn`。

#### **4.2 impl 签名必须与 trait 完全一致**

```
impl UserService for TestUserService {
    fn get_user<'a>(&'a self, id: &'a str)
        -> Pin<Box<dyn Future<Output = Option<User>> + Send + 'a>> { ... }
}
```

如果 impl 写成了不带 `'a` 的 `Pin<Box<dyn Future + Send>>`,就和 trait 的 `+ 'a` 对不上,报:

```
impl item signature doesn't match trait item signature
```

这就是你之前踩的坑:两边生命周期不一致。

#### **4.3 async 块里为什么是 move?**

```
let id = id.to_string();       // 先把借用转成拥有的 String
Box::pin(async move { ... })   // move 把 id 移进闭包
```

`async move` 会把用到的变量**移动进** future,而不是借用。这样 future 就不再借用任何外部参数,生命周期更宽松。因为 `id` 已经是 `String`(owned),所以 future 实际上变成了 `'static`——虽然我们签名里写了 `'a`,但 `'a` 是上界,`'static` 满足它,合法。

### **5\. 生命周期方案对比**

| 方案 | 签名 | 适用场景 |
| --- | --- | --- |
| **借用 &self(非 static)** | `Pin<Box<dyn Future + Send + 'a>>` | 需要持有 `&self` 内部字段引用时 |
| **不借用(static)** | `Pin<Box<dyn Future + Send>>` | 已在进入 async 块前复制/拥有数据(最常见) |

如果你想用"不借用"方案,更简洁:

```
trait UserService: Send + Sync {
    fn get_user(&self, id: &str)
        -> Pin<Box<dyn Future<Output = Option<User>> + Send>>;
}

impl UserService for TestUserService {
    fn get_user(&self, id: &str)
        -> Pin<Box<dyn Future<Output = Option<User>> + Send>> {
        let id = id.to_string();
        Box::pin(async move { Some(User { id, .. }) })
    }
}
```

因为 `id.to_string()` 把数据复制出来了,future 不借用任何东西 → 天然 `'static`。**如果你的方法不依赖** `**&self**` **内部的借用,优先用这个方案**,省去所有生命周期标注。

### **6\. 为什么动态分发 + Send/Sync 一起出现**

-   `user_service(service: Arc<dyn UserService>)` → 用 `Arc` 做动态分发并共享。
-   `tokio::spawn(async move { ... })` → 要求 future `Send`。
-   future 借用了 `service`,`&Arc<dyn UserService>` 要 Send → `dyn UserService` 必须 `Sync`。
-   所以 trait 声明了 `: Send + Sync`。

去掉任意一环(比如不用 `tokio::spawn`,或 `Arc` 改成 `Box`),就不需要 Sync。这也是为什么错误常在 `spawn` 处出现。

### **7\. 什么时候该用 async\_trait 而不是手写?**

|  | 手写 `Pin<Box<dyn Future>>` | `async_trait` |
| --- | --- | --- |
| 代码量 | 多,要手动管生命周期 | 少,直接写 `async fn` |
| 出错率 | 高(签名不匹配、生命周期) | 低,宏自动处理 |
| 每调用一次 | 有一次堆分配(Box) | 同样有堆分配 |
| 可读性 | 差 | 好 |

**结论**:除非你在写底层库、追求极致控制,否则**推荐** `**async_trait**`。手写的价值在于:彻底搞懂 async 方法为什么需要装箱、生命周期怎么标、Send/Sync 从哪来。本文就是这份"底层原理"。

### **8\. 常见错误速查**

| 报错 | 原因 | 修法 |
| --- | --- | --- |
| `E0038: trait ... not dyn compatible` | 返回 `impl Future` 无法对象化 | 返回 `Pin<Box<dyn Future>>` |
| `impl item signature doesn't match trait item signature` | impl 和 trait 生命周期不一致 | 让两者签名逐字一致 |
| `future cannot be sent between threads safely` | future 借用 `&self`,但 `dyn` 无 `Sync` | trait 加 `: Send + Sync` |
| `cannot find lifetime 'self` / `'str` | 无效生命周期名 | 用 `'a` 或省略(`&self`) |