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,还是文件读写,或是一些复杂的业务流程状态机呢?

相关推荐
进击的编程浪人2 小时前
c/c++输入方法及对比
c语言·c++·c#
载数而行5203 小时前
QT前置2 可视化文件,QRC文件两种处理
c++·qt·学习
生活很暖很治愈3 小时前
Linux——UDP编程&通信
linux·服务器·c++·ubuntu
共享家95273 小时前
C++ string 类从原理到实战
开发语言·c++
Mr_WangAndy4 小时前
C++数据结构与算法_大数据处理
c++·大数据查重·大数据求topk
ZCollapsar.4 小时前
C++从入门到入土 (5):.C/C++内存管理
c语言·c++·学习
超级哇塞4 小时前
vscode快速验证和团队协作
c++
睡一觉就好了。4 小时前
C++ 模板进阶
c++
一叶落4384 小时前
【LeetCode】1. 两数之和(Two Sum)— 哈希表经典题解(C语言)
数据结构·c++·算法·leetcode