首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别再学协程异步了!先搞懂什么是例程

别再学协程异步了!先搞懂什么是例程

作者头像
早起的鸟儿有虫吃
发布2026-05-26 19:46:22
发布2026-05-26 19:46:22
580
举报

一、例程和普通函数有什么区别?

定义: 例程就是普通的函数 / 方法,。 它有明确的入口和出口,执行时从入口开始,到出口结束 ,中间不能被随意暂停。

例程和线程/协程中 函数有什么区别?

eph_ll_read是例程, 因为它严格遵守了例程的 5 个核心特征: 线性执行、单一入口出口、独立栈帧、不可主动暂停、调用者阻塞等待。

代码语言:javascript
复制
// src/libcephfs.cc
extern "C" int ceph_ll_read(class ceph_mount_info *cmount, Fh* filehandle,
                            int64_t off, uint64_t len, char* buf)
{
    // 1. 入口:调用时从这里开始执行
    bufferlist bl;  // 在当前线程栈上创建局部变量
    int r = 0;
    
    // 2. 线性执行:按顺序调用内部例程
    r = cmount->get_client()->ll_read(filehandle, off, len, &bl);
    
    if (r >= 0) {
        // 3. 继续线性执行:处理返回结果
        bl.copy(0, bl.length(), buf);
        r = bl.length();
    }
    
    // 4. 唯一出口:执行到这里结束,返回结果
    return r;
    // 5. 栈帧销毁:局部变量bl和r被自动释放
}

MySQL 的 C 客户端 API 也是基于例程设计的

