2603C++,简单实现协程

如何实现简单协程?

什么是异步高并发,说白了,它就是个可自己控制挂起和继续的函数.

接着100多行代码,让你彻底看清楚,协程到底是怎么回事?

一,为何协程?

写过网络编程的,谁又没被回调地狱折磨过呢?

简单业务,用异步来写,性能是上去了,代码就变成了一坨.

cpp 复制代码
void on_request(Request req) {
    db_query(req.id, [](DBResult res) {
        if (res.ok) {
            rpc_call(res.data, [](RPCResult rpc_res) {
                if (rpc_res.ok) { /*...*/ } else { /*...*/ }
            });
        } else { /*...*/ }
    });
}

逻辑被撕得粉碎,可读性烂到家.

想要的是既有异步的性能,又有同步代码的清爽.

协程,就是为了让你可用同步的写法,干出异步的活儿.

它允许你在耗时操作上挂起,把CPU让给别人,等完成操作了,再从挂起的地方恢复,继续往下走.

二,从玩具到工具,手撸一个分发器

要实现一个可挂起和恢复的函数,只需要三样东西:

1,保存状态,知道执行到哪了,

2,恢复入口,显式从哪继续,

3,及一个分发器,也就是谁来调用.

思路就是,用一个状态变量当PC,用switchcase跳转表.这套组合拳就是无栈协程的精髓.

现在,直接从该思路出发,搭一个可模拟网络分发事件的架子.

需要一个Scheduler分发器,它知道哪些协程就绪队列里,哪些在等待队列中.
协程需要IO时,就从就绪队列移动到等待队列里去,然后让出CPU.

IO事件来了,分发器再把它挪回就绪队列.看代码:

cpp 复制代码
#include <cstdio>
#include <queue>
#include <map>
//`协程`基类和核心宏
struct Coroutine {
    int state = 0;
    bool finished = false;
    virtual ~Coroutine() = default;
    void resume() { if (!finished) step(); }
protected:
    virtual void step() = 0;
};

#define CORO_BEGIN() switch (state) { case 0:
#define CORO_YIELD() do { state = __LINE__; return; case __LINE__:; } while (0)
#define CORO_END() } finished = true;

//分发器
class Scheduler {
    std::queue<Coroutine*> ready_queue;
    std::map<int, std::queue<Coroutine*>> wait_map;
public:
    void spawn(Coroutine* c) { ready_queue.push(c); }

//`协程`调用此函数以等待事件
    void wait_for(Coroutine* c, int event) {
        wait_map[event].push(c);
    }

//外部`事件源`调用此函数以通知事件
    void notify(int event) {
        if (wait_map.count(event)) {
            auto& q = wait_map[event];
            while (!q.empty()) {
                ready_queue.push(q.front());
                q.pop();
            }
            wait_map.erase(event);
        }
    }
    void run() {
        while (!ready_queue.empty()) {
            Coroutine* c = ready_queue.front();
            ready_queue.pop();
            c->resume();
            if (c->finished) delete c;
        }
    }
};

//等待事件的宏
#define SCHED_YIELD_WAIT(sched, event) do { \
    (sched)->wait_for(this, event); \
    state = __LINE__; return; case __LINE__:; \
} while (0)

//模拟网络请求的`协程`
struct NetReader : Coroutine {
    Scheduler* sched;
    const char* name;
    NetReader(Scheduler* s, const char* n) : sched(s), name(n) {}
protected:
    void step() override {
        CORO_BEGIN();
        printf("[%s] 开始,等待IO事件...\n", name);
        SCHED_YIELD_WAIT(sched, 1);

//等待事件1(模拟IO)
        printf("[%s] IO事件到达,处理完成.\n", name);
        CORO_END();
    }
};
int main() {
    Scheduler scheduler;
    scheduler.spawn(new NetReader(&scheduler, "请求A"));
    scheduler.spawn(new NetReader(&scheduler, "请求B"));
    printf("分发器启动,所有`协程`将挂起等待IO...\n");
    scheduler.run();

//`协程`执行到`SCHED_YIELD_WAIT`后挂起
    printf("\n--- 外部IO事件到达 ---\n");
    scheduler.notify(1);
//唤醒所有等待事件1的`协程`
    printf("\n分发器再次运行,处理已就绪的`协程`...\n");
    scheduler.run();
//执行后续逻辑
    return 0;
}

这段代码就是一套缩微的事件驱动协程框架.函数模拟外部世界,先运行,挂起协程;然后通知,模拟IO事件;再运行,唤醒协程继续执行.

这,就是epoll/kqueue这类IO多路复用结合协程底层原理.

三,宏的坑与C++20的春天

上面的这套宏,虽然讲清了原理,但有坑.

比如不能在子函数yield,RAII也容易失效.

幸好C++20带来了语言级的co_await.上面的NetReader,用C++20如下写出来.

cpp 复制代码
#include <iostream>
#include <coroutine>
#include <thread>
struct Task { /*...样板...*/ };
struct awaitable { /*...样板...*/ };

Task net_reader_cpp20(const char* name) {
    std::cout << "[" << name << "] Started, waiting for IO..." << std::endl;
    co_await awaitable{};

//`编译器`在此生成挂起点
    std::cout << "[" << name << "] IO received, finished." << std::endl;
}

//`主`函数示意
int main() {
    net_reader_cpp20("Reader C++20 A");
    ... 实际需要一个分发器来驱动
}

一个co_await``关键字,编译器就帮你做了之前用宏做的所有脏活累活,而且做得更好.但你只有亲手用宏撸一遍,才能真正理解co_await背后,编译器到底帮你省了多大的事.

软件干到头就是硬件,但把软件自身压榨到极限,你就是别人口中的大神.

今天从一个最简单的宏,一步步构建出一个有模有样的协程分发器,最后看到了C++20的方案.这整个过程,比你直接去背C++20``协程的八股文要有值得多.

我把协程的底裤给你扒了,但怎么穿上它,在什么场景下穿,就是你的修行了.

在你的项目里,哪个环节最适合协程来改造?

是网络IO,还是文件读写,或是一些复杂的业务流程状态机呢?

相关推荐
Laurence3 小时前
C++ 引入第三方库(一):直接引入源文件
开发语言·c++·第三方库·添加·添加库·添加包·源文件
蒸汽求职4 小时前
机器人软件工程(Robotics SDE):特斯拉Optimus落地引发的嵌入式C++与感知算法人才抢夺战
大数据·c++·算法·职场和发展·机器人·求职招聘·ai-native
charlee444 小时前
最小二乘问题详解17:SFM仿真数据生成
c++·计算机视觉·sfm·数字摄影测量·无人机航测
Tanecious.4 小时前
蓝桥杯备赛:Day4-P9749 公路
c++·蓝桥杯
旖-旎4 小时前
分治(库存管理|||)(4)
c++·算法·leetcode·排序算法·快速选择算法
Tanecious.5 小时前
蓝桥杯备赛:Day3-P1102 A-B 数对
c++·蓝桥杯
Tanecious.5 小时前
蓝桥杯备赛:Day3-P1918 保龄球
c++·蓝桥杯
良木生香5 小时前
【C++初阶】:C++类和对象(下):构造函数promax & 类型转换 & static & 友元 & 内部类 & 匿名对象 & 超级优化
c语言·开发语言·c++
三雷科技6 小时前
使用 `dlopen` 动态加载 `.so` 文件
开发语言·c++·算法
旖-旎7 小时前
分治(快速选择算法)(3)
c++·算法·leetcode·排序算法·快速选择