C语言标准库之setjmp
协程的介绍
协程(coroutine),意思就是“协作的例程”(co-operative routines),最早由Melvin Conway在1963年提出并实现。跟主流程序语言中的线程不一样,线程属于侵入式组件,线程实现的系统称之为抢占式多任务系统,而协程实现的多任务系统成为协作式多任务系统。线程由于缺乏yield语义,所以运行过程中不可避免需要调度,休眠挂起,上下文切换等系统开销,还需要小心使用同步机制保证多线程正常运行。而协程的运行指令系列是固定的,不需要同步机制,协程之间切换也只涉及到控制权的交换,相比较线程来说是非常轻便的。不过同一时刻可以有多个线程运行,但却只能有一个协程运行。
实际上协程的概念比线程还要早,按照 Knuth 的说法“子例程是协程的特例”,一个子例程就是一次子函数调用,那么实际上协程就是类函数一样的程序组件,你可以在一个线程里面轻松创建数十万个协程,就像数十万次函数调用一样。只不过子例程只有一个调用入口起始点,返回之后就结束了,而协程入口既可以是起始点,又可以从上一个返回点继续执行,也就是说协程之间可以通过 yield 方式转移执行权,对称(symmetric)、平级地调用对方,而不是像例程那样上下级调用关系。当然 Knuth 的“特例”指的是协程也可以模拟例程那样实现上下级调用关系,这就叫非对称协程(asymmetric coroutines)。
setjmp.h
setjmp/longjmp 其实是C语言标准库中的内容,它被定义在<setjmp.h>头文件中,我认识的相当部分的人包括写过很多年C/C++的都表示没听过,并且他们在了解了一些setjmp的特性和功能之后还不以为然,说我又不会用到它;然而你们想过为什么标准库中会去实现一个相对这么怪异特性的语法支持?原因很简单,就是为了实现协程(coroutine),如果你一开始就给自己定位成协程的使用者,不关心它具体怎么实现的,甚至给自己定位成从不用协程,后面的内容你放心可以直接略过。
我们首先来看 setjmp/longjmp 这两个函数的定义。
int setjmp( jmp_buf _Buf ); void longjmp( jmp_buf _Buf, int _Value);
使用注意事项:
1、setjmp与longjmp结合使用时,它们必须有严格的先后执行顺序,也即先调用setjmp函数,之后再调用longjmp函数,以恢复到先前被保存的“程序执行点”。否则,如果在setjmp调用之前,执行longjmp函数,将导致程序的执行流变的不可预测,很容易导致程序崩溃而退出
2、longjmp必须在setjmp调用之后,而且longjmp必须在setjmp的作用域之内。具体来说,在一个函数中使用setjmp来初始化一个全局标号,然后只要该函数未曾返回,那么在其它任何地方都可以通过longjmp调用来跳转到 setjmp的下一条语句执行。实际上setjmp函数将发生调用处的局部环境保存在了一个jmp_buf的结构当中,只要主调函数中对应的内存未曾释放 (函数返回时局部内存就失效了),那么在调用longjmp的时候就可以根据已保存的jmp_buf参数恢复到setjmp的地方执行。
说白一点就是:在使用 setjmp 时,最常见的一个错误用法就是对它做封装,不应该封装在一个函数中。比如:
int try(breakpoint bp) { return setjmp(bp->jb); } void throw(breakpoint bp) { longjmp(bp->jb,1); }
这样写并不会引起编译错误,但是极易容易发生运行时错误,因为setjmp的栈是在try函数中,而下一次调用longjmp的时候try函数可能已经不在栈中被清除了。
来个简单的例子:
#include <stdio.h> #include <setjmp.h> jmp_buf buf; void second() { printf("second\n"); longjmp(buf, 1); } void first() { second(); printf("first\n"); } int coro_main() { if ( !(setjmp(buf)) ) { first(); } else { printf("main\n"); } return 0; }
输出结果:
second
main
除此之外还有广为使用的C语言协程非标准库有 ucontext,据我所知ucontext应用更广泛一些,网上绝大多数 C 协程库也是基于 ucontext 组件实现的。有空下次再去研究研究它。。。