代码语言:javascript
复制
// MySQL的同步查询例程
int mysql_query(MYSQL *mysql, const char *sql)
{
    // 发送查询请求到服务器
    int r = send_query(mysql, sql);
    if (r != 0) {
        return r;
    }
    
    // 阻塞等待服务器响应
    r = read_query_result(mysql);
    
    return r;
}
  • 同样是同步阻塞调用
  • 同样通过返回值表示成功或失败
  • 同样需要传入一个上下文对象(MYSQL*对应ceph_mount_info*

总结:

例程就是一段连续的、不可分割的 CPU 指令序列。 它有且只有一个入口和一个(逻辑上的)出口,执行时从入口开始 ,到出口结束,中间不能被主动暂停

回调函数 100% 是例程

  • 它和普通函数的唯一区别是调用者不同:普通函数由你直接调用,回调函数由系统 / 框架 / 其他线程调用
  • 它的执行行为和普通函数完全一样:调用→执行→返回
  • Ceph 中所有继承自Context的类的complete()方法,都是标准的回调例程

例程本身没有 "同步 / 异步" 属性

  • 同一个例程,既可以被同步调用,也可以被异步调用
  • 例如 Ceph 的bufferlist::copy()函数,你可以直接调用它(同步),也可以把它放到线程池里执行(异步)
  • "同步 / 异步" 是调用方式,不是例程本身的属性

协程内部执行的也是例程

  • 协程只是提供了 "暂停和恢复" 的能力,协程内部的每一段代码,仍然是按照例程的规则线性执行的

Ceph 中的例程实例

  • 普通内部例程:bufferlist::copy()Client::ll_read()
  • API 例程:ceph_ll_read()rados_write()
  • 回调例程:Context::complete()AioCompletion::callback()
  • 协程内部例程:co_await前后的所有代码片段
代码语言:javascript
复制
┌─────────────────────────────────────────────────────────────────┐
│                     异步编程范式(Asynchronous)                 │
│  ┌─────────────────────┐  ┌──────────────────────────────────┐ │
│  │  回调式异步         │  │  协程式异步                      │ │
│  │  (Callback-based)   │  │  (Coroutine-based)               │ │
│  └─────────────────────┘  └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
                              ▲
                              │
┌─────────────────────────────────────────────────────────────────┐
│                     执行载体(Execution Carriers)              │
│  ┌─────────────────────┐  ┌──────────────────────────────────┐ │
│  │  线程(Thread)     │  │  协程(Coroutine)                │ │
│  │  内核态调度         │  │  用户态调度                      │ │
│  │  抢占式             │  │  协作式                          │ │
│  └─────────────────────┘  └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
                              ▲
                              │
┌─────────────────────────────────────────────────────────────────┐
│                     基础执行单元(Basic Unit)                  │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │  例程(Routine)                                        │ │
│  │  函数/方法/子程序/回调函数                              │ │
│  │  调用-执行-返回 线性流                                  │ │
│  │  所有软件的基石                                        │ │
│  └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

二、函数执行慢该如何处理?异步,线程协程什么关系

例程就是设计模式, 例如服务器 来处理

  1. 处理客户端统一请求
  2. 分类处理
  3. 返回复处理 这样函数 称作 例程

问题:例程不具备切换功能,不能阻塞处理,如果处理慢该怎么办?

你知道的,别人也知道

同步调用

异步调用

特性

同步调用

异步调用

执行方式

调用线程阻塞,直到操作完成

调用线程立即返回,操作在后台执行

线程模型

单线程串行执行

多线程并行执行(librados 内部 finisher 线程池)

性能

吞吐量低,受网络 RTT 影响大

吞吐量高,可流水线处理多个请求

结果获取

直接通过返回值获取

通过回调函数或等待 completion 对象获取

适用场景

简单操作、低并发场景

高并发、批量操作、需要高吞吐量的场景

别人知道 我不知道的

异步调用,例如回调函数,当前堆栈都退出,其他空间堆栈执行回调 变量作为访问还存在吗?

Ceph 执行回调处理,lambda 回调“在另外一个空间”,对象会不会被释放?

  • 会,如果你用 [&] 捕获了局部变量,而回调在另一个线程延迟执行,原作用域已结束,对象必然被释放 / 变成悬空引用。
  • 不会,如果你采用按值捕获、shared_ptr 捕获,或保证被引用对象的生命周期长于回调(如全局/长生命周期组件)

最佳实践

  • 使用动态分配的std::function包装 Lambda
  • 优先使用值捕获而不是引用捕获
  • 对于类成员访问,使用shared_ptr确保对象生命周期
  • 考虑使用 Ceph 原生的Context机制,它更符合 Ceph 的设计哲学
代码语言:javascript
复制
auto data = std::make_shared<std::string>("important");


auto *ctx = new LambdaContext([data](int r) {
  std::cout << *data << std::endl;
});

特性

Ceph Context 模式

手动 Lambda 包装模式

内存管理

自动自管理,框架负责

需要手动 new/delete

安全性

极高,几乎不会出错

容易出现内存泄漏和悬空指针

代码复杂度

中等,需要定义子类

低,但需要编写包装代码

与 Ceph 集成

原生支持,无缝集成

需要适配 C 风格回调

功能丰富度

支持组合、链式、并行等

基础功能,需要自己实现

性能

极高,无额外开销

有 std::function 间接调用开销

在 Ceph 里,所有异步回调都继承自 Context, 它的契约是: **当回调被执行(finish)后,由实现者自己负责 delete this**。 如果你用 LambdaContext(实际就是 C_Lambda), 它会在 finishdelete this,因此你不用手动释放,但必须保证闭包里的数据在回调执行前不被释放。


示例代码

代码语言:javascript
复制
#include <iostream>
#include <functional>
#include <thread>
#include <chrono>

// 模拟 Ceph 的 Context 基类
class Context {
public:
    // 异步操作完成后调用 complete,默认会调用 finish 并删除自己
    void complete(int r) {
        finish(r);
        delete this;   // 关键:回调执行后自毁
    }
protected:
    // 子类实现自己的回调逻辑
    virtual void finish(int r) = 0;
    virtual ~Context() {}   // 虚析构,允许子类正确析构
};

// 模拟 LambdaContext,把用户 lambda 包进去
template<typename F>
class LambdaContext : public Context {
    F f;
public:
    LambdaContext(F &&f_) : f(std::forward<F>(f_)) {}
protected:
    void finish(int r) override {
        f(r);   // 执行用户逻辑
    }
};

// 一个简单的异步任务模拟器,类似 Objecter
class AsyncOpRunner {
public:
    // 提交异步操作,稍后会在另一个线程里触发回调
    void submit(Context *ctx) {
        // 这里用线程模拟异步回调
        std::thread([ctx]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            // 模拟操作完成,返回码 0
            ctx->complete(0);
        }).detach();
    }
};

