两个时代的并发:ELF vs async/await
Vapor 4 发布于 Swift Concurrency(async/await、Actor)成熟之前,因此其早期 API 大量使用 EventLoopFuture(简称 ELF)。随着 Swift 5.5 引入原生并发,Vapor 4 也逐步提供了 async/await 版本的 API,两者可以共存。
理解两者的区别,不只是语法偏好,更是运行时模型的差异:ELF 是基于回调的 Future/Promise 模式,链式调用(flatMap、map)在编译期难以发现所有问题;async/await 则将异步代码写成同步风格,借助 Swift 的结构化并发(Structured Concurrency)和编译器检查,大大降低了犯错的概率。
| 特性 | EventLoopFuture (ELF) | async/await |
|---|---|---|
| 语法风格 | 链式回调,嵌套可能深 | 顺序线性,与同步代码相似 |
| 错误处理 | .flatMapError 链 | 标准 try/catch |
| 编译器支持 | 较少检查 | 强类型检查,Sendable 保障 |
| 调试栈帧 | 难以追踪 | 保留完整调用栈 |
| Vapor 4 支持 | 全面(老 API) | 推荐(新 API) |
| 性能 | 极高 | 极高(编译为协程) |
核心概念词典
EventLoop
NIO 的单线程事件循环,每个 EventLoop 绑定一个 OS 线程,不断轮询 I/O 事件。Vapor 启动时创建 CPU 核心数个 EventLoop(通常 4-8 个),所有请求分配到某个 EventLoop 上处理,I/O 期间不阻塞线程。
EventLoopFuture<T>
代表一个尚未完成的异步计算,最终会产生类型 T 的值或错误。通过
.map(转换成功值)、.flatMap(链接另一个 Future)、.whenComplete(注册回调)等方法组合。是 Vapor 旧版 API 的基础类型。async/await
Swift 5.5 引入的原生异步语法。
async 函数可以暂停执行(挂起),await 等待异步结果恢复。挂起期间线程不被阻塞,Swift 运行时会调度其他工作。本质上是对 EventLoopFuture 的语法糖,但由编译器和运行时协作实现。Actor
Swift 的引用类型,内部状态保证串行访问(同一时刻只有一个 Task 执行其内部代码),从根本上消除数据竞争。访问 Actor 的方法必须
await,编译器强制此规则。Vapor 限流器等共享状态的组件是 Actor 的典型使用场景。Sendable
Swift 协议,标记一个类型可以安全地在并发域(Task、Actor)之间传递。值类型(struct、enum)通常自动符合 Sendable;引用类型(class)需要手动保证线程安全后才能声明为 Sendable。是 Swift Concurrency 类型安全的基础之一。
Task / TaskGroup
Task 是并发执行的独立工作单元,类似轻量级线程。Task.detached 创建不继承上下文的独立任务;withTaskGroup 创建可结构化管理的任务组,支持并行执行多个异步操作并收集结果。ELF 与 async/await 对比示例
下面用同一个"查询用户及其文章"的场景,分别展示 ELF 和 async/await 两种写法,直观感受可读性差异:
// ── ELF 写法(Vapor 旧式 API)──────────────────────────
app.get("users", ":id", "posts") { req -> EventLoopFuture<[PostResponse]> in
let userID = try req.parameters.require("id", as: UUID.self)
return User.find(userID, on: req.db) // EventLoopFuture<User?>
.unwrap(or: Abort(.notFound)) // 解包 Optional
.flatMap { user in // 链接下一个 Future
user.$posts.query(on: req.db)
.filter(\.$published == true)
.all()
}
.map { posts in // 转换结果
posts.map { PostResponse(from: $0) }
}
}
// ── async/await 写法(推荐)──────────────────────────────
app.get("users", ":id", "posts") { req async throws -> [PostResponse] in
let userID = try req.parameters.require("id", as: UUID.self)
guard let user = try await User.find(userID, on: req.db) else {
throw Abort(.notFound)
}
let posts = try await user.$posts.query(on: req.db)
.filter(\.$published == true)
.all()
return posts.map { PostResponse(from: $0) }
}
并发执行多个异步操作
async/await 的顺序写法默认是串行的——每个 await 等前一个完成再继续。如果几个操作相互独立,用 async let 或 withTaskGroup 可以并行执行,减少总耗时:
// 串行(慢):两次数据库查询依次执行,总耗时 = A + B
let user = try await User.find(userID, on: db)
let posts = try await Post.query(on: db).filter(\.$published==true).all()
// 并行(快):两个查询同时发起,总耗时 = max(A, B)
async let userFetch = User.find(userID, on: db)
async let postsFetch = Post.query(on: db).filter(\.$published==true).all()
let (user, posts) = try await (userFetch, postsFetch)
// TaskGroup:并行处理动态数量的任务
let enrichedPosts = try await withThrowingTaskGroup(of: EnrichedPost.self) { group in
for post in posts {
group.addTask {
let comments = try await post.$comments.query(on: db).count()
return EnrichedPost(post: post, commentCount: comments)
}
}
var results: [EnrichedPost] = []
for try await result in group {
results.append(result)
}
return results
}
多请求并发处理模型
Vapor 多请求并发处理(4 核 CPU = 4 个 EventLoop):
请求队列:req1 req2 req3 req4 req5 req6 ...
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
EL-0 ── [req1] ────── await DB ────────── [req5] ──
│ │ 挂起,不阻塞线程 │
EL-1 ── [req2] ─── await DB ─────────────[req6] ──
│ │
EL-2 ── [req3] ──────── await DB ────────────────
EL-3 ── [req4] ──────────── await DB ────────────
每个 EventLoop 线程:
- 不阻塞,遇到 await 立即处理其他请求
- 4 个线程轻松支撑数千并发连接
- 与"每请求一线程"模型(需要数千线程)形成对比
混用 ELF 和 async/await 时的注意事项
当你在 async 上下文中需要调用返回
EventLoopFuture 的老 API 时,使用 .get() 方法将 ELF 转换为 async:let result = try await someELF.get()。反向转换(async → ELF)使用 req.eventLoop.performWithTask { try await asyncFunc() }。不要跨 EventLoop 传递 Future,使用 .hop(to: eventLoop) 正确切换。
本章小结
Swift Concurrency 的 async/await 相比 EventLoopFuture 显著提升了代码可读性和错误处理能力;async let 和 TaskGroup 实现并行加速;Actor 从语言层面消除数据竞争,是 Swift 并发安全的杀手锏。下一章将把并发能力应用到实时通信场景,实现 WebSocket 聊天室和 SSE 推送。