C/C++协程框架原理:从寄存器切换到调度器设计
2026/9/24 23:10:31 网站建设 项目流程

面试考场上聊到协程,十个考生有九个会先背一遍“协程是用户态线程”,然后面试官追问一句“那你说说用户态线程怎么切换的”,场面立刻安静。这个现象我见得太多了。C/C++协程框架原理之所以能成为面试硬核考点,就是因为它是少有的横跨操作系统、编译原理、异步编程范式三个层次的综合性问题。你能把协程讲透,说明你对程序执行模型的理解是成体系的,而不是背了几个API。

这篇文章我会完全站在面试视角,把协程从底层寄存器切换一路拆到调度器设计,再给出几个高频追问的标准答法。全部内容都是我在实际阅读libco、libgo、boost.context以及C++20协程实现时沉淀下来的理解,尽量说人话,不端着。

1. 面试官问协程时,真正想考察的底层认知

绝大多数人对协程的理解停留在“可以暂停的函数”这个层面。这句话没错,但它太表面了,等于没说。面试官真正想确认的,是你是否理解协程为什么能暂停、暂停时发生了什么、恢复时又发生了什么,以及这套机制和线程切换的本质区别在哪里。

1.1 协程的核心不是并发,而是让出执行权

协程的本质是一次函数调用能够被挂起,并在挂起点恢复执行。所谓挂起,就是把当前函数执行到一半的所有状态保存下来,然后这个函数主动把CPU让给别人;所谓恢复,就是把之前保存的状态重新装回去,从挂起的那一刻接着往下跑。

注意两个关键词。第一个是“保存状态”,第二个是“主动让出”。线程切换是内核态完成的,协程切换是在用户态完成的。线程切换需要陷入内核、经过调度器、保存完整的线程上下文,这是一条很重的路径。而协程切换只是保存和恢复几个寄存器的内容,不需要陷入内核,所以它比线程切换快一个数量级。

我在面试里会这样组织答案:把CPU的执行时间想象成一条流水线,线程是流水线上的一道道工序,工位切换需要车间主任统一安排;协程则是每个工位自己决定什么时候停下来换个活干,车间主任完全不用插手。这就是为什么协程被称为“用户态线程”——它把切换这件事从内核搬到了用户态。

1.2 一个最小协程的朴素原型

要真正理解协程,最好的方式是先忘掉所有框架,用一个最原始的思路来手写一个。最早的协程可以通过setjmp/longjmp实现,也可以用ucontext,甚至可以用纯C语言配合switch/case硬造一个。

// 一个用switch/case实现的最简协程 struct Coroutine { int state; // 保存挂起点,这是无栈协程的核心存储 int data; // 协程自己的数据 }; int coroutine_run(struct Coroutine *co) { switch (co->state) { case 0: // 挂起点0:开始执行 co->data = 1; printf("step 1: data=%d\n", co->data); co->state = 1; return 0; // 让出执行权 case 1: // 挂起点1:从上次让出的地方继续 co->data++; printf("step 2: data=%d\n", co->data); co->state = 2; return 0; case 2: // 挂起点2:最后一次执行 printf("step 3: done\n"); return 1; // 协程结束 } return 1; }

这段代码可能看起来很简单,但它完美揭示了协程最底层的机制:一个协程本质上就是一段能够记住自己执行到哪里的代码。state变量就是“上下文”的最小形态,每次进入函数都根据状态跳到对应的位置继续执行。这就是后来C++20无栈协程、Python生成器的全部秘密所在。

当然,这种写法有个致命问题:如果你在函数内部想暂停,只能维护一个一个的case标签,它没法做到“执行到任意一个函数的任意一行代码然后停下来”。这就是有栈协程和无栈协程的分水岭,也是面试必考的一个分化点。

2. 有栈与无栈:面试必须先说清的分岔口

协程按实现方式分两大流派:有栈协程(stackful)和无栈协程(stackless)。这是面试中第一道分岔题。只会背概念不够,得能把两者的机制差异讲出来。

2.1 有栈协程:每个协程都有一个独立的栈空间

有栈协程的实现思路是:每个协程单独分配一块栈内存,切换时保存和恢复的是完整的寄存器上下文(包括栈指针寄存器和指令指针寄存器)。因为协程拥有自己的栈,所以它可以做到“执行到任意嵌套深度时挂起”,无论是深层次函数调用、递归、还是进入第三方库代码,都可以随时让出执行权,恢复时再从原位置继续。

