在C/C++中使用coroutine、setjmp和longjmp的优势有哪些?

该图片来自Stack Overflow上《Practical usage of setjmp and longjmp in C》的回答。
你说得没错,协程从直观感受上确实像“并行”的两个执行流,但本质上还是单线程内的上下文切换,并没有真正的并行执行——这一点和状态机是一致的。但为啥大家还会用setjmp/longjmp或者协程来实现这类逻辑呢?核心原因还是代码的可读性、可维护性,以及逻辑的自然表达,咱们具体拆解来看:
一、逻辑的“线性表达” vs 状态机的“碎片化拆分”
你用状态机实现的方式,需要把原本连贯的逻辑拆成一个个case块,每一块都要手动管理状态切换——比如Process A的A1做完要切到B1,B1做完再切回A2,整个逻辑被拆得七零八落。如果逻辑复杂一点,比如有多个分支、嵌套的等待逻辑,状态机的switch会变得无比臃肿,每个状态块都要记得“我接下来要跳去哪个状态”,很容易出错。
而用setjmp/longjmp或者协程的话,你可以把每个执行流写成线性的代码:比如Process A就是从头写到尾的函数,需要切换的时候直接“挂起”,下次恢复的时候从挂起点继续执行——完全不用手动拆分状态,逻辑和你写普通单线程代码一样自然。举个简单的例子:
// 协程风格的Process A void coroutine_A() { do_A1(); yield(); // 切换到Process B do_A2(); yield(); // 再切换回去 do_A3(); } // 协程风格的Process B void coroutine_B() { do_B1(); yield(); // 切换回Process A do_B2(); yield(); // 切换回去 do_B3(); }
这种写法和你直观理解的“两个执行流交替运行”完全一致,读起来、写起来都不用在脑子里反复跳转状态,成本低太多。
二、自动维护上下文 vs 手动保存状态
状态机的另一个痛点是手动保存上下文:如果你的Process A在执行A1的时候有局部变量(比如循环计数器、临时计算结果),切换到B1的时候必须手动把这些变量存在全局或者堆上的状态结构体里,下次切回来再手动恢复。变量多了的话,这个保存/恢复的过程会无比繁琐,还容易漏存或者存错。
而setjmp/longjmp会自动保存寄存器状态和栈帧(注意longjmp的栈帧安全问题,但这是另一个话题),协程则更完善,会把整个执行流的上下文(栈、寄存器、局部变量)都帮你存好——你完全不用关心这些细节,只要专注于业务逻辑就行。
三、应对复杂逻辑的扩展性
如果你的业务逻辑有复杂的分支、嵌套的等待(比如Process A需要等待Process B完成某个子任务,然后再继续执行某个分支),状态机的实现会变得异常复杂:你需要为每个分支新增状态,还要处理各种状态之间的跳转关系,很容易陷入“状态爆炸”的困境。
而协程或者基于setjmp/longjmp的实现,天然支持这种嵌套和分支逻辑——因为每个执行流都是独立的函数,你可以用普通的if/else、for/while来控制流程,切换逻辑只需要在需要暂停的地方调用yield或者longjmp,逻辑复杂度不会随着分支增加而指数上升。
四、一些场景下的性能优势
虽然状态机的性能通常不错(因为没有上下文切换的开销),但在某些场景下,协程或者setjmp/longjmp的实现反而更高效:比如当状态切换非常频繁,或者状态需要保存的变量很多时,手动保存/恢复状态的开销可能比自动上下文切换更大。而且协程的上下文切换开销比线程小得多,适合高并发的场景。
当然,setjmp/longjmp也有明显的缺点:比如它破坏了C语言的函数调用栈结构,容易导致内存泄漏、调试困难;而协程则更安全、更易用,现在很多语言(比如Go、Python)都内置了协程支持,C语言也有libco这样的库可以用。
总结一下:状态机适合简单的、状态较少的场景,代码轻量、性能好;但当逻辑复杂、状态多、需要自然表达执行流时,setjmp/longjmp或者协程能让你的代码更易读、更易维护,开发效率更高。
内容的提问来源于stack exchange,提问作者JustWe

