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
Promise 状态机与链路追踪上下文传递 Pending Fulfilled Rejected resolve(value) reject(error) 链路追踪关键点: Promise 链中的每个 .then() 都会创建新的微任务,原始的 traceId 必须通过上下文传播机制(如 AsyncLocalStorage)显式传递。 Promise 链示例: fetchData() → .then(parse) → .then(save) → .catch(handleError)

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)是定位异步错误的关键。

事件循环相位(Node.js 模型)与错误传播路径 timers pending callbacks poll check close callbacks 微任务队列 Promise.then / catch process.nextTick 在每个相位后清空 错误传播路径 未捕获异常 → uncaughtException 事件
常见错误定位陷阱: 在事件循环的 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 每个异步资源的创建。后续章节将深入这些机制的实现细节。