ucontext是Linux上最经典的有栈协程实现接口,libgo、libco这些框架的底层原理都和它千丝万缕。C++20以前的协程方案,比如boost.context、boost.coroutine,也都是有栈协程。有栈协程的优点是灵活,缺点是要为每个协程分配栈内存,如果创建十万个协程,内存开销非常可观。

2.2 无栈协程:整个程序共享一个调用栈,挂起点由编译器记录

无栈协程不拥有独立栈。它的挂起动作是编译器在代码层面完成的——编译器把每个挂起点变成一个状态,协程恢复时根据状态跳到对应的代码位置继续执行。

C++20的co_await、Python的yield、Go的goroutine在语言层面属于另一种复杂情况(Go是官方运行时管理的混合模型),但在C/C++语境里,无栈协程的典型代表就是C++20 Coroutines。它的特点是轻量,创建协程几乎不占额外内存,天然就没有栈空间的开销和栈溢出的担忧。

对比维度有栈协程无栈协程
栈分配每个协程独立栈(4KB-1MB不等)复用线程的栈,无需分配
挂起能力任意嵌套函数中都能挂起只能挂起当前函数,不能跨函数挂起
切换成本保存/恢复完整寄存器上下文只需切换状态枚举
内存开销较大,大量协程时明显极小,可创建百万级
编译器依赖不需要特殊支持需要编译器生成状态机
典型代表ucontext、boost.context、libco、libgoC++20 co_await、Python yield

这里有个面试官特别爱挖的坑:无栈协程为什么不能跨函数挂起?因为整个程序只有一个调用栈,当一个无栈协程调用了一个内部含有co_await的函数时,编译器实际上是把那个函数整个做成了独立的协程状态机,调用它的外层函数也会被拆成状态机的一部分。但如果你调用的第三方库函数没有用co_await、没有经过编译器特殊处理,那它就是无法挂起的普通函数,协程一旦跑进去就只能执行完才能出来。

3. 上下文切换的底层真相:从ucontext到boost.context

有栈协程之所以灵活,靠的是真正意义上的上下文切换。这一节我们深入寄存器层面,看看切换发生时CPU究竟做了什么。

3.1 ucontext:Linux里最直白的上下文接口

ucontext是POSIX标准中提供的API,它把一个协程的上下文完整封装在ucontext_t结构体里。核心方法只有几个:getcontext获取当前上下文、makecontext设置协程入口栈和入口函数、swapcontext切换上下文。

#include <ucontext.h> #include <stdio.h> static ucontext_t ctx_main, ctx_coro; static char coro_stack[64 * 1024]; void coroutine_func(void) { printf("协程运行中\n"); // 主动让出执行权,回到主流程 swapcontext(&ctx_coro, &ctx_main); printf("协程恢复运行\n"); } int main(void) { getcontext(&ctx_coro); ctx_coro.uc_stack.ss_sp = coro_stack; // 协程独立栈 ctx_coro.uc_stack.ss_size = sizeof(coro_stack); ctx_coro.uc_link = &ctx_main; // 协程结束后的去向 makecontext(&ctx_coro, coroutine_func, 0); printf("主流程:启动协程\n"); swapcontext(&ctx_main, &ctx_coro); // 切入协程 printf("主流程:协程让出后回到这里\n"); swapcontext(&ctx_main, &ctx_coro); // 再次切入协程 printf("主流程:结束\n"); return 0; }

这段代码的运行顺序很有画面感。第一次swapcontext让出主流程,CPU跳到协程函数开始执行,协程打印一句话后再次swapcontext把执行权交还主流程。第二次swapcontext又从上次挂起点继续协程的执行,打印“协程恢复运行”,然后协程函数返回,由于uc_link指向了主流程上下文,CPU自动回到主流程。

你观察这个流程会发现:协程的切换本质就是保存当前所有寄存器的值到内存、再把目标协程的寄存器值从内存装载到CPU寄存器。现代CPU上下文保存的寄存器数量大概是几十个,包括通用寄存器、栈指针(SP)、指令指针(PC)、标志寄存器、浮点寄存器等。这比内核线程切换轻多了,因为完全不需要进入内核态。