// ==================== 正确示例:按值捕获 ====================
void correct_async_op() {
    std::string data = "important_data";

    // 创建 LambdaContext,按值捕获 data 的副本
    Context *ctx = new LambdaContext([data](int r) {
        // 此时 data 是闭包内部的副本,生命周期与闭包一致
        std::cout << "Callback: data = " << data << ", result = " << r << std::endl;
    });

    AsyncOpRunner runner;
    runner.submit(ctx);

    // data 局部变量在这里会被析构,但闭包里的副本还活着
    std::cout << "Submitted, data local will be destroyed soon.\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 等回调执行完
}

// ==================== 错误示例:引用捕获 ====================
void incorrect_async_op() {
    std::string data = "dangerous_data";

    // 引用捕获!危险!
    Context *ctx = new LambdaContext([&data](int r) {
        // 回调在另一个线程执行时,data 已经被析构 → 悬空引用
        std::cout << "Callback: data = " << data << ", result = " << r << std::endl;
    });

    AsyncOpRunner runner;
    runner.submit(ctx);

    // data 在此函数返回后析构,但回调可能还没执行
}

int main() {
    std::cout << "== Correct async example ==" << std::endl;
    correct_async_op();

    std::cout << "== Incorrect async example (may crash) ==" << std::endl;
    incorrect_async_op(); // 可能崩溃或打印垃圾
    std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 等未定义行为发生
    return 0;
}

关键点解释

  1. Context 自管理内存 complete(r) 里调用 delete this,回调执行完自动释放。你不需要手动 delete ctx,但前提是回调一定会被执行。如果异步操作被取消,Ceph 也会调用 complete(带负值错误码),确保内存释放。没有泄露。
  2. 闭包内数据的生命周期
    • correct_async_op 中,data 按值捕获进 LambdaContext 的闭包,闭包存在 Context 对象内部。Context 对象生命周期由自删除管理,一直存活到回调执行完毕。所以闭包内的 data 副本安全。
    • incorrect_async_op 中,[&data] 只存了引用,Context 本身存活,但它指向的局部变量 dataincorrect_async_op 返回后就析构了,回调执行时就是悬空引用。
  3. 适用于 Ceph 任何地方
    • Objecter::readFinisher::queue 等地方,传递 new C_Lambda(...) 都是一样的模式。
    • 复杂情况通常用 shared_ptrauto sp = std::make_shared<MyObject>(); ... new LambdaContext([sp](int r){ sp->handle(r); });,闭包持有 shared_ptr 增加引用计数,直到回调执行完释放。

结论

遵循 Ceph 的 Context 自管理内存模式时:

  • Context 对象自己 delete this → 你不用管释放,但必须保证它一定被 complete
  • 闭包内的数据必须独立于外部局部变量 → 按值捕获或 shared_ptr 捕获,绝不能引用捕获局部变量。

疑问:调用者为申请内存,回调来释放?

1. 调用者申请,回调负责释放

代码语言:javascript
复制
// 调用者 Context是base 类 继承法类 LambdaContext
Context *ctx = new LambdaContext([data](int r) {
    std::cout << "Callback: data = " << data << ", result = " << r << std::endl;
});
调用对象(lambda、函数指针等)
// 交给异步模块
runner.submit(ctx);

之后,**调用者绝不去 delete ctx**。真正的释放发生在 Context 内部:

