1. 异步编程基础:回调函数、Future/Promise模式、协程与事件循环
在分布式开发环境中,异步任务编排是链路追踪与错误定位的基石。本章从最基础的异步编程概念出发,厘清回调函数、Future/Promise、协程与事件循环的核心机制,为后续的链路追踪技术打下理论基础。
1.1 回调函数(Callback)
回调函数是最原始的异步编程范式。它将一个函数作为参数传递给异步操作,当操作完成时,系统调用该函数以处理结果。
核心特征: 非阻塞调用 + 结果处理分离。调用方不等待结果,而是注册一个“后续动作”。
// 伪代码:回调函数示例
function fetchData(callback) {
// 模拟异步网络请求
setTimeout(() => {
const data = { id: 1, name: "service-a" };
callback(null, data); // 第一个参数为错误,第二个为结果
}, 100);
}
fetchData((err, result) => {
if (err) {
console.error("请求失败:", err);
return;
}
console.log("收到数据:", result);
});
| 特性 | 说明 | 常见问题 |
|---|---|---|
| 简单直接 | 无需额外库,语言原生支持 | 嵌套过深导致“回调地狱” |
| 错误处理分散 | 每个回调需单独处理错误 | 遗漏错误捕获导致静默失败 |
| 控制流隐式 | 执行顺序由回调注册顺序决定 | 难以进行链路追踪与超时控制 |
链路追踪陷阱: 回调函数会切断调用栈(Call Stack)。当异常发生时,错误堆栈中丢失了“谁发起了这个异步调用”的上下文信息,这是分布式追踪需要解决的核心问题之一。
1.2 Future / Promise 模式
Future(在 JavaScript 中称为 Promise)是对异步操作结果的占位符。它提供了一种更结构化的方式来处理异步结果,将“发起调用”与“消费结果”解耦。
// JavaScript Promise 示例
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve({ id: 1, name: "service-a" });
} else {
reject(new Error("网络超时"));
}
}, 100);
});
}
fetchData()
.then(data => console.log("数据:", data))
.catch(err => console.error("错误:", err));
| 对比维度 | 回调函数 | Future / Promise |
|---|---|---|
| 结果获取 | 通过参数传递 | 通过 .then() / .await 获取 |
| 错误传播 | 手动传递 err 参数 | 自动沿 Promise 链传播 |
| 链式编排 | 嵌套回调,难以阅读 | 链式调用,扁平化结构 |
| 状态管理 | 无状态,仅执行函数 | 三种状态:Pending / Fulfilled / Rejected |
1.3 协程(Coroutine)与 async/await
协程是一种用户态的轻量级线程,允许函数在执行过程中暂停(yield)并在后续恢复。async/await 语法糖让协程的编写方式与同步代码几乎一致,极大地提升了异步代码的可读性。
// Python 协程示例
import asyncio
async def fetch_data(url):
print(f"开始请求: {url}")
await asyncio.sleep(1) # 模拟 IO 等待,此处暂停但不阻塞线程
return {"url": url, "status": 200}
async def main():
# 并发执行两个协程
task1 = asyncio.create_task(fetch_data("http://service-a/api"))
task2 = asyncio.create_task(fetch_data("http://service-b/api"))
result1 = await task1
result2 = await task2
print(f"结果: {result1}, {result2}")
asyncio.run(main())
协程 vs 线程: 协程的切换由程序自身控制(非抢占式),开销远小于操作系统线程。在 IO 密集型场景下,单线程可以管理数十万协程,而线程池通常只能支撑数千。
| 特性 | 协程 | 操作系统线程 |
|---|---|---|
| 调度方式 | 用户态协作式 | 内核态抢占式 |
| 切换开销 | ~1μs(函数调用级别) | ~1-10μs(上下文切换) |
| 内存占用 | ~2KB 栈 | ~1MB 栈 |
| 并发数 | 数十万级别 | 数千级别 |
| 链路追踪难度 | 需手动传递上下文 | 可利用 ThreadLocal |
1.4 事件循环(Event Loop)
事件循环是异步运行时的核心引擎。它不断检查任务队列,将就绪的回调、微任务或协程调度执行。理解事件循环的相位(Phase)是定位异步错误的关键。
常见错误定位陷阱: 在事件循环的 poll 相位中抛出的异常,如果未被 try/catch 捕获,会直接触发 uncaughtException 事件,导致进程崩溃。在链路追踪中,这种异常往往丢失了原始的调用链上下文,需要通过 async_hooks 或 AsyncLocalStorage 在创建异步资源时捕获上下文。
1.5 四种异步模型对比总结
| 模型 | 可读性 | 错误处理 | 上下文保持 | 适用场景 |
|---|---|---|---|---|
| 回调函数 | 差(嵌套地狱) | 手动传递 | 丢失 | 简单事件处理 |
| Future/Promise | 中(链式调用) | 自动传播 | 需显式传递 | 单次异步操作 |
| 协程 async/await | 好(同步风格) | try/catch 统一 | 需上下文库 | 复杂编排、高并发 |
| 事件循环 | 隐式调度 | 全局捕获 | 依赖 async_hooks | IO 多路复用 |
链路追踪核心启示: 无论使用哪种异步模型,分布式链路追踪都需要解决同一个问题——跨异步边界的上下文传播。回调函数丢失调用栈,Promise 链需要手动传递 traceId,协程依赖语言运行时(如 Java 的 ThreadLocal 或 Node.js 的 AsyncLocalStorage),事件循环则需要 hook 每个异步资源的创建。后续章节将深入这些机制的实现细节。