3.2 boost.context的汇编级黑魔法

ucontext虽然经典,但它有一个致命弱点:它的上下文切换里包含了信号掩码的处理,这涉及到系统调用,性能上不去。所以后来的高性能协程库纷纷转向boost.context或者自己手写汇编切换。

boost.context的核心是fcontext_t类型,搭配make_fcontextjump_fcontext两个函数。jump_fcontext会使用一段精心编写的汇编代码来完成寄存器保存与恢复,同时支持一个额外的void*参数在协程间传递数据。libco就是直接改写了boost.context的汇编代码,针对x86和ARM都做了深度优化。

; boost.context风格切换的核心逻辑(以x86_64为例的示意) ; 保存被调用者保存寄存器(callee-saved registers) push %rbp push %rbx push %r12 push %r13 push %r14 push %r15 mov %rsp, [%rdi] ; 将当前栈指针保存到旧上下文中 mov [%rsi], %rsp ; 从新上下文恢复栈指针 pop %r15 pop %r14 pop %r13 pop %r12 pop %rbx pop %rbp ret ; 跳转到新上下文的执行地址

这里藏着两个相当深的点。第一是为什么只保存callee-saved寄存器而不保存全部寄存器?因为根据调用约定,caller-saved寄存器由调用者自己负责保存,协程切换发生在执行流内部,调用者保存的现场自然会被保留,不需要在切换点重复处理。第二是jump_fcontext从汇编返回时,它会利用栈上预先摆放好的入口地址来跳转,这就是“启动一个全新协程”的原理:make_fcontext把栈指针调整到指定位置,把入口函数地址压栈,切换过去时执行ret就恰好进入了协程函数。

3.3 协程切换为什么比线程快:一次对比实验

既然切换快是协程的关键卖点,我们实际测一下会有更直观的感受。在一台常见Linux服务器上做基准测试,线程切换(用futex或者互斥锁触发)单次大约在1到2微秒,而协程切换(boost.context级别)单次大约在20到50纳秒。换句话说,协程切换比线程快整整一个数量级。

这个差距从根本上来自两条路径的成本。线程切换要走系统调用陷入内核、经过内核调度器、处理上下文切换、再返回到用户态;协程切换只是用户态的几十条指令,纯粹是保存和恢复寄存器。所以在大规模IO并发场景下,协程可以炸出很高的并发能力,而线程会因为频繁切换导致CPU空转在调度上。

4. 回调地狱难题:协程如何重塑异步编程范式

如果协程只是切换快,那它顶多是个性能优化工具,不值得成为面试考点。协程真正的杀手锏,是它改变了异步编程的代码组织方式。这一节我们从编程范式的演进角度来讲协程的价值。

4.1 同步阻塞、多线程、回调:传统并发方案各有什么代价

传统的网络服务处理一个请求,最朴素的做法是每来一个连接就阻塞在一个线程里等待数据。这种方式代码清晰、逻辑直观,但线程数量一旦上来,内存不够用、CPU大量时间花在切换上,服务就垮了。

后来人们用io多路复用加回调函数的方式来解决。一个线程可以同时监听几千个socket事件,有事件来了就调用对应的回调。这个方案在性能上是没问题的,但代码复杂度爆炸。一个业务流程如果分为“等待请求数据”“查询数据库”“调用下游服务”“组装响应”四步,每一步都是异步回调,就要写四层嵌套的回调函数。业务逻辑被拆得七零八落,出问题现场根本没法查。

这个阶段的痛点被业内叫回调地狱,也是协程粉最愿意强调的场景。

4.2 协程的答案:把异步流程写成同步代码

协程的破局点是把挂起和恢复包装成了一种语言级能力。网络库发起异步读操作,不再提供回调,而是让协程发起读之后主动挂起,底层调度器负责监听这个socket的可读事件,事件到了再恢复协程。对于写业务代码的人来说,整个过程看起来就像一次普通的同步调用。

// 回调风格实现网络请求 void on_connect(int fd) { async_read(fd, buffer, [&] { parse_request(buffer); async_query_db(sql, [&] { async_call_downstream(req, [&] { build_response(); async_write(fd, response, [&] { close(fd); }); }); }); }); } // 协程风格实现同一个请求处理流程 void process_connection(int fd) { auto buffer = async_read(fd); // 挂起,等数据 auto request = parse_request(buffer); auto db_result = async_query_db(sql); // 挂起,等数据库 auto downstream_resp = async_call_downstream(req); // 挂起,等下游 auto response = build_response(db_result, downstream_resp); async_write(fd, response); // 挂起,等写完 close(fd); }