代码语言:javascript
复制
// 这是框架(如 Finisher 或 Objecter)最终调用的
void Context::complete(int r) {
    finish(r);
    delete this;   // 回调执行完后自动销毁
}

所以:

  • 申请方new LambdaContext(...) → 对象的所有权转移到异步框架
  • 释放方:异步框架(在回调线程中)通过 ctx->complete(r)delete this

这就是“调用者申请,回调释放”的不对称模式。

2. 为什么是安全的?

因为 Ceph 的异步接口**保证 Context 一定会被 complete**(除非系统崩溃)。 即使操作被取消或出错,框架也会用负值错误码调用 complete,从而触发 delete this,不会泄漏内存。

契约

  • 调用者 new 出来的 Context* 交出去后,所有权已移交,调用者不能再 delete 它,也不能依赖任何生命周期假设。
  • 底层框架必须确保在某个时间点调用 ctx->complete() 来执行回调并销毁对象。

3. 这种不对称的 new/delete 是常见模式

在 C++ 里,只要生命周期管理契约清晰,newdelete 完全可以由不同角色完成。 类似场景:

  • 线程池中提交 new std::function<void()>,线程执行完后 delete
  • 事件驱动框架:new 一个回调对象,事件发生时系统执行并 delete

Ceph 把它标准化为 Context 层级,所有异步回调都按“创建 → 移交所有权 → 框架执行 + 自毁”的规则工作。

疑问2 构造函数输入什么结构?

代码语言:javascript
复制
Context *ctx = new LambdaContext([data](int r) {
    std::cout << "Callback: data = " << data << ", result = " << r << std::endl;
});

这里 LambdaContext 不是函数, 而是一个类模板

1. LambdaContext 是什么?

它是我们先前为模拟 Ceph 写的一个模板类,用来将任何可调用对象(lambda、函数指针等)包装成 Context 的子类。定义大概是:

代码语言:javascript
复制
template<typename F>
class LambdaContext : public Context {
    F f;   // ← 这是它的核心成员变量!
public:
    LambdaContext(F &&f_) : f(std::forward<F>(f_)) {}

protected:
    void finish(int r) override {
        f(r);   // 调用存储的回调
    }
};

2. 它有哪些成员?

就一个关键成员:**F f F 是根据你传入的 lambda 表达式的唯一类型**推导出来的。 对于 [data](int r){ ... } 这个 lambda, 编译器会生成一个匿名的闭包类型,假设叫 __lambda_123,那么 F 就是 __lambda_123,而成员 f 就是那个闭包对象。


3. new LambdaContext([data](int r){...}) 做了什么?

分三步:

  1. 构造临时 lambda 对象 [data](int r){...} 创建了一个闭包对象,data 按值捕获,所以闭包里存了 data 的副本。
  2. 模板参数推导 编译器看到 LambdaContext 的构造函数接受 F&&,自动推导 F 为该 lambda 的类型。
  3. 在堆上构造 LambdaContext<F> 对象
    • 调用构造函数 LambdaContext(F&& f_)
    • std::forward 把 lambda 对象移动到成员 f 里面。
    • 此时,堆上的 LambdaContext 对象内部就持有了那个包含 data 副本的闭包。

实际的 Ceph 代码也类似

Ceph 里真实存在的包装类是 C_LambdaLambdaContext(不同版本可能名字不同),本质上都是一个持有 std::function<void(int)> 或模板类型 FContext 子类。 例如:

代码语言:javascript
复制
// src/include/Context.h
class C_Lambda : public Context {
    std::function<void(int)> f;
public:
    C_Lambda(std::function<void(int)> &&f_) : f(std::move(f_)) {}
    void finish(int r) override { f(r); }
};

调用时就是:

代码语言:javascript
复制
new C_Lambda([data](int r) { ... });

同样是 new 一个类对象,构造参数是你的 lambda,存到 std::function 成员里。

std::forward vs std::move()

unsetunset三、为什么写这篇文章 这个是ARTS打卡一部分unsetunset

