
定义: 例程就是普通的函数 / 方法,。 它有明确的入口和出口,执行时从入口开始,到出口结束 ,中间不能被随意暂停。
例程和线程/协程中 函数有什么区别?
eph_ll_read是例程, 因为它严格遵守了例程的 5 个核心特征: 线性执行、单一入口出口、独立栈帧、不可主动暂停、调用者阻塞等待。
// 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 也是基于例程设计的
// 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% 是例程
Context的类的complete()方法,都是标准的回调例程✅ 例程本身没有 "同步 / 异步" 属性
bufferlist::copy()函数,你可以直接调用它(同步),也可以把它放到线程池里执行(异步)✅ 协程内部执行的也是例程
bufferlist::copy()、Client::ll_read()ceph_ll_read()、rados_write()Context::complete()、AioCompletion::callback()co_await前后的所有代码片段┌─────────────────────────────────────────────────────────────────┐
│ 异步编程范式(Asynchronous) │
│ ┌─────────────────────┐ ┌──────────────────────────────────┐ │
│ │ 回调式异步 │ │ 协程式异步 │ │
│ │ (Callback-based) │ │ (Coroutine-based) │ │
│ └─────────────────────┘ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲
│
┌─────────────────────────────────────────────────────────────────┐
│ 执行载体(Execution Carriers) │
│ ┌─────────────────────┐ ┌──────────────────────────────────┐ │
│ │ 线程(Thread) │ │ 协程(Coroutine) │ │
│ │ 内核态调度 │ │ 用户态调度 │ │
│ │ 抢占式 │ │ 协作式 │ │
│ └─────────────────────┘ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲
│
┌─────────────────────────────────────────────────────────────────┐
│ 基础执行单元(Basic Unit) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 例程(Routine) │ │
│ │ 函数/方法/子程序/回调函数 │ │
│ │ 调用-执行-返回 线性流 │ │
│ │ 所有软件的基石 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
例程就是设计模式, 例如服务器 来处理
问题:例程不具备切换功能,不能阻塞处理,如果处理慢该怎么办?
同步调用
异步调用
特性 | 同步调用 | 异步调用 |
|---|---|---|
执行方式 | 调用线程阻塞,直到操作完成 | 调用线程立即返回,操作在后台执行 |
线程模型 | 单线程串行执行 | 多线程并行执行(librados 内部 finisher 线程池) |
性能 | 吞吐量低,受网络 RTT 影响大 | 吞吐量高,可流水线处理多个请求 |
结果获取 | 直接通过返回值获取 | 通过回调函数或等待 completion 对象获取 |
适用场景 | 简单操作、低并发场景 | 高并发、批量操作、需要高吞吐量的场景 |
异步调用,例如回调函数,当前堆栈都退出,其他空间堆栈执行回调 变量作为访问还存在吗?
Ceph 执行回调处理,lambda 回调“在另外一个空间”,对象会不会被释放?
[&] 捕获了局部变量,而回调在另一个线程延迟执行,原作用域已结束,对象必然被释放 / 变成悬空引用。shared_ptr 捕获,或保证被引用对象的生命周期长于回调(如全局/长生命周期组件)最佳实践:
std::function包装 Lambdashared_ptr确保对象生命周期Context机制,它更符合 Ceph 的设计哲学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), 它会在 finish 里 delete this,因此你不用手动释放,但必须保证闭包里的数据在回调执行前不被释放。
#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;
}
complete(r) 里调用 delete this,回调执行完自动释放。你不需要手动 delete ctx,但前提是回调一定会被执行。如果异步操作被取消,Ceph 也会调用 complete(带负值错误码),确保内存释放。没有泄露。correct_async_op 中,data 按值捕获进 LambdaContext 的闭包,闭包存在 Context 对象内部。Context 对象生命周期由自删除管理,一直存活到回调执行完毕。所以闭包内的 data 副本安全。incorrect_async_op 中,[&data] 只存了引用,Context 本身存活,但它指向的局部变量 data 在 incorrect_async_op 返回后就析构了,回调执行时就是悬空引用。Objecter::read、Finisher::queue 等地方,传递 new C_Lambda(...) 都是一样的模式。shared_ptr:auto sp = std::make_shared<MyObject>(); ... new LambdaContext([sp](int r){ sp->handle(r); });,闭包持有 shared_ptr 增加引用计数,直到回调执行完释放。遵循 Ceph 的 Context 自管理内存模式时:
delete this → 你不用管释放,但必须保证它一定被 complete。shared_ptr 捕获,绝不能引用捕获局部变量。// 调用者 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 内部:
// 这是框架(如 Finisher 或 Objecter)最终调用的
void Context::complete(int r) {
finish(r);
delete this; // 回调执行完后自动销毁
}
所以:
new LambdaContext(...) → 对象的所有权转移到异步框架。ctx->complete(r) → delete this。这就是“调用者申请,回调释放”的不对称模式。
因为 Ceph 的异步接口**保证 Context 一定会被 complete**(除非系统崩溃)。
即使操作被取消或出错,框架也会用负值错误码调用 complete,从而触发 delete this,不会泄漏内存。
契约:
new 出来的 Context* 交出去后,所有权已移交,调用者不能再 delete 它,也不能依赖任何生命周期假设。ctx->complete() 来执行回调并销毁对象。在 C++ 里,只要生命周期管理契约清晰,new 和 delete 完全可以由不同角色完成。
类似场景:
new std::function<void()>,线程执行完后 delete。new 一个回调对象,事件发生时系统执行并 delete。Ceph 把它标准化为 Context 层级,所有异步回调都按“创建 → 移交所有权 → 框架执行 + 自毁”的规则工作。
Context *ctx = new LambdaContext([data](int r) {
std::cout << "Callback: data = " << data << ", result = " << r << std::endl;
});
这里 LambdaContext 不是函数, 而是一个类模板
LambdaContext 是什么?它是我们先前为模拟 Ceph 写的一个模板类,用来将任何可调用对象(lambda、函数指针等)包装成 Context 的子类。定义大概是:
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); // 调用存储的回调
}
};
就一个关键成员:**F f。
F 是根据你传入的 lambda 表达式的唯一类型**推导出来的。
对于 [data](int r){ ... } 这个 lambda, 编译器会生成一个匿名的闭包类型,假设叫 __lambda_123,那么 F 就是 __lambda_123,而成员 f 就是那个闭包对象。
new LambdaContext([data](int r){...}) 做了什么?分三步:
[data](int r){...} 创建了一个闭包对象,data 按值捕获,所以闭包里存了 data 的副本。LambdaContext 的构造函数接受 F&&,自动推导 F 为该 lambda 的类型。LambdaContext<F> 对象LambdaContext(F&& f_)std::forward 把 lambda 对象移动到成员 f 里面。LambdaContext 对象内部就持有了那个包含 data 副本的闭包。实际的 Ceph 代码也类似
Ceph 里真实存在的包装类是 C_Lambda 或 LambdaContext(不同版本可能名字不同),本质上都是一个持有 std::function<void(int)> 或模板类型 F 的 Context 子类。
例如:
// 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); }
};
调用时就是:
new C_Lambda([data](int r) { ... });
同样是 new 一个类对象,构造参数是你的 lambda,存到 std::function 成员里。
std::forward vs std::move()
ARTS是由左耳朵耗子在极客时间专栏《左耳听风》中发起的一个每周学习打卡计划
学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。 学习是为了改变自己的思考方式,改变自己的思维方式, 改变自己与生俱来的那些垃圾和低效的算法。 总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长
学习并不是为了要记忆那些知识点, 而是为了要找到一个知识的地图, 你在这个地图上能通过关键路径找到你想要的答案,从第一手资料开始对于一个学习者来说,找到优质的信息源可以让你事半功倍。 一方面,就像找到一本很好的武林秘籍一样,而不是被他人翻译过或消化过的,也不会有信息损失甚至有错误信息会让你走火入魔 来源:https://time.geekbang.org/column/article/14321 来源:https://time.geekbang.org/column/article/14360
1. 这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题
这个问题非常关键,也就是说,
你在学习一个技术的时候,需要知道这个技术的成因和目标,
也就是这个技术的灵魂。
如果不知道这些的话,那么你会看不懂这个技术的一些设计理念。
2. 这个技术的优势和劣势分别是什么,或者说,这个技术的 trade-off 是什么
任何技术都有其好坏,在解决一个问题的时候,也会带来新的问题。
另外,一般来说,任何设计都有 trade-off(要什么和不要什么),
所以,你要清楚这个技术的优势和劣势,以及带来的挑战。
3. 这个技术适用的场景
任何技术都有其适用的场景,离开了这个场景,这个技术可能会有很多槽点
,所以学习技术不但要知道这个技术是什么,还要知道其适用的场景。没有任何一个技术是普适的。
注意,所谓场景一般分为两个,一个是业务场景,一个是技术场景。
4. 技术的组成部分和关键点
这是技术的核心思想和核心组件了,也是这个技术的灵魂所在了
学习技术的核心部分是快速掌握关键。
5. 技术的底层原理和关键实现
任何一个技术都有其底层的关键基础技术,这些关键技术很可能也是其它技术的关键基础技术。
所以,学习这些关键的基础底层技术,可以让你未来很快地掌握其它技术。
可以参考我在 CoolShell 上写的 Docker 底层技术那一系列文章。
6. 已有的实现和它们之间的对比
一般来说,任何一个技术都会有不同的实现,不同的实现都会有不同的侧重。
学习不同的实现,可以让你得到不同的想法和思路,对于开阔思维,深入细节是非常重要的。
如果这篇文章确实帮助到了你, 希望可以点赞、收藏、关注一下, 这也是我持续创作的最大动力!