同一个业务流程,两种写法的可维护性差了好几倍。协程版本看起来就是一行一行顺序执行的同步代码,有挂起的地方代码不会像回调那样往右边疯长。

4.3 协程不是银弹:它优化的是IO密集而非CPU密集

这里要专门提醒一点,面试时如果能主动说清楚协程的适用边沿,会显得比背书高级很多。协程并不提升单核的绝对计算能力,它优化的是等待过程中的CPU利用效率。一个协程在等网络数据时让出CPU,调度器可以切换到另一个就绪的协程去跑,等数据来了再切回来。

但如果你面对的是纯CPU密集的任务,比如图像渲染、加密解密、大规模矩阵计算,协程几乎帮不上忙。它切换再快,也没有办法凭空多出一颗核心。所以优秀的服务器架构往往是“多线程+协程”的组合——用线程数等于CPU核心数来最大化计算能力,每个线程内部跑协程来消化海量IO请求。

5. 调度器与栈管理:一个协程框架的自我修养

协程本身只是一个能暂停和恢复的函数,它不会自己调度自己。真正支撑起框架能力的,是背后的调度器。这节内容属于面试中的加分项,也是区分“只会用”和“真懂原理”的关键。

5.1 协程状态机:就绪、运行、等待、死亡

所有协程调度器核心都是一个状态机。每个协程在生命周期里至少要经历四种状态:

  • 就绪态:已经可被执行,只是还没轮到CPU
  • 运行态:正在被CPU执行
  • 等待态:在等待某种外部事件(如网络数据、定时器、锁)
  • 死亡态:协程函数已执行完毕

调度器的任务就是在就绪队列里挑一个协程,切入执行,等它主动让出或者等到它因为等待外部事件被挂起,然后回到队列挑下一个。这个过程循环往复,每轮从队列取出协程、切换到它、等它挂起、再切换出来,形成一个完整的调度循环。

5.2 协作式调度:信任模型下如何避免饥饿

协程的调度是协作式的,什么意思?协程自己决定什么时候让出执行权。如果某个协程写了个死循环而且从不主动让出,整个线程里其他协程全部饿死,这种情况在协作式调度里没有太好的办法。

libco、libgo这类框架的处理思路一般是:在协程内部的关键操作点(比如socket读写、sleep、锁等待)调用框架的API时,框架自动触发让出逻辑,从而给其他协程执行机会。风险在于如果协程代码里用了非框架原生的阻塞调用(比如直接调read而不是框架封装的co_read),一旦阻塞,整个线程都会卡住。这是所有用协程写业务的人都要牢记的一条红线。

5.3 栈管理:共享栈与独立栈的权衡艺术

有栈协程的栈管理和内存分配策略,是框架层面的一个核心设计点。独立栈模式下每个协程都有一个固定大小的栈,默认一般是4KB到128KB不等。如果栈太小,深递归会溢出;如果栈太大,十万个协程抢内存。

libco的解决方案是共享栈:一个线程内的多个协程共用一块较大的栈内存,切换时把当前协程栈上的内容拷贝到协程自己保存的私有内存里,切入新协程时再把新协程保存的内容恢复到共享栈上。共享栈吞吐高、内存省,代价是切换时的拷贝开销。

可以这么理解:共享栈是所有协程共用一张办公桌,每次换人干活要把桌面上的文件收进自己的柜子里,换新人再把自己的文件摆上来。独立栈是每个人一张固定办公桌,永远不用收文件,但桌子数量受物理空间限制。

另外现代协程框架普遍还会在栈顶放一个“保护页”,通过设置内存保护属性来捕获栈溢出。一旦出现非法访问,保护页会产生段错误信号,框架就能定位到具体协程,而不是让整个进程直接崩溃。

5.4 定时器与IO事件:协程调度器如何感知“等待”

一个纯调度的循环是跑不远的,因为协程们会为了等IO事件而挂起。调度器必须和事件循环(epoll/kqueue/IOCP)深度绑定。