unsetunsetARTS打卡unsetunset

ARTS是由左耳朵耗子在极客时间专栏《左耳听风》中发起的一个每周学习打卡计划

  • Algorithm:至少做一个 LeetCode 的[算法]题。
  • Review :阅读并点评至少一篇英文技术文章。主要为了学习英文,如果你英文不行,很难成为技术高手。
  • Tip:学习至少一个技术技巧。主要是为了总结和归纳你日常工作中所遇到的知识点。
  • Share:分享一篇有观点和思考的技术文章。主要为了输出你的影响力,能够输出你的价值
  • 来源:https://time.geekbang.org/column/article/14271

举一反三

学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。 学习是为了改变自己的思考方式,改变自己的思维方式, 改变自己与生俱来的那些垃圾和低效的算法。 总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长

学习并不是为了要记忆那些知识点, 而是为了要找到一个知识的地图, 你在这个地图上能通过关键路径找到你想要的答案,从第一手资料开始对于一个学习者来说,找到优质的信息源可以让你事半功倍。 一方面,就像找到一本很好的武林秘籍一样,而不是被他人翻译过或消化过的,也不会有信息损失甚至有错误信息会让你走火入魔 来源:https://time.geekbang.org/column/article/14321 来源:https://time.geekbang.org/column/article/14360

具体如何

代码语言:javascript
复制
1.  这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题
    
 这个问题非常关键,也就是说,
 你在学习一个技术的时候,需要知道这个技术的成因和目标,
 也就是这个技术的灵魂。
 如果不知道这些的话,那么你会看不懂这个技术的一些设计理念。

2.  这个技术的优势和劣势分别是什么,或者说,这个技术的 trade-off 是什么

任何技术都有其好坏,在解决一个问题的时候,也会带来新的问题。
另外,一般来说,任何设计都有 trade-off(要什么和不要什么),
所以,你要清楚这个技术的优势和劣势,以及带来的挑战。

3.  这个技术适用的场景
    
任何技术都有其适用的场景,离开了这个场景,这个技术可能会有很多槽点
,所以学习技术不但要知道这个技术是什么,还要知道其适用的场景。没有任何一个技术是普适的。
注意,所谓场景一般分为两个,一个是业务场景,一个是技术场景。

4. 技术的组成部分和关键点
    

这是技术的核心思想和核心组件了,也是这个技术的灵魂所在了
学习技术的核心部分是快速掌握关键。

5.  技术的底层原理和关键实现
    

任何一个技术都有其底层的关键基础技术,这些关键技术很可能也是其它技术的关键基础技术。
所以,学习这些关键的基础底层技术,可以让你未来很快地掌握其它技术。
可以参考我在 CoolShell 上写的 Docker 底层技术那一系列文章。

6.  已有的实现和它们之间的对比

一般来说,任何一个技术都会有不同的实现,不同的实现都会有不同的侧重。
学习不同的实现,可以让你得到不同的想法和思路,对于开阔思维,深入细节是非常重要的。

如果这篇文章确实帮助到了你, 希望可以点赞、收藏、关注一下, 这也是我持续创作的最大动力!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、例程和普通函数有什么区别?
    • Ceph 中的例程实例
  • 二、函数执行慢该如何处理?异步,线程协程什么关系
    • 你知道的,别人也知道
    • 别人知道 我不知道的
    • 示例代码
    • 关键点解释
    • 结论
    • 疑问:调用者为申请内存,回调来释放?
    • 1. 调用者申请,回调负责释放
    • 2. 为什么是安全的?
    • 3. 这种不对称的 new/delete 是常见模式
    • 疑问2 构造函数输入什么结构?
    • 1. LambdaContext 是什么?
    • 2. 它有哪些成员?
    • 3. new LambdaContext([data](int r){...}) 做了什么?
    • unsetunset三、为什么写这篇文章 这个是ARTS打卡一部分unsetunset
    • unsetunsetARTS打卡unsetunset
      • 举一反三
      • 具体如何
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档