以Linux上的epoll为例,调度循环里会去epoll_wait等待就绪事件。当某个协程执行到async_read时,它注册一个文件描述符事件到epoll中,然后挂起。调度器回到循环主体进入epoll_wait阻塞等待,一旦这个fd可读,epoll返回,调度器根据fd找到对应的等待协程,把它恢复到就绪队列。这样一来,调度器就把“阻塞等待IO”和“运行协程”统一成了一个循环,而且正常情况下循环不会卡死,因为总有就绪的协程可以跑。

定时器也是同理。框架内部维护一个按超时时间排序的最小堆或者时间轮,每次循环取出最近的超时时间,把这一时间作为epoll_wait的最大等待时长。协程里调co_sleep(1000)实际上就是向定时器注册一个1秒后恢复自己的回调。事件循环和定时器协同,调度器才能处理好大量的超时和等待场景。

6. 主流C/C++协程框架横向对比与面试追问清单

讲完底层原理,最后落地看一下常见的C/C++协程框架各有什么脾气,以及面试中常见的追问环节该怎么答。

6.1 框架对比:腾讯libco、C++20协程、libgo、boost.context

框架类型核心特点适用场景
boost.context有栈协程底层库提供最纯粹的上下文切换原语,不提供调度器自研协程框架的地基
boost.coroutine有栈协程基于boost.context封装,提供协程对象语义老项目用得多
libco(腾讯)有栈协程+调度器大量使用共享栈优化内存,提供socket hook,可以近乎无感地改造阻塞网络代码高并发后端服务,微信后台大规模使用
libgo有栈协程+多线程调度在协程之上还做了多线程工作窃取调度,支持并行执行需要同时利用多核又要协程简单语法的服务
C++20 Coroutines无栈协程语言原生支持,编译器生成状态机,零额外内存新一代C++网络库、生成器、懒计算

这里有个很关键的认知是:boost.contextC++20 Coroutines不能直接对比。boost.context是更底层的汇编切换原语,它之上可以搭出完整的有栈协程框架;C++20则是在语言层面定义了协程的编译器框架。真要选型,应该问的是“我要有栈还是无栈”,而不是“libco还是C++20”。

6.2 libco的socket hook是怎么做到的

libco在面试中是个高频角色,因为它被微信后台大量使用。你可能会觉得神奇:为什么你的代码里没有显式调用libco的co_read,原本的read系统调用却会自动让出协程?

秘密在于动态库符号替换。libco在协程切换到运行态前,会把read/write/recv/send/poll等系统调用的符号替换成自己实现的版本。自己的版本内部会向epoll注册事件并主动让出协程。当事件就绪并被调度器唤醒后,才真正执行原本的系统调用逻辑把数据读回来。

这意味着老代码几乎不需要改动,只要把阻塞式逻辑塞进协程里跑,被拦截的阻塞调用就会自动变为协程挂起。这也是libco最狠的地方——它对业务代码几乎零侵入。不过hook机制同样是双刃剑,非框架感知的阻塞点不在替换清单里就会出现整线程卡死的坑。

6.3 C++20协程的关键部件:promise_type、awaiter、coroutine_handle

C++20协程是新入行的同学最容易晕的部分,因为它的关键字少,但概念抽象。别慌,把它拆成三块看很快就能清醒。

第一块是promise_type。它定义了协程本身的行为:结果怎么存储、怎么在协程结束时设置值和异常、怎么响应co_returnco_await。可以把它理解为协程和外部世界的连接器。第二块是awaiter,它定义了单个挂起操作的细节,核心就三个方法:await_ready判断是否直接继续不需要挂起、await_suspend决定挂起时做什么、await_resume决定恢复后表达式的结果。第三块是coroutine_handle,它是对协程帧的句柄,外部通过它来resume()协程,相当于拿到了一个协程的遥控器。

#include <coroutine> #include <iostream> struct Generator { struct promise_type { int current_value; Generator get_return_object() { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } std::suspend_always yield_value(int value) { current_value = value; return {}; } void return_void() {} void unhandled_exception() { std::terminate(); } }; std::coroutine_handle<promise_type> coro; bool next() { coro.resume(); return !coro.done(); } int value() { return coro.promise().current_value; } }; Generator range(int n) { for (int i = 0; i < n; ++i) co_yield i; } int main() { auto gen = range(5); while (gen.next()) std::cout << gen.value() << " "; }

这个例子是无栈协程的典型应用——惰性生成器。co_yield让协程挂起,外部通过resume()恢复,每次恢复都会继续跑循环体直到下一次co_yield。整个过程没有独立的栈分配,状态全部保存在编译器生成的协程帧里。

6.4 面试高频追问清单

最后给一份面试官常常连环追问的问题清单,附上踩分逻辑。

协程和线程有什么区别?

最核心的是切换机制不同。线程切换由内核调度器完成,需要陷入内核,属于抢占式调度;协程切换在用户态完成,是协作式调度,上下文更轻、切换更快。但协程不解决多核并行问题,它解决的是IO密集场景下CPU等待浪费的问题。

协程会发生死锁吗?

会。只要你在协程里等一个永远等不到的事件,比如一个早期就退出且不会再发送数据的连接,又或者互相等对方的锁,协程一样会挂到天荒地老。更麻烦的是,如果一个协程阻塞在一个非框架管理的调用上,会连累整条线程上的所有协程,这是协作式调度特有的风险。

C++20协程为什么不能跨函数挂起?

C++20是无栈协程模型,没有自己的栈空间,挂起只能发生在编译器可见、被重写为状态机的函数内。如果你在一个函数里调用另一个协程函数,本质上是把新函数也变成了一个可挂起的协程状态机,而不是像有栈协程那样直接嵌套在同一个栈里。

goroutine和C++协程是一回事吗?

不是完全一样。goroutine底层是Go运行时维护的,每个goroutine有自己的栈,初始栈很小(几KB),可以动态增长。它是一种运行在运行时调度器上的有栈协程,并做了工作窃取、GC协作等大量运行时层面的优化。C++20的协程是语言机制,更底层的调度完全交给开发者。真要类比,goroutine更像“由官方运行时托管的有栈协程 + 多线程工作窃取调度器”的完整封装。

实际生产环境你会怎么选型?

如果是从零做新服务,代码允许用C++20,我愿意优先选择无栈协程,内存开销小、和现代编译器集成好。如果是要快速改造存量阻塞代码,libco这种带socket hook的共享栈方案更省事。如果对性能有极致要求,那最终都要接受一个现实:再好的协程框架也拯救不了糟糕的调用和锁设计,协程只是并发工具的一部分,不是全部。

7. 复盘:面试时如何用一份清晰的答案结构击中考点

讲完所有技术细节,我们最后把整个面试回答的思路串一遍。我在面试候选人或模拟面试时,常常发现一个问题:很多人对每一个细节都多少知道一点,但一开口就东一榔头西一棒子。面试官想看到的不是碎片化知识点,而是一条清晰的逻辑线。

我的建议是,被问到协程框架原理时,按照下面这个顺序组织答案,既有深度又有层级。

第一步,先定义。协程是一个可以在执行中途挂起并在未来恢复的函数,挂起本质是保存执行现场,恢复本质是恢复现场继续执行。

第二步,作对比。对比线程切换,点出用户态切换和内核态切换的区别,说明协程切换为什么快。

第三步,讲分类。明确有栈和无栈的分野,说清楚有栈协程独立栈带来的灵活性和内存代价,无栈协程靠编译器生成状态机的轻量性。

第四步,谈调度。从调度器状态机讲到事件循环和定时器,说明协程框架是如何把“挂起-等待-恢复”组织成完整体系的。

第五步,落到实践。给出框架层面的横向对比和选型建议,用真实场景说明什么情况下选什么方案。

这套结构最大的好处是:不管面试官从哪个点打断追问,你的回答都不会乱。因为每一步往下都有底层细节可以展开,而每一步之间都有清晰的逻辑脉络。就算面试官最后真的问到一个你没准备过的深入细节,比如某个汇编指令的具体行为,你也可以用上下文切换的整体认知去推断出一个合理的答案。

我在做协程相关项目的实际感受是:理解这些原理对日常编码最直接的影响,是你能够预判代码在并发场景下的行为。你不会再写出一个阻塞调用拖垮线程里所有协程的代码,也不会在CPU密集任务上傻傻地指望协程能提升性能。框架迭代很快,库也可能更替,但“执行现场保存与恢复”这条线是永远不变的,想清楚这一点,面对任何新出的协程库都只是换了个壳